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

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

基于 AVOS Cloud 的一对多、多对多数据建模》上有24条评论

  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 想自己做一个类似于朋友圈的社交的  但是对于这个一对多以及多对多的表弄不清楚,看您的博客也是一知半解,您能提供你的这份源码吗? 十分感谢。

    回复

发表评论

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