LeanCloud iOS 数据模型设计简介

LeanCloud 算是自己第一个接触的真实数据结构应用,本篇主要是用来总结看官方文档过程中习得的一些关于 LeanCloud 数据存储&获取的收获。本篇是基于个人阅读过程中的理解总结的文章,也可直接查看 LeanCloud 官方文档。如有不对的地方,欢迎拍砖~

LeanCloud 存储后台大量采用了 MongoDB 这种文档数据库来存储结构化数据,得以为我们提供了面向对象的、海量的、schema free 的存储能力。

Schema 是什么?

Schema 是对一个数据库的结构描述。在一个关系型数据库里面,Schema 定义了表、每个表的字段,还有表跟字段之间的关系。查看资料后个人的理解是,Schema 就像一个 namespace ,不同的 Schema 下的数据不会互相影响,除非有引用的权限。点到为止,后面再做深入了解。


数据库常识

先对比下关系型数据库、MangoDB 和 LeanCloud 的对应属于,如下表:

RDBMS MongoDB LeanCloud
Database Database Application
Table Collection Class
Row Document Object
Index Index Index
JOIN Embedded, Reference Embedded Object, Pointer, Relation

自己主要做的是 iOS 平台的开发,从上面的对比中可以很友好的发现,LeanCloud 支持了对象式的存储,看到的第一眼感觉还是比较亲切的~

关系型数据库( RDBMS )和文档型数据库的区别在于:

  • RDBMS 优化了数据存储效率
  • 文档数据库 优化了数据访问

传统的关系型数据模型,所有的数据都会被映射到二维的表结构行与列中;LeanCloud 提供了动态的对象模型(MongoDB 的文档模型),还可以很方便的内嵌子对象和数组。


表型数据结构与动态的文档型数据结构

在企业级的复杂数据结构中,使用 JSON 对象来建模比表更为轻便和高效。通过内嵌子对象和数组,JSON 对象可以和应用层的数据结构对齐。可以更容易得将应用层的数据映射到数据库里的对象。

对于表来说,将应用层的数据映射到关系数据库的表中,需要额外的对象关系映射层(ORM),这样就降低了 Schema 拓展和查询的灵活性,更为复杂。

下面以一个例子来说明 RSBMS 表结构与 LeanCloud 文档结构的差异:

(下图的数据显示的是每个车主拥有每辆车的信息)

PERSON

Per_ID Surname First_Name City
0 Zhou Yue London
1 Jony Fang London
2 Lo Tomasi Zurich

CAR

Car_ID Model Year Value Pers_ID
101 LAND ROVER 2022 300000 0
102 LAND ROVER 2022 500000 1
103 Porsche 2026 1318000 1
103 BYD 2013 100000 0

由上表可以看出,RDBMS 通过 Pers_ID 域来链接 PERSON 表和 CAR 表。
文档模型,通过内嵌子对象和数组将相关的数据提前合并到一个单一的数据结构中,传统形势的跨表的行与列都被存储到了一个文档内,更为方便。
再使用 LeanCloud 对上面表结构数据进行数据建模。需要创建这样的 Schema:一个单一的 Person 对象,对象内通过一子对象数组来保存该用户所拥有的每一个 Car,如下:

{
first_name: "Jony",
surname: "Fang",
city: "London",
cars: [
       {model: "LAND ROVER", year: 2022, value: 500000},
       {model: "Persche", year: 2026, value: 1318000}
       ]
}

文档数据库里的一篇文档,相当于 LeanCloud 平台的一个对象。

再以一个博客平台的例子来展示关系模型和文档模型的区别。依赖 RDBMS 的应用需要 join 5 张不同的表来获得一篇博客的完整数据;而在 LeanCloud 中,所有的博客数据都包含在一个文档中,博客作者和评论者的用户信息通过一个到 User 的引用(指针)进行关联。


文档模型的其它优点

除了数据表现更加自然外,文档模型还由性能和拓展性的优势:

  • 通过单一调用即可获得完整的文档数据,避免了多表 join 的开销。LeanCloud 的 Object 物理上作为一个单一的块进行存储,只需要一次内存或者磁盘的读操作即可。RDBMS 则相反,一个 join 操作需要从不同的地方多次读取操作才可以完成。
  • 文档是自包含的,将数据库内容分布到多个节点(sharding)会更为简单,同时也更容易通过普通硬件的水平扩展获得更高性能。

嗯~上面算是一个纪录,后面再深入细致了解文档模型的存储及性能。


设计文档 Schema

应用的数据访问模式决定了 Schema 设计,因此我们需要明确几点:

  • 数据库读写操作的比例以及是否需要重点优化某一方的性能
  • 对数据库进行查询和更新的操作类型
  • 数据生命周期和文档的增长率

对于普通的 [key--value] 对来说,和 RDBMS 的表结构差别不大。对于 1:1 或 1:many 的关系可以使用内嵌对象的方式。

哪些类型使用内嵌方式会更好呢?

  • 数据间有“所有”和“包含”的关系
  • 经常需要同时、原子改动的属性作为一个对象嵌入到一个单独属性(key)中

举一个例子,建立一个数据结构,纪录每个学生的家庭住址信息(可以把地址信息作为一个整体嵌入到 Student 类中)。

    AVObject *studentTom = [[AVObject alloc] initWithClassName:@"Student"];
    [studentTom setObject:@"Tom" forKey:@"name"];

    NSDictionary *addr = @{
                            @"city" : "ShangHai",
                            @"address" : "BaoShan", 
                            @"postcode" : "10000"
                        };
    [studentTom setObject:addr forKey:@"address"];

    [studentTom saveInBackground];// 保存到 LeanCloud 云端

然而并不是所有的 1:1 关系都适合内嵌的方式,下面这些场景使用引用会更为合适:

  • 一个对象呗频繁地读取,但部分子对象却很少会被访问
  • 对象的一部分属性频繁被更新,数据大小持续增长,但部分子对象属性基本保持稳定不变
  • 对象大小超过了 LeanCloud 最大 16 MB 限制

接着再总结下在 LeanCloud 上如何通过“引用”机制来实现复杂的数据模型。


复杂关系模型的设计

数据对象之间存在 3 种类型的关系。一对一关系、一对多关系、多对多关系。LeanCloud 支持4种方式来构建对象之间的关系(通过 MongoDB 的文档引用来实现):

  • Pointers(一对一、一对多)
  • Arrays(一对多、多对多)
  • AVRelation(多对多)
  • 关联表(多对多)

一对多关系

创建一个一对多关系时,我们需要根据关系中包含的对象数量,来选择使用 Pointers 还是 Arrays 来实现。如果关系“多”方包含的对象数量很多(>100左右),必须使用 Pointers。如果对象数量很小(<100),使用 Arrays 会更方便,特别是获取父对象的同时获取所有相关的对象(“多”方)。

使用 Pointers 实现一对多关系

Pointers 存储

中国的“省份”与“城市”就是一对多关系,以“广东省”下的“深圳市”、“广州市”为例,使用 Pointers 来存储。

    AVObject *GuangZhou = [[AVObject alloc] initWithClassName:@"City"];
    [GuangZhou setObject:@"广州市" forKey:@"name"];

    AVObject *GuangDong = [[AVObject alloc] initWithClassName:@"Province"];
    [GuangDong setObject:@"广东省" forKey:@"name"];

    [GuangZhou setObject:GuangDong forKey:@"dependent"];// 为广州市设置 dependent 属性为广东省

    [GuangZhou saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        if (succeeded) {
          // 广州市被保存成功  
        }
    }];
    // 广东省无需被单独保存,因为在保存广州市的时候已经上传到云端。

保存关联对象的同时,被关联的对象也会随之被保存到云端。

接着,我们需要关联一个已经在云端的对象,例如将“东莞市”添加至“广东省”,方法如下:

    // 假设 GuangDong 的 objectId 为 56545c5b00b09f857a603632
    AVObject *GuangDong = [AVObject objectWithClassName:@"Province" objectId:@"56545c5b00b09f857a603632"];    
    AVObject *DongGuan = [[AVObject alloc] initWithClassName:@"City"];
    [DongGuan setObject:@"东莞市" forKey:@"name"];

    [DongGuan setObject:GuangDong forKey:@"dependent"];// 为东莞市设置 dependent 属性为广东省
    [DongGuan saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        if (succeeded) {
          // 东莞市被保存成功  
        }
    }];

执行上述代码上传成功后,在应用控制台可以看到 dependent 字段显示为 Pointer 数据类型,而它本质上存储的是一个指向 Province 这张表的某个 AVObject 的指针。

Pointer 查询

如果已知一个城市,想知道他的省份,如下操作:

    // 假设东莞市作为 City 对象存储的时候它的 objectId 是 568e743c00b09aa22162b11f
    AVObject *DongGuan = [AVObject objectWithClassName:@"City" objectId:@"568e743c00b09aa22162b11f"];
    NSArray *keys = @[@"dependent"]; // 指定要获取的 objects
    [DongGuan fetchInBackgroundWithKeys:keys block:^(AVObject *object, NSError *error) {
         // 获取广东省
         AVObject *province = [object objectForKey:@"dependent"];
    }];

如果查询结果中包含了 City,并想通过一次查询同时把对应的 Province 一并加载到本地,如下操作:

    AVQuery *query = [AVQuery queryWithClassName:@"City"];

    // 查询名字是广州市的城市
    [query whereKey:@"name" equalTo:@"广州市"];

    // 找出城市对应的省份
    [query includeKey:@"dependent"];

    [query findObjectsInBackgroundWithBlock:^(NSArray *cities, NSError *error) {
        // cities 的结果为 name 等于广州市的集合,当然我们知道现实中只存在一个广州市
        for (AVObject *city in cities) {
            // 获取对应的省份,并不需要网络访问
            AVObject *province = [city objectForKey:@"dependent"];
        }
    }];

如果已知一个省份,要找出它的所有下辖城市,操作如下:

    // 假设 GuangDong 的 objectId 为 56545c5b00b09f857a603632
    AVObject *GuangDong = [AVObject objectWithoutDataWithClassName:@"Province" objectId:@"56545c5b00b09f857a603632"];

    AVQuery *query = [AVQuery queryWithClassName:@"City"];

    [query whereKey:@"dependent" equalTo:GuangDong];

    [query findObjectsInBackgroundWithBlock:^(NSArray *cities, NSError *error) {
        for (AVObject *city in cities) {
             // cities 的结果为广东省下辖的所有城市
        }
    }];

如上几步操作,就已经使用 Pointers 实现了一个简单的一对多(省份-城市)关系数据的存储和获取。

使用 Arrays 实现一对多关系

Arrays 存储

当一对多关系中所包含的对象数量很少时,使用 Arrays 比较理想。Arrays 可以通过 includeKey 进行查询。传递对应的 key 可以在获取一方对象数据的同时获取到所有多方对象的数据。但如果关系中包含的对象数量巨大,相应的查询会相应缓慢。

前面说的城市与省份的对应关系也可以使用 Arrays 来实现。重新建立对象,为 Province 表添加一列 cityList 来保存城市数组,如下操作:

    AVObject *GuangDong = [[AVObject alloc] initWithClassName:@"Province"];
    [GuangDong setObject:@"广东省" forKey:@"name"];

    AVObject *GuangZhou = [[AVObject alloc] initWithClassName:@"City"];
    [GuangZhou setObject:@"广州市" forKey:@"name"];

    AVObject *ShenZhen = [[AVObject alloc] initWithClassName:@"City"];
    [ShenZhen setObject:@"深圳市" forKey:@"name"];

    // 把广州和深圳放置在一个数组里面,然后把这个数组设置为广东的 cityList 属性
    NSArray *cityList = [NSArray arrayWithObjects:GuangZhou, ShenZhen, nil];

    [AVObject saveAllInBackground:cityList block:^(BOOL succeeded, NSError *error) {
        [GuangDong addUniqueObjectsFromArray:@[GuangZhou, ShenZhen] forKey:@"cityList"];
        // 只要保存 GuangDong 即可,它关联的对象都会一并被保存在云端。
        [GuangDong saveInBackground];
    }];

Arrays 查询

获取上面存储的 City 对象,如下操作:

    // 假设 GuangDong 的 objectId 是 56a740071532bc0053f335e6
    AVObject *GuangDong = [AVObject objectWithoutDataWithClassName:@"Province" objectId:@"56a740071532bc0053f335e6"];
    [GuangDong fetchIfNeededWithKeys:[NSArray arrayWithObjects:@"cityList",nil]];
    [GuangDong fetchIfNeededInBackgroundWithBlock:^(AVObject *object, NSError *error) {
        NSArray *cityList = [GuangDong objectForKey:@"cityList"];
        for (AVObject *city in cityList) {
             // cityList 的结果为广东省下辖的所有城市
             // 下面可以打印出所有城市的 objectId
             NSLog(@"objectId: %@", city.objectId);
             // 下面可以打印出所有城市的 name
             NSLog(@"name: %@", [city objectForKey:@"name"]);
        }
    }];

如果要在查询某一个省份的时候,顺便把所有下辖的城市也获取到本地,可以在构建查询的时候使用 includeKey 操作,这样就可以通过一次查询同时获取 cityList 列中存放的 City 对象数组,如下操作:

    AVQuery *query = [AVQuery queryWithClassName:@"Province"];

    [query whereKey:@"name" equalTo:@"广东省"];
    [query includeKey:@"cityList"];

    [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
        // objects 是查询 Province 这张表的结果,因为我们是根据 name 查询的,表中 name  等于广东省的有且只有一个数据
        // 因此这个集合有且只有一个数据
        for (AVObject *province in objects) {
            NSArray *cityList = [province objectForKey:@"cityList"];
            for (AVObject *city in cityList) {
                // 打印出所有城市的 objectId
                NSLog(@"city objectId: %@", city.objectId);
                // 打印出所有城市的 name
                NSLog(@"city name: %@", [city objectForKey:@"name"]);
            }
        }
    }];

同样的,也可以通过已知的城市来查询它的上级省份,例如查找南京的所属省份:

    AVObject *NanJing = [AVObject objectWithoutDataWithClassName:@"City" objectId:@"56a74006d342d30054168a29"];

    AVQuery *query = [AVQuery queryWithClassName:@"Province"];
    [query whereKey:@"cityList" equalTo:NanJing];

    [query getFirstObjectInBackgroundWithBlock:^(AVObject *province, NSError *error) {
         NSLog(@"province name: %@", [province objectForKey:@"name"]);
    }];

多对多关系

以选课系统为例,为 Student 对象和 Course 对象建模。一个学生可以选多门课程,一个课程选的学生也可以有多个,这样就有了一个多对多的关系。可以使用 Arrays、Relation 或创建自己的关联表来实现这种关系。具体选择哪种方式取决于是否需要为这个关系附加属性。

如果不需附加属性,Relation 或 Arrays 最为简单;如果多对多关系中任何一方对象比较多(>100左右),Relation 或关联表会更好;如果需要为关系附加一些属性,可以创建一个独立的表(关联表)来存储两端的关系,附加的属性用于描述这个关系,不适用于描述关系中的任何一方。附加的属性可以是:

  • 关系的创建时间
  • 关系的创建者
  • 某人查看此关系的次数

使用 Relation 实现多对多关系

Relation 的存储

一个学生可以选多门课程,一个课程可以被多人选择。下面使用 Relation 来构建 Student 和 Course 之间的关系。

一个学生选多门课程

    AVObject *studentTom = [[AVObject alloc] initWithClassName:@"Student"];// 学生 Tom
    [studentTom setObject:@"Tom" forKey:@"name"];

    AVObject *courseLinearAlgebra = [[AVObject alloc] initWithClassName:@"Course"];// 线性代数
    [courseLinearAlgebra setObject:@"Linear Algebra" forKey:@"name"];

    AVObject *courseObjectOrientedProgramming = [[AVObject alloc] initWithClassName:@"Course"];// 面向对象程序设计
    [courseObjectOrientedProgramming setObject:@"Object-Oriented Programming" forKey:@"name"];

    AVObject *courseOperatingSystem = [[AVObject alloc] initWithClassName:@"Course"];// 操作系统
    [courseOperatingSystem setObject:@"Operating System" forKey:@"name"];

    [AVObject saveAllInBackground:@[courseLinearAlgebra,courseObjectOrientedProgramming,courseOperatingSystem] block:^(BOOL succeeded, NSError *error) {
        if (error) {
            // 出现错误
        } else {
            // 保存成功
            AVRelation *relation = [studentTom relationforKey:@"coursesChosen"];// 新建一个 AVRelation,用来保存所选的课程
            [relation addObject:courseLinearAlgebra];
            [relation addObject:courseObjectOrientedProgramming];
            [relation addObject:courseOperatingSystem];

            [studentTom saveInBackground];
        }
    }];

Relation 的查询

获取某课程的所有选课学生,如下操作:

    // 微积分课程
    AVObject *courseCalculus = [AVObject objectWithClassName:@"Course" objectId:@"562da3fdddb2084a8a576d49"];

    // 构建 Student 的查询
    AVQuery *query = [AVQuery queryWithClassName:@"Student"];

    // 查询条件
    [query whereKey:@"coursesChosen" equalTo:courseCalculus];

    // 执行查询
    [query findObjectsInBackgroundWithBlock:^(NSArray *students, NSError *error) {
        // students 就是所有选择了微积分的学生
        for (AVObject *student in students) {
            // 打印 student 的 objectId 以及 name
            NSLog(@"objectId: %@", student.objectId);
            NSLog(@"name: %@", [student objectForKey:@"name"]);
        }
    }];

获取某学生学习的所有课程,可以通过如下查询来获取这种方向关系的结果:

    // 假设 Tom 被保存到云端之后的 objectId 是 562da3fdddb2084a8a576d49
    AVObject *studentTom = [AVObject objectWithClassName:@"Student" objectId:@"562da3fdddb2084a8a576d49"];

    // 读取 AVRelation 对象
    AVRelation *relation = [studentTom relationforKey:@"coursesChosen"];

    // 获取关系查询
    AVQuery *query = [relation query];

    [query findObjectsInBackgroundWithBlock:^(NSArray *courses, NSError *error) {
        // courses 就是当前学生 Tom 所选择的所有课程
        for (AVObject *course in courses) {
            // 打印 course 的 objectId 以及 name
            NSLog(@"objectId: %@", course.objectId);
            NSLog(@"name: %@", [course objectForKey:@"name"]);
        }
    }];

使用关联表实现多对多关系

有时需要为关系添加更多的附加信息,例如上面的学生选课系统,要了解学生打算选修的这门课课时是多久,或学生通过哪种平台选课(手机、网站...)。AVRelation 无法满足这些附加信息的需求,AVRelation 不支持额外的自定义属性,可以通过关联表来构建数据。

如下构建一个独立的表 StudentCourseMap 来保存 Student 和 Course 关系:

字段 类型 说明
course Pointer Course 指针实例
student Pointer Student 指针实例
duration Array 所选课程的开始时间和结束时间,如["2016-09-01","2016-07-21"]
platform String 选课平台,如 iOS

下面是实现代码:

    AVObject *studentTom = [[AVObject alloc] initWithClassName:@"Student"];// 学生 Tom
    [studentTom setObject:@"Tom" forKey:@"name"];

    AVObject *courseLinearAlgebra = [[AVObject alloc] initWithClassName:@"Course"];// 线性代数
    [courseLinearAlgebra setObject:@"Linear Algebra" forKey:@"name"];

    AVObject *studentCourseMapTom= [[AVObject alloc] initWithClassName:@"StudentCourseMap"];// 选课表对象

    // 设置关联
    [studentCourseMapTom setObject:studentTom forKey:@"student"];
    [studentCourseMapTom setObject:courseLinearAlgebra forKey:@"course"];

    // 设置学习周期
    [studentCourseMapTom setObject: @[@"2016-02-19",@"2016-04-21"] forKey:@"duration"];
    // 获取操作平台
    [studentCourseMapTom setObject: @"iOS" forKey:@"platform"];

    // 保存选课表对象
    [studentCourseMapTom saveInBackground];

查询某课程所有的选修学生:

    // 微积分课程
    AVObject *courseCalculus = [AVObject objectWithClassName:@"Course" objectId:@"562da3fdddb2084a8a576d49"];

    // 构建 StudentCourseMap 的查询
    AVQuery *query = [AVQuery queryWithClassName:@"StudentCourseMap"];

    // 查询所有选择了线性代数的学生
    [query whereKey:@"course" equalTo:courseCalculus];

    // 执行查询
    [query findObjectsInBackgroundWithBlock:^(NSArray *studentCourseMaps, NSError *error) {
        // studentCourseMaps 是所有 course 等于线性代数的选课对象
        // 然后遍历过程中可以访问每一个选课对象的 student,course,duration,platform 等属性
        for (AVObject *studentCourseMap in studentCourseMaps) {
            AVObject *student =[studentCourseMap objectForKey:@"student"];
            AVObject *course =[studentCourseMap objectForKey:@"course"];
            NSArray *duration = [studentCourseMap objectForKey:@"duration"];
            NSLog(@"platform: %@", [studentCourseMap objectForKey:@"platform"]);
        }
    }];

查询某一个学生选修的所有课程,如下操作:

    AVObject *studentTom = [AVObject objectWithoutDataWithClassName:@"Student" objectId:@"562da3fc00b0bf37b117c250"];
    AVQuery *query = [AVQuery queryWithClassName:@"StudentCourseMap"];
    [query whereKey:@"student" equalTo:studentTom];

    [query findObjectsInBackgroundWithBlock:^(NSArray *courses, NSError *error) {
        for (AVObject *course in courses) {

        }
    }];

使用 Arrays 实现多对多关系

使用 Arrays 实现多对多关系,关系中一方有一个数组列来包含关系另一方的一些对象。以选课系统为例,使用 Arrays 来实现学生选课的操作:

    AVObject *studentTom = [[AVObject alloc] initWithClassName:@"Student"];// 学生 Tom
    [studentTom setObject:@"Tom" forKey:@"name"];

    AVObject *courseLinearAlgebra = [[AVObject alloc] initWithClassName:@"Course"];// 线性代数
    [courseLinearAlgebra setObject:@"Linear Algebra" forKey:@"name"];

    AVObject *courseObjectOrientedProgramming = [[AVObject alloc] initWithClassName:@"Course"];// 面对对象程序设计
    [courseObjectOrientedProgramming setObject:@"Object-Oriented Programming" forKey:@"name"];

    AVObject *courseOperatingSystem = [[AVObject alloc] initWithClassName:@"Course"];// 操作系统
    [courseOperatingSystem setObject:@"Operating System" forKey:@"name"];

    // 所选课程的数组
    NSArray *courses = @[courseLinearAlgebra,courseObjectOrientedProgramming,courseOperatingSystem];

    // 使用属性名字 coursesChosen 保存所选课程的数组
    [studentTom setObject:courses forKey:@"coursesChosen"];

    // 保存在云端
    [studentTom saveInBackground];

当查询某学生所有选的课程,使用 includeKey 操作来获取对应的数组值,操作如下:

    AVQuery *query = [AVQuery queryWithClassName:@"Student"];

    [query whereKey:@"name" equalTo:@"Tom"];

    // 以下这条语句是关键语句
    [query includeKey:@"coursesChosen"];

    [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
        // objects 是查询 Student 这张表的结果,因为我们是根据 name 查询的,我们假设表中 name  等于 Tom 的学生有且只有一个数据
        // 因此这个集合有且只有一个数据
        for (AVObject *tom in objects) {
            NSArray *coursesChosenArray = [tom objectForKey:@"coursesChosen"];
            for (AVObject *course in coursesChosenArray) {
                // coursesChosenArray 的结果为 Tom 选修的所有课程
                // 下面可以打印出所有课程的 objectId
                NSLog(@"objectId: %@", course.objectId);
                // 下面可以打印出所有课程的 name
                NSLog(@"name: %@", [course objectForKey:@"name"]);
            }
        }
    }];

当查询某课程所有选修的学生时,如下操作:

    // 假设线性代数的 objectId 是 562da3fd60b2c1e233c9b250
    AVObject *courseLinearAlgebra = [AVObject objectWithoutDataWithClassName:@"Course" objectId:@"562da3fd60b2c1e233c9b250"];

    // 构建针对 Student 这张表的查询
    AVQuery *query = [AVQuery queryWithClassName:@"Student"];
    [query whereKey:@"coursesChosen" equalTo:courseLinearAlgebra];

    [query findObjectsInBackgroundWithBlock:^(NSArray *students, NSError *error) {
        // students 即为所有选择了线性代数这门课的学生
        for (AVObject *student in students) {
            // 下面可以打印出所有学生的 objectId、name
            NSLog(@"objectId: %@", student.objectId);
            NSLog(@"name: %@", [student objectForKey:@"name"]);
        }
    }];

待验证:如果 Arrays 中包含多个课程,想知道一个课程选修的学生有多少,怎么查询,查询过程是怎样的?


一对一关系

当需要将一个对像拆分成两个对象时,一对一关系就会用到。下面的几个实例体现了这种需求:

  • 限制部分用户数据的权限:这个场景中,可以将对像拆分为两部分。一部分数据所有用户可见,另一部分数据仅自己可见;一部分数据所有用户可修改,另一部分数据仅自己可修改。
  • 避免大对象:原始对像大小如果超过 128 kb 上限值,可以通过创建另一个对象来存储额外的数据。
  • 文件对象:可以使用 AVObject 构建一个文件对象,并与 AVFile 建立一对一的关联,将文件属性存在 AVObject 中。通过这种方法既可以方便查询修改文件属性,也可以方便存取文件。

关联数据的删除

当表中有一 Pointer 或 Relation 指向的源数据被删除时,此源数据对应的 Pointer 或 Relation 不会被自动删除。基于业务场景如果有必要做数据清理的话,可以调用对象上的删除接口将 Pointer 或 Relation 关联的对象删除。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容