产品需求
假设有一个类似于 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();
}
});
好了,到这里,我们终于获得了可以展示的完整结果数据了!
不错,很好
赞!不错,建议多分享一些具体场景的数据建模设计。
如果需求再多一些,比如 “管理我的所有评论”、“我收到的所有赞” 有什么比较好的方案么?
如果需要 “管理我的所有评论”,那么评论就需要和 Image 做成多对多的映射。要得到 “我收到的所有赞”,这个就比较麻烦了,按照上面的模型需要列出来我的所有 Image,然后查看里面的 Like Relation;还有一种客户端的做法就是给 AVUser 增加一个 “赞”(ImageId+PeerId,如果需要按人去重则只有 PeerId)的 AVRelation,这样每次点赞,除了在 Image 上增加一条 AVRelation 记录外,还需要在 AVUser 上增加一条 AVRelation 记录。
谢谢,确实如此。点赞的第一种方案如果我的所有 Image 比较多,再对每条 Image 执行 Query,太多异步请求回调客户端处理困难。第二种方案对其他用户表并没有修改权限,需要借助云代码。目前自己在用的是借鉴 “事件流系统” 的处理方法,将 Like 关系单独保存成一个表,同时保存关系双方用户、Image 的引用,这样在查询的时候非常自由。其实发现在多对多关系中,建立新表比 AVRleation 更加方便灵活。
赞一个!
到底是简单了还是复杂了 。。。
大家使用 MySQL 来做,其实复杂度也差不多。
final Image img = (Image)tmp; 这个转换是怎么做到的?不会有 runtime error?
不会的,因为是 Image 的 Query,我们会自动转成 Image 的 Object: query = new AVQuery (“Image”);
AVQuery
可以再扩展一下” 关注”” 粉丝”, 形成一个完整的社交模型.
感觉用对象比直接用数据库建立关系更复杂,
以前用 Parse 的时候也是自己摸索了半天, 能多提供一些这样的模型就太赞了.
我们有事件流系统,就是干你说的这个事情 https://cn.avoscloud.com/docs/status_system.html
赞
在 Mysql 里只要点一下就可以实现内联了,现在这样岂不是更复杂了么?我看了半天没看明白。
你好,我在使用 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 {
}
你这个都是同步的吧,异步要怎么搞呢?是不是嵌套很多?
这里面就有个严重的问题,不知道我理解的对不对,之前我也是按此文章的逻辑来实现的,后来下了 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 了,不知道这样我理解的对不对,期待回复。
你说的很对,这篇文章里面确实没有考虑 ACL,修改 relation 的时候涉及到 Image 类的 update 操作,所以如果我们不能允许所有人都可以修改 Image,那么别人就无法增加一个 comment。这里稍微纠正一下的是,LeanCloud 的 ACL 是区分了 update 和 delete 操作的,所以就算所有人都可以修改,也不是所有人都可以删除这个 Image 实例的,所以「我发了个图片别人就能删掉」这句话不太对。
Parse 使用一个全新的关联类,是另一种关系表达方式,确实更适合有严格 ACL 存在的场景,谢谢指正!
谢谢,这下了解了,刚又找到 Anypic 的 model 实现: https://parse.com/tutorials/anypic#model
:thumbup:
image 中的 AVFile 怎么取出来啊,getImages 取出来的是 List,没法转成 AVFile 啊,用 AVFile 里的 withAVObject 静态方法也没法转,这里要怎么做呢,请赐教啊
您好,我是第一次接触 LeanCloud 想自己做一个类似于朋友圈的社交的 但是对于这个一对多以及多对多的表弄不清楚,看您的博客也是一知半解,您能提供你的这份源码吗? 十分感谢。
能给个 demo 吗?
你这代码都是各种。。。看不懂
怎么看用户赞过哪些图片呢,这一点没说呢