[TOC]
多年以來,關系型數據庫已經成為了企業數據管理的基礎,很多工程師對于關系模型和 6 個范式都比較了解,但是如今來構建和運行一個應用,隨著數據來源的越發多樣和用戶量的不斷增長,關系數據庫的限制逐漸成為業務的瓶頸,因此越來越多的公司開始向其它 NoSQL 數據庫進行遷移。
眾所周知,LeanCloud 存儲后臺大量采用了 MongoDB 這種文檔數據庫來存儲結構化數據,正因如此我們才能提供面向對象的、海量的、schema free 的存儲能力。從傳統的關系型數據庫轉換到 LeanCloud(or MongoDB)存儲系統,最基礎的改變就是數據建模,也就是「schema 設計」。
## [Schema 設計](#Schema_設計)
在探索 schema 設計之前,我們先統一一下關系型數據庫、MongoDB 和 LeanCloud 上的對應術語,如下表所示:
| RDBMS | MongoDB | LeanCloud |
| --- | --- | --- |
| Database | Database | Application |
| Table | Collection | Class |
| Row | Document | Object |
| Index | Index | Index |
| JOIN | Embedded,Reference | Embedded Object, Pointer,?~~Relation~~ |
關系型數據庫和文檔型數據庫的根本區別在于:
* RDBMS 優化了數據存儲效率(它的前提是系統中存儲是一個非常昂貴的組件)。
* 文檔數據庫優化了數據訪問(它認為開發時間和發布速度現在是比存儲更可寶貴的東西)
在 LeanCloud 上進行 Schema 設計需要數據架構師、開發人員和 DBA 在觀念上做一些轉變:之前是傳統的關系型數據模型,所有數據都會被映射到二維的表結構——行和列;現在是豐富、動態的對象模型(也就是 MongoDB 的「文檔模型」),包括內嵌子對象和數組。
### [從死板的「表」結構到靈活、動態的「文檔」](#從死板的_表_結構到靈活_動態的_文檔_)
> 后文中我們有時候采用 LeanCloud 的核心概念Object(對象),有時候提到 MongoDB 中的名詞Document(文檔),他們是等同的。
>
>
>
> +
>
>
我們現在使用的大部分數據,都有比較復雜的結構,用 「JSON 對象」來建模是比「表」更高效的方式。通過內嵌子對象和數組,JSON 對象可以和應用層的數據結構完全對齊。這對開發者來說,會更容易將應用層的數據映射到數據庫里的對象。相反,將應用層的數據映射到關系數據庫的表,則會降低開發效率,而比較普遍的增加額外的對象關系映射(ORM)層的做法,也同時降低了 schema 擴展和查詢優化的靈活性,引入了新的復雜度。
例如,在 RDBMS 中有父子關系的兩張表,通常就會變成 LeanCloud 里面含有內嵌子對象的單文檔結構。以下圖的數據為例:
* PERSON
| Pers_ID | Surname | First_Name | City |
| --- | --- | --- | --- |
| 0 | 柳 | 紅 | London |
| 1 | 楊 | 真 | Beijing |
| 2 | 洛 | 托馬斯 | Zurich |
* CAR
| Car_ID | Model | Year | Value | Pers_ID |
| --- | --- | --- | --- | --- |
| 101 | 大眾邁騰 | 2015 | 180000 | 0 |
| 102 | 豐田漢蘭達 | 2016 | 240000 | 0 |
| 103 | 福特翼虎 | 2014 | 220000 | 1 |
| 104 | 現代索納塔 | 2013 | 150000 | 2 |
RDBMS 中通過 Pers_ID 域來連接 PERSON 表和 CAR 表,以此支持應用中顯示每輛車的擁有者信息。使用文檔模型,通過內嵌子對象和數組可以將相關數據提前合并到一個單一的數據結構中,傳統的跨表的行和列現在都被存儲到了一個文檔內,完全省略掉了 join 操作。
換成 LeanCloud 來對同樣的數據建模,則允許我們創建這樣的 schema:一個單一的 Person 對象,里面通過一個子對象數組來保存該用戶所擁有的每一部 Car,例如:
~~~
{
first_name:"紅",
surname: "柳",
city:"London",
location:[45.123,47.232],
cars:[
{model:"大眾邁騰",
year: 2015,
value:180000,...},
{model:"豐田漢蘭達",
year: 2016,
value:240000,...}
]
}
~~~
文檔數據庫里的一篇文檔,就相當于 LeanCloud 平臺里的一個對象。這個例子里的關系模型雖然只由兩張表組成(現實中大部分應用可能需要幾十、幾百甚至上千張表),但是它并不影響我們思考數據的方式。
為了更好地展示關系模型和文檔模型的區別,我們考慮下圖所示的博客平臺的例子。在這里,依賴 RDBMS 的應用需要 join 5 張不同的表來獲得一篇博客的完整數據,而在 LeanCloud 中,所有的博客數據都包含在一個文檔中,博客作者和評論者的用戶信息則通過一個到 User 的引用(指針)進行關聯。

#### [文檔模型的其它優點](#文檔模型的其它優點)
除了數據表現更加自然之外,文檔模型還有性能和擴展性方面的優勢:
* 通過單一調用即可獲得完整的文檔,避免了多表 join 的開銷。LeanCloud 的 Object 物理上作為一個單一的塊進行存儲,只需要一次內存或者磁盤的讀操作即可。RDBMS 與此相反,一個 join 操作需要從不同地方多次讀取操作才可完成。
* 文檔是自包含的,將數據庫內容分布到多個節點(也叫 sharding)會更簡單,同時也更容易通過普通硬件的水平擴展獲得更高性能。DBA 們不再需要擔心跨節點進行 join 操作可能帶來的性能惡化問題。
### [定義文檔 schema](#定義文檔_schema)
應用的數據訪問模式決定了 schema 設計,因此我們需要特別明確以下幾點:
* 數據庫讀寫操作的比例以及是否需要重點優化某一方的性能;
* 對數據庫進行查詢和更新的操作類型;
* 數據生命周期和文檔的增長率;
以此來設計更合適的 schema 結構。
對于普通的「屬性名-值」對來說,設計比較簡單,和 RDBMS 中平坦的表結構差別不大。對于 1:1 或 1:many 的關系會很自然地考慮使用內嵌對象:
* 數據「所有」和「包含」的關系,都可以通過內嵌對象來進行建模。
* 同時,在架構上也可以把那些經常需要同時、原子改動的屬性作為一個對象嵌入到一個單獨的屬性中。
例如,為了記錄每個學生的家庭住址,我們可以把住址信息作為一個整體嵌入 Student 類里面。
~~~
// 學生 Tom
var studentTom = new AV.Object('Student');
studentTom.set('name', 'Tom');
var addr = { "city": "北京", "address": "西城區西長安街 1 號", "postcode":"100017" };
studentTom.set('address', addr);
studentTom.save();
~~~
但并不是所有的 1:1 關系都適合內嵌的方式,對下面的情況后文介紹的「引用」(等同于 MongoDB 的?`reference`)方式會更加合適:
* 一個對象被頻繁地讀取,但是內嵌的子對象卻很少會被訪問。
* 對象的一部分屬性頻繁地被更新,數據大小持續增長,但是剩下的一部分屬性基本穩定不變。
* 對象大小超過了 LeanCloud 當前最大 16 MB 限制。
接下來我們重點討論一下在 LeanCloud 上如何通過「引用」機制來實現復雜的關系模型。
## [復雜關系模型的設計](#復雜關系模型的設計)
數據對象之間存在 3 種類型的關系。一對一關系將一個對象與另一個對象關聯,一對多關系是一個對象關聯多個對象,多對多關系則用來實現大量對象之間的復雜關系。我們支持 4 種方式來構建對象之間的關系(都是通過 MongoDB 的文檔引用來實現的):
1. Pointers(適合一對一、一對多關系)
2. Arrays(一對多、多對多)
3. ~~AVRelation(多對多)~~
4. 關聯表(多對多)
### [一對多關系](#一對多關系)
在創建一對多關系時,選擇用 Pointers 還是 Arrays 來實現,需要考慮關系中包含的對象數量。如果關系「多」方包含的對象數量非常大(大于 100 左右),那么就必須使用 Pointers。反之,如果對象數量很小(低于 100 或更少),那么 Arrays 可能會更方便,特別是在獲取父對象的同時得到所有相關的對象,即一對多關系中的「多」。
2
#### [使用 Pointers 實現一對多關系](#使用_Pointers_實現一對多關系)
##### Pointers 存儲
中國的「省份」與「城市」具有典型的一對多的關系。深圳和廣州(城市)都屬于廣東省(省份),而朝陽區和海淀區(行政區)只能屬于北京市(直轄市)。廣東省對應著多個一級行政城市,北京對應著多個行政區。下面我們使用 Pointers 來存儲這種一對多的關系。
注:為了表述方便,后文中提及城市都泛指一級行政市以及直轄市行政區,而省份也包含了北京、上海等直轄市。
~~~
// 新建一個 AV.Object
var GuangZhou = new AV.Object('City');// 廣州
GuangZhou.set('name', '廣州');
var GuangDong = new AV.Object('Province');// 廣東
GuangDong.set('name', '廣東');
GuangZhou.set('dependent', GuangDong);// 為廣州設置 dependent 屬性為廣東
GuangZhou.save().then(function (guangZhou) {
console.log(guangZhou.id);
});
// 廣東無需被單獨保存,因為在保存廣州的時候已經上傳到云端。
~~~
注意:保存關聯對象的同時,被關聯的對象也會隨之被保存到云端。
要關聯一個已經存在于云端的對象,例如將「東莞市」添加至「廣東省」,方法如下:
~~~
// 假設 GuangDong 的 objectId 為 56545c5b00b09f857a603632
var GuangDong = AV.Object.createWithoutData('Province', '56545c5b00b09f857a603632');
var DongGuan = new AV.Object('City');
DongGuan.set('name', '東莞');
DongGuan.set('dependent', GuangDong);
DongGuan.save();
~~~
執行上述代碼后,在應用控制臺可以看到?`dependent`?字段顯示為 Pointer 數據類型,而它本質上存儲的是一個指向?`Province`?這張表的某個 AVObject 的指針。
##### Pointers 查詢
假如已知一個城市,想知道它的上一級的省份:
~~~
// 假設東莞作為 City 對象存儲的時候它的 objectId 是 568e743c00b09aa22162b11f,這個 objectId 可以在控制臺查看
var DongGuan = AV.Object.createWithoutData('City', '568e743c00b09aa22162b11f');
DongGuan.fetch({ include: ['dependent'] }, null).then(function (city) {
var province = city.get('dependent');
console.log(province.get('name'));
});
~~~
假如查詢結果中包含了城市,并想通過一次查詢同時把對應的省份也一并加載到本地:
~~~
var query = new AV.Query('City');
query.equalTo('name', '廣州');
query.include(['dependent']);
query.find().then(function (result) {
if (result.length > 0) {
var GuangZhou = result[0];
var province = GuangZhou.get('dependent');
}
});
~~~
假如已知一個省份,要找出它的所有下轄城市:
~~~
// 假設 GuangDong 的 objectId 為 56545c5b00b09f857a603632
var GuangDong = AV.Object.createWithoutData('Province', '56545c5b00b09f857a603632');
var query = new AV.Query('City');
query.equalTo('dependent', GuangDong);
query.find().then(function (cities) {
cities.forEach(function (city, i, a) {
console.log(city.id);
});
});
~~~
大多數場景下,Pointers 是實現一對多關系的最好選擇。
#### [使用 Arrays 實現一對多關系](#使用_Arrays_實現一對多關系)
##### Arrays 存儲
當一對多關系中所包含的對象數量很少時,使用 Arrays 比較理想。Arrays 可以通過?`includeKey`?簡化查詢。傳遞對應的 key 可以在獲取「一」方對象數據的同時獲取到所有「多」方對象的數據。但是如果關系中包含的對象數量巨大,查詢將響應緩慢。
城市與省份對應關系也可以使用 Arrays 實現。我們重新建立對象,為?`Province`?表添加一列?`cityList`?來保存城市數組:
~~~
// 創建省份對象
var GuangDong = new AV.Object('Province');
GuangDong.set('name', '廣東');
var GuangZhou = new AV.Object('City');
GuangZhou.set('name', '廣州');
var ShenZhen = new AV.Object('City');
ShenZhen.set('name', '深圳');
var cityArray = [GuangZhou, ShenZhen];
GuangDong.addUnique('cityList', cityArray);// 如此做,是為了以后添加更多的城市,保證城市不會重復添加
GuangDong.save();
~~~
##### Arrays 查詢
獲取這些?`City`?對象:
~~~
// 假設 GuangDong 的 objectId 是 56a740071532bc0053f335e6
var GuangDong = AV.Object.createWithoutData('Province', '56a740071532bc0053f335e6');
GuangDong.fetch({ include: ['cityList'] }, null).then(function (result) {
// 讀取城市列表
var cityList = GuangDong.get('cityList');
});
~~~
如果要在查詢某一個省份的時候,順便把所有下轄的城市也獲取到本地,可以在構建查詢的時候使用?`includeKey`?操作,這樣就可以通過一次查詢同時獲取?`cityList`?列中存放的?`City`?對象集合:
~~~
var query = new AV.Query('Province');
query.equalTo('name', '廣東');
// 以下這條語句是關鍵語句
query.include(['cityList']);
query.find().then(function (result) {
// objects 是查詢 Province 這張表的結果,因為我們是根據 name 查詢的,表中 name 等于廣東的有且只有一個數據
// 因此這個集合有且只有一個數據
var provice = result[0];
// cityList 的結果為廣東省下轄的所有城市
var cityList = provice.get('cityList');
// 下面可以打印出所有城市的 name
cityList.map(function (city, index, a) {
console.log(city.get('name'));
});
});
~~~
我們同樣也可以根據已知的城市來查詢它所屬的上級省份,例如找出南京所屬的省份:
~~~
var NanJing = AV.Object.createWithoutData('City', testNanJingId);
var query = new AV.Query('Province');
query.equalTo('cityList', NanJing);
query.first().then(function (jiangsu) {
//jiangsu 就是查詢出來的省份,這里使用 first() 這個接口原因是我們默認情況下「南京」只可能屬于一個省份
console.log(jiangsu.id);
console.log(jiangsu.get('name'));
});
~~~
### [多對多關系](#多對多關系)
假設有選課應用,我們需要為?`Student`?對象和?`Course`?對象建模。一個學生可以選多門課程,一個課程也有多個學生,這是一個典型的多對多關系。我們必須使用 Arrays 或創建自己的關聯表來實現這種關系。決策的關鍵在于是否需要為這個關系附加一些屬性。
如果不需要附加屬性,使用 Arrays 最為簡單。通常情況下,使用 Arrays 可以使用更少的查詢并獲得更好的性能。如果多對多關系中任何一方對象數量可能達到或超過 100,使用 關聯表是更好的選擇。
反之,若需要為關系附加一些屬性,就創建一個獨立的表(關聯表)來存儲兩端的關系。記住,附加的屬性是描述這個關系的,不是描述關系中的任何一方。所附加的屬性可以是:
* 關系創建的時間
* 關系創建者
* 某人查看此關系的次數
#### [使用關聯表實現多對多關系(推薦)](#使用關聯表實現多對多關系_推薦_)
有時我們需要知道更多關系的附加信息,比如在一個學生選課系統中,我們要了解學生打算選修的這門課的課時有多長,或者學生選修是通過手機選修還是通過網站操作的,此時我們可以使用傳統的數據模型設計方法:關聯表。
為此,我們創建一個獨立的表?`StudentCourseMap`?來保存?`Student`?和?`Course`?的關系:
| 字段 | 類型 | 說明 |
| --- | --- | --- |
| `course` | Pointer | Course 指針實例 |
| `student` | Pointer | Student 指針實例 |
| `duration` | Array | 所選課程的開始和結束時間點,如?`["2016-02-19","2016-04-21"]`。 |
| `platform` | String | 操作時使用的設備,如?`iOS`。 |
如此,實現選修功能的代碼如下:
~~~
var studentTom = new AV.Object('Student');
studentTom.set('name', 'Tom');// 學生 Tom
var courseLinearAlgebra = new AV.Object('Course');
courseLinearAlgebra.set('name', 'Linear Algebra');// 線性代數
// 選課表對象
var studentCourseMapTom = new AV.Object('StudentCourseMap');
// 設置關聯
studentCourseMapTom.set('student', studentTom);
studentCourseMapTom.set('course', courseLinearAlgebra);
// 設置學習周期
studentCourseMapTom.set('duration', [new Date(2015, 2, 19), new Date(2015, 4, 21)]);
// 設置操作平臺
studentCourseMapTom.set('platform', 'web');
// 保存選課表對象
studentCourseMapTom.save();
~~~
查詢選修了某一課程的所有學生:
~~~
// 微積分課程
var courseLinearAlgebra = AV.Object.createWithoutData('Course', courseLinearAlgebraId);
// 構建 StudentCourseMap 的查詢
var query = new AV.Query('StudentCourseMap');
// 查詢所有選擇了線性代數的學生
query.equalTo('course', courseLinearAlgebra);
// 執行查詢
query.find().then(function (studentCourseMaps) {
// studentCourseMaps 是所有 course 等于線性代數的選課對象
// 然后遍歷過程中可以訪問每一個選課對象的 student,course,duration,platform 等屬性
studentCourseMaps.forEach(function (scm, i, a) {
var student = scm.get('student');
var duration = scm.get('duration');
var platform = scm.get('platform');
});
});
~~~
同樣我們也可以很簡單地查詢某一個學生選修的所有課程,只需將上述代碼變換查詢條件即可:
~~~
var studentTom = AV.Object.createWithoutData('Student', '579f0441128fe10054420d49');
var query = new AV.Query('StudentCourseMap');
query.equalTo('student', studentTom);
~~~
#### [使用 Arrays 實現多對多關系](#使用_Arrays_實現多對多關系)
使用 Arrays 實現多對多關系,跟實現一對多關系大致相同。關系中一方的所有對象擁有一個數組列來包含關系另一方的一些對象。
以選課系統為例,現在我們使用 Arrays 方式來實現學生選課的操作:
~~~
// 學生 Tom
var studentTom = new AV.Object('Student');
studentTom.set('name', 'Tom');
// 線性代數
var courseLinearAlgebra = new AV.Object('Course');
courseLinearAlgebra.set('name', 'Linear Algebra');
// 面對對象程序設計
var courseObjectOrientedProgramming = new AV.Object('Course');
courseObjectOrientedProgramming.set('name', 'Object-Oriented Programming');
// 操作系統
var courseOperatingSystem = new AV.Object('Course');
courseOperatingSystem.set('name', 'Operating System');
// 設置 coursesChosen 屬性為課程數組
studentTom.set('coursesChosen', [courseLinearAlgebra, courseObjectOrientedProgramming, courseOperatingSystem]);
// 保存在云端
studentTom.save();
~~~
當查詢某一個學生選修的所有課程時,需要使用?`includeKey`?操作來獲取對應的數組值:
~~~
var query = new AV.Query('Student');
query.equalTo('name', 'Tom');
// 以下這條語句是關鍵語句
query.include('coursesChosen');
query.find().then(function (students) {
// students 是查詢 Student 這張表的結果,因為我們是根據 name 查詢的,我們假設表中 name 等于 Tom 的學生有且只有一個數據
// 因此這個集合有且只有一個數據
students.forEach(function (student, i, a) {
var coursesChosenArray = student.get('coursesChosen');
coursesChosenArray.forEach(function (course, ii, aa) {
// 下面可以打印出所有課程的 objectId
console.log(course.id);
// 注意,盡管使用 query.include 方法,但是它拉取的關聯對象,僅僅包含 objectId,如果想獲取其他屬性,還需要調用 fetch 接口
course.fetch().then(function (fetched) {
console.log(course.get('name'));
});
});
});
});
~~~
查找選修了某一個課程的所有學生:
~~~
// 假設線性代數的 objectId 是 562da3fd60b2c1e233c9b250
var courseLinearAlgebra = AV.Object.createWithoutData('Course', '562da3fd60b2c1e233c9b250');
var query = new AV.Query('Student');
query.equalTo('coursesChosen', courseLinearAlgebra);
query.find().then(function (students) {
// students 就是所有選擇了線性代數的學生
students.forEach(function (student, i, a) {
console.log(student.id);
});
});
~~~
### [一對一關系](#一對一關系)
當你需要將一個對象拆分成兩個對象時,一對一關系是一種重要的需求。這種需求應該很少見,但是在下面的實例中體現了這樣的需求:
* 限制部分用戶數據的權限
在這個場景中,你可以將此對象拆分成兩部分,一部分包含所有用戶可見的數據,另一部分包含所有僅自己可見的數據(通過?[ACL 控制](https://leancloud.cn/docs/data_security.html#Class_級別的_ACL)?)。同樣你也可以實現一部分包含所有用戶可修改的數據,另一部分包含所有僅自己可修改的數據。
* 避免大對象
原始對象大小超過了對象的 128 KB 的上限值,此時你可以創建另一個對象來存儲額外的數據。當然通常的作法是更好地設計和優化數據模型來避免出現大對象,但如果確實無法避免,則可以考慮使用 AVFile 存儲大數據。
* 更靈活的文件對象
AVFile 可以方便地存取文件,但對對象進行查詢和修改等操作就不是很方便了。此時可以使用 AVObject 構造一個自己的文件對象并與 AVFile 建立一對一關聯,將文件屬性存于 AVObject 中,這樣既可以方便查詢修改文件屬性,也可以方便存取文件。
### [關聯數據的刪除](#關聯數據的刪除)
當表中有一個 Pointer 指向的源數據被刪除時,這個源數據對應的 Pointer?不會被自動刪除。所以建議用戶在刪除源數據時自行檢查是否有 Pointer 指向這條數據,基于業務場景有必要做數據清理的話,可以調用對應的對象上的刪除接口將 Pointer 關聯的對象刪除。
## [索引](#索引)
在任何一個數據庫系統中,索引都是優化性能的重要手段,同時它與 schema 設計也是密不可分的。LeanCloud 也支持索引,其索引與關系數據庫中基本相同。在索引的選擇上,應用查詢操作的模式和頻率起決定性作用,同時我們也要明白,索引不是沒有代價的,在加速查詢的同時,它也會降低寫入速度、消耗更多存儲(磁盤和內存)資源。是否建索引,如何建索引,建多少索引,我們需要綜合權衡后來下決定。
### [索引類型](#索引類型)
LeanCloud 的索引可以包含任意的屬性(包括數組),下面是一些索引選項:
* 復合索引——在多個屬性域上構建一個單獨的索引結構。例如,以一個存儲客戶數據的應用為例,我們可能需要根據姓、名和居住地來查詢客戶信息。通過在「姓」、「名」、「居住地」上建立復合索引,LeanCloud 可以快速給出滿足這三個條件的結果,此外,這一復合索引也能加速任何前置屬性的查詢。例如根據「姓」或者根據「姓」+「名」的查詢,都會使用到這個復合索引。注意,如果單按照「名」來查詢,則此復合索引不起作用。
* 唯一索引——通過給索引加上唯一性約束,LeanCloud 就會拒絕含有相同索引值的對象插入和更新。所有的索引默認都不是唯一索引,如果把復合索引指定為唯一索引,那么應用層必須保證索引列的組合值是唯一的。
* 數組索引——對數組屬性也能創建索引。
* 地理空間索引——MongoDB 提供了地理空間索引,來方便大家進行地理位置相關的查詢。LeanCloud 會自動為 GeoPoint 類型的屬性建立地理空間索引,但是要求一個 Object 內 GeoPoint 的屬性不能超過一個。
* 稀疏索引——這種索引只包含那些含有指定屬性的文檔,如果文檔不含有目標屬性,那么就不會進入索引。稀疏索引體積更小,查詢性能更高。LeanCloud 默認都會創建稀疏索引。
LeanCloud 的索引可以在任何域上建立,包括內嵌對象和數組類型,這使它帶來了比 RDBMS 更強大的功能。
### [通過索引優化性能](#通過索引優化性能)
LeanCloud 后臺會根據每天的訪問日志,自動歸納、學習頻繁使用的訪問模式,并自動創建合適的索引。不過如果你對索引優化比較有經驗,也可以在控制臺為每一個 Class 手動創建索引。
1
## [持續優化 Schema](#持續優化_Schema)
在 LeanCloud 的存儲系統里,Class 可以在沒有完整的結構定義(包含哪些屬性,數據類型如何,等)時就提前創建好,一個 Class 下的對象(Object)也無需包含所有屬性域,我們可以隨時往對象中增減新的屬性。
這種靈活、動態的 schema 機制,使 schema 的持續優化變得非常簡單。相比之下,關系數據庫的開發人員和 DBA 在開始一個新項目的時候,寫下第一行代碼之前,就需要制定好數據庫 schema,這至少需要幾天,有的需要數周甚至更長。而 LeanCloud 則允許開發者通過不斷迭代和敏捷過程,持續優化 schema。開發者可以開始寫代碼并將他們創建的對象持久化存儲起來,以后當需要增加新的功能,LeanCloud 可以繼續存儲新的對象而不需要對原來的 Class 做 ALTER TABLE 操作,這會給我們的開發帶來很大的便利。