基于 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();
        }
    });

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

26 thoughts on “基于 AVOS Cloud 的一对多、多对多数据建模

  1. 头像Yao

    如果需求再多一些,比如 “管理我的所有评论”、“我收到的所有赞” 有什么比较好的方案么?

    回复
    1. Junwen FengJunwen Feng 文章作者

      如果需要 “管理我的所有评论”,那么评论就需要和 Image 做成多对多的映射。要得到 “我收到的所有赞”,这个就比较麻烦了,按照上面的模型需要列出来我的所有 Image,然后查看里面的 Like Relation;还有一种客户端的做法就是给 AVUser 增加一个 “赞”(ImageId+PeerId,如果需要按人去重则只有 PeerId)的 AVRelation,这样每次点赞,除了在 Image 上增加一条 AVRelation 记录外,还需要在 AVUser 上增加一条 AVRelation 记录。

      回复
      1. 头像Yao

        谢谢,确实如此。点赞的第一种方案如果我的所有 Image 比较多,再对每条 Image 执行 Query,太多异步请求回调客户端处理困难。第二种方案对其他用户表并没有修改权限,需要借助云代码。目前自己在用的是借鉴 “事件流系统” 的处理方法,将 Like 关系单独保存成一个表,同时保存关系双方用户、Image 的引用,这样在查询的时候非常自由。其实发现在多对多关系中,建立新表比 AVRleation 更加方便灵活。

        回复
    1. Junwen FengJunwen Feng 文章作者

      不会的,因为是 Image 的 Query,我们会自动转成 Image 的 Object:
      AVQuery query = new AVQuery(“Image”);

      回复
  2. 头像iimgal

    可以再扩展一下” 关注”” 粉丝”, 形成一个完整的社交模型.
    感觉用对象比直接用数据库建立关系更复杂,
    以前用 Parse 的时候也是自己摸索了半天, 能多提供一些这样的模型就太赞了.

    回复
  3. 头像Donny

    在 Mysql 里只要点一下就可以实现内联了,现在这样岂不是更复杂了么?我看了半天没看明白。

    回复
  4. 头像Marine8888

    你好,我在使用 AVRalation 进行查询时报错:
    Caused by: java.lang.IllegalStateException: Target class does not exist
    E/AndroidRuntime(16160): at com.avos.avoscloud.AVRelation.getQuery(AVRelation.java:97)
    E/AndroidRuntime(16160): at com.avos.avoscloud.AVRelation.getQuery(AVRelation.java:81)
    E/AndroidRuntime(16160): at com.yy.db.DaoImpl.ProductDao.queryProducts(ProductDao.java:89)
    E/AndroidRuntime(16160): at com.yy.db.DaoImpl.ProductDao.findAll(ProductDao.java:74)

    代码:
    AVRelation imageRel = product.getImageRelation();
    List images = imageRel.getQuery(YYImage.class).find();

    YYImage.class 是 AVObject.class 的子类。并且已经注册过了 。
    如:AVObject.registerSubclass(YYImage.class);

    另:

    @AVClassName(YYImage.CLASS)
    public class YYImage extends AVObject implements FlowDataItem {

    public static final String CLASS = "YYImage";
    

    }

    回复
  5. 头像Jason

    这里面就有个严重的问题,不知道我理解的对不对,之前我也是按此文章的逻辑来实现的,后来下了 Parse 的 Anypic 应用,发现它里面的逻辑架构完全不是这样的,可能此文章没有考虑一个问题就是 ACL,如果对 image 这个类启用了 ACL 那么别人是无法在此类的基础上增删此类的 comment relation 的,如果不启用 ACL 那么任何人都能对类中的实例进行删改, 我发了个图片别人就能删掉。所以 Anypic 中又自定义了另一个类叫 Activity,别人评论了照片就新建个 Activity,from which user to which post with what content,这样一来各个权限就能很好的管理了,如果要加载某一照片的评论直接在 Activity 里面 query post = xxx,就 OK 了,不知道这样我理解的对不对,期待回复。

    回复
    1. jfengjfeng

      你说的很对,这篇文章里面确实没有考虑 ACL,修改 relation 的时候涉及到 Image 类的 update 操作,所以如果我们不能允许所有人都可以修改 Image,那么别人就无法增加一个 comment。这里稍微纠正一下的是,LeanCloud 的 ACL 是区分了 update 和 delete 操作的,所以就算所有人都可以修改,也不是所有人都可以删除这个 Image 实例的,所以「我发了个图片别人就能删掉」这句话不太对。
      Parse 使用一个全新的关联类,是另一种关系表达方式,确实更适合有严格 ACL 存在的场景,谢谢指正!

      回复
  6. 头像TRY_To_TRY

    image 中的 AVFile 怎么取出来啊,getImages 取出来的是 List,没法转成 AVFile 啊,用 AVFile 里的 withAVObject 静态方法也没法转,这里要怎么做呢,请赐教啊

    回复
  7. 头像jack

    您好,我是第一次接触 LeanCloud 想自己做一个类似于朋友圈的社交的  但是对于这个一对多以及多对多的表弄不清楚,看您的博客也是一知半解,您能提供你的这份源码吗? 十分感谢。

    回复
    1. Junwen FengJunwen Feng 文章作者

      是的,这只是平铺的结构,如果要支持对评论再评论,那么评论就需要做成一个树形结构,可以在 Comment 里面增加一个指向评论标的的 target Pointer(optional)来实现。

      回复

TRY_To_TRY进行回复 取消回复

电子邮件地址不会被公开。 必填项已用*标注