iOS Developer寒哥管理的技术专题iOS

LeanCloud iOS 数据模型设计简介

2016-11-10  本文已影响498人  JonyFang

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 )和文档型数据库的区别在于:

传统的关系型数据模型,所有的数据都会被映射到二维的表结构行与列中;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 的引用(指针)进行关联。


文档模型的其它优点

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

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


设计文档 Schema

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

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

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

举一个例子,建立一个数据结构,纪录每个学生的家庭住址信息(可以把地址信息作为一个整体嵌入到 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 上如何通过“引用”机制来实现复杂的数据模型。


复杂关系模型的设计

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

一对多关系

创建一个一对多关系时,我们需要根据关系中包含的对象数量,来选择使用 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 中包含多个课程,想知道一个课程选修的学生有多少,怎么查询,查询过程是怎样的?


一对一关系

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


关联数据的删除

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

上一篇下一篇

猜你喜欢

热点阅读