基于 AVOS Cloud 的一对多、多对多数据建模

产品需求

demo (1)

假设有一个类似于 Instagram 的产品,核心的数据类型可能包括:用户(User),图片(Image),评论(Comment),点赞(Like)。他们之间的关系如下:

  • 对于每张图片(Image),有一个publisher,是一个User对象的实例;
  • 每张图片,可能会有很多Comment,每条Comment会包含一段文字说明和一个Creator(也是一个User对象的实例);
  • 每张图片还会有很多人点赞(Like),我们可以根据图片找到所有点赞的人,也可以根据人找到他所有赞过的图片。

对于这样的模型,在 MySQL 中我们能很容易地通过主键、外键建立关联,但在 AVOS Cloud 里面如何表现出来呢?下面我会为大家仔细说明一下如何处理这种复杂的数据建模。

数据模型

由于图片和发布者是紧密联系在一起的,它们是一对一的关系,这在 AVOS Cloud 的数据模型里面可以使用在一个 Image Object 中直接保存另一个 User Object 的 pointer 实现;图片和评论之间则是一对多的关系,由于评论都是随图片一起出现的,所以可以在一个 Image Object 中保存另外一类 Comment Object 的实例数组来实现;至于点赞这一个操作,由于它链接了人和图片两方,并且也需要双向查询,所以是多对多的关系,可以使用 AVOS Cloud 提供的 AVRelation 来实现,这也是 AVOS Cloud 提供的数据模型中最复杂的部分(详见 关联查询)。

下面我们来看一下具体如何实现这种一对一、一对多、多对多的数据映射关系。
User 对象我们可以直接使用 AVUser,不必再额外定义一个新的数据类型。而对于 Image 类,则可以这样声明(注意这里我们使用了子类化):

@AVClassName("Image")
public class Image extends AVObject{
    public Image() {
        super();
    }

    public AVUser getPublisher() {
        return (AVUser)super.getAVUser("publisher");
    }
    public void setPublisher(AVUser user) {
        super.put("publisher", user);
    }

    public String getCaption() {
        return getString("caption");
    }
    public void setCaption(String caption) {
        put("caption", caption);
    }
    public String getTakenAt() {
        return super.getCreatedAt().toString();
    }

    public AVFile getRawImage() {
        return super.getAVFile("imageFile");
    }
    public void setRawImage(AVFile file) {
        super.put("imageFile", file);
    }

    @SuppressWarnings("unchecked")
    public List getComments() {
        return (List)getList("comments");
    }
    public void addComment(Comment com) {
        addUnique("comments", com);
    }
} 

Comment 的声明是这样的:

@AVClassName("Comment")
public class Comment extends AVObject{
    public Comment() {
        super();
    }
    public String getContent() {
        return getString("content");
    }
    public void setContent(String value) {
        put("content", value);
    }
    public void setCreator(AVUser user) {
        put("creator", user);
    }
    public AVUser getCreator() {
        return getAVUser("creator");
    }
}

对于Like,我们使用 AVRelation 来链接 Image 和 AVUser,给 Image 类增加如下属性和方法:

@AVClassName("Image")
public class Image extends AVObject{
    ...
    public AVRelation getLiker() {
        AVRelation relation = getRelation("likes");
        return relation;
    }
    public void removeLiker(AVUser user) {
        AVRelation users = getLiker();
        users.remove(user);
        this.saveInBackground();
    }
    public void addLiker(AVUser user) {
        AVRelation users = getLiker();
        users.add(user);
        this.saveInBackground();
    }
    ...
} 

并且,为了展示方便,我们给 Image 类增加一个非持久化的属性:

@AVClassName("Image")
public class Image extends AVObject{
    List likedUsers = new ArrayList();
    public void setLikedUsers(List usr) {
        if (null == usr) return;
        this.likedUsers = usr;
    }
    public List getLikedUsers() {
        return this.likedUsers;
    }
    public int getLikerCount() {
        return this.likedUsers.size();
    }
} 

数据读写

好了,接下来我们就来看看如何对于这些数据进行增删改查。首先,我们看看如何保存这样的数据。

  • Image 的保存
    AVFile remoteFile = AVFile.withFile(timeInSeconds, new File(processedImageUri.getPath()));
    remoteFile.saveInBackground();
    Image image = new Image();
    image.setPublisher(AVUser.getCurrentUser());
    image.setRawImage(remoteFile);
    image.setCaption(txtCaption.getText().toString().trim());
    image.saveInBackground();
  • 给一张图片增加一条评论
    final Comment comt = new Comment();
    comt.setContent(comment);
    comt.setCreator(AVUser.getCurrentUser());
    comt.saveInBackground(new SaveCallback(){
        public void done(com.avos.avoscloud.AVException arg0) {
            if (null != arg0) {
                Toast.makeText(ImageListActivity.this,
                        "Save Comment failed", Toast.LENGTH_SHORT).show();
            } else {
                image.addComment(comt);
                image.saveInBackground(new SaveCallback() {
                    public void done(com.avos.avoscloud.AVException arg0) {
                        if (null == arg0) {
                            Toast.makeText(ImageListActivity.this,
                                    "Comment successful", Toast.LENGTH_SHORT).show();
                            adapter.notifyDataSetChanged();
                        } else {
                            Toast.makeText(ImageListActivity.this,
                                    "Save Comment2Image failed", Toast.LENGTH_SHORT).show();
                        }
                    }
                 });
            }
        }
    });
  • 给一张图片点赞(或取消)
    public void like(Image image, String username) {
        image.addLiker(AVUser.getCurrentUser());
        adapter.notifyDataSetChanged();
    }

    public void unlike(Image image, String username) {
        image.removeLiker(AVUser.getCurrentUser());
        adapter.notifyDataSetChanged();
    }

其次,我们如何获取这些数据?要获取到图片的信息,很简单,调用 Query 接口就可以了,类似于:

    AVQuery query = new AVQuery("Image");
    query.orderByDescending("createAt");
    query.findInBackground(new FindCallback() {
        public void done(List avObjects, AVException e) {
            if (null == avObjects || null != e) {
                return;
            }
            adapter.notifyDataSetChanged();
        }
    }); 

但是这里有一个很麻烦的问题:返回的结果数据中,并没有publisher、comments、点赞者的信息,因为他们在 AVOS Cloud 的后台中都是 pointer 类型,系统并不会自动做级联查询并把结果填充完整,我们在显示图片流的时候,要能够如 Instagram 一般显示出所有信息,该怎么办?

一种办法是对结果中的每一个 item,再做一次查询,获取到第二级的对象实体信息;第二种办法是通过云代码来做数据填充,以返回完整的结果。但是这都比较麻烦。AVOS Cloud 为了支持这种需求,Query 接口是提供级联属性自动填充的选项的。我们在查询的时候,设置 include 属性,如下面所示:

    AVQuery query = new AVQuery("Image");
    query.orderByDescending("createAt");
    query.include("publisher");
    query.include("rawFile");
    query.include("comments");
    query.include("likes");
    query.findInBackground(new FindCallback() {
        public void done(List avObjects, AVException e) {
            if (null == avObjects || null != e) {
                return;
            }
            adapter.notifyDataSetChanged();
        }
    }); 

那么,我们得到的结果中就包含完整的对象了。

这种级联数据获取对单个 object 或者 object 数组都是有效的,但是这里还有一个问题:AVRelation 无法自动填充,并且关联的 Comment 中的 creator 属性也不能自动填充!

要是我们在展示图片的时候,要能够显示若干条评论和部分点赞的用户,那么目前为止,数据依然是不完备的,怎么办?可能你已经想到了,我们可以在 Query 的回调函数里面,去再次 fetch 我们需要的第三级数据:

// 先给Comment加一个fetchCreator方法
public class Comment extends AVObject{
    ...
    public void fetchCreator() {
        AVUser usr = getAVUser("creator");
        if (null == usr.getCreatedAt()) {
            try {
                usr.fetchInBackground(null);
            } catch (Exception ex) {
                Log.e("CMT", "failed to fetch user info. cause:" + ex.getMessage());
            }
        }
    }
    ...
}

// 在Query的回调函数中完成属性更新
    AVQuery query = new AVQuery("Image");
    query.orderByDescending("createAt");
    query.include("publisher");
    query.include("rawFile");
    query.include("comments");
    query.include("likes");
    query.findInBackground(new FindCallback() {
        public void done(List avObjects, AVException e) {
            if (null == avObjects || null != e) {
                return;
            }
            for (AVObject tmp : avObjects) {
                final Image img = (Image)tmp;
                AVRelation likers = img.getLiker();
                likers.getQuery().findInBackground(new FindCallback() {
                    public void done(List results, AVException e) {
                        if (e == null) {
                            // results have all the Posts the current user liked.
                            img.setLikedUsers(results);
                        }
                    }
                });
                List comments = img.getComments();
                if (null != comments) {
                    for (Comment cmt: comments) {
                        cmt.fetchCreator();
                    }
                }
                instagramImageList.add(img);
            }
            adapter.notifyDataSetChanged();
        }
    }); 

好了,到这里,我们终于获得了可以展示的完整结果数据了!

评论