iOS 應(yīng)用架構(gòu)談 本地持久化方案
前言
嗯,你們要的大招。跟著這篇文章一起也發(fā)布了CTPersistance和CTJSBridge這兩個(gè)庫(kù),希望大家在實(shí)際使用的時(shí)候如果遇到問(wèn)題,就給我提issue或者PR或者評(píng)論區(qū)。每一個(gè)issue和PR以及評(píng)論我都會(huì)回復(fù)的。
持久化方案不管是服務(wù)端還是客戶(hù)端,都是一個(gè)非常值得討論的話(huà)題。尤其是在服務(wù)端,持久化方案的優(yōu)劣往往都會(huì)在一定程度上影響到產(chǎn)品的性能。然而在客戶(hù)端,只有為數(shù)不多的業(yè)務(wù)需求會(huì)涉及持久化方案,而且在大多數(shù)情況下,持久化方案對(duì)性能的要求并不是特別苛刻。所以我在移動(dòng)端這邊做持久化方案設(shè)計(jì)的時(shí)候,考慮更多的是方案的可維護(hù)和可拓展,然后在此基礎(chǔ)上才是性能調(diào)優(yōu)。這篇文章中,性能調(diào)優(yōu)不會(huì)單獨(dú)開(kāi)一節(jié)來(lái)講,而會(huì)穿插在各個(gè)小節(jié)中,大家有心的話(huà)可以重點(diǎn)看一下。
持久化方案對(duì)整個(gè)App架構(gòu)的影響和網(wǎng)絡(luò)層方案對(duì)整個(gè)架構(gòu)的影響類(lèi)似,一般都是導(dǎo)致整個(gè)項(xiàng)目耦合度高的罪魁禍?zhǔn)住6乙彩且蝗缂韧娜odel化的實(shí)踐者,在持久層去Model化的過(guò)程中,我引入了Virtual Record的設(shè)計(jì),這個(gè)在文中也會(huì)詳細(xì)描述。
這篇文章主要講以下幾點(diǎn):
根據(jù)需求決定持久化方案
持久層與業(yè)務(wù)層之間的隔離
持久層與業(yè)務(wù)層的交互方式
數(shù)據(jù)遷移方案
數(shù)據(jù)同步方案
另外,針對(duì)數(shù)據(jù)庫(kù)存儲(chǔ)這一塊,我寫(xiě)了一個(gè)CTPersistance,這個(gè)庫(kù)目前能夠完成大部分的持久層需求,同時(shí)也是我的Virtual Record這種設(shè)計(jì)思路的一個(gè)樣例。這個(gè)庫(kù)可以直接被cocoapods引入,希望大家使用的時(shí)候,能夠多給我提issue。這里是CTPersistance Class Reference。
根據(jù)需求決定持久化方案
在有需要持久化需求的時(shí)候,我們有非常多的方案可供選擇:NSUserDefault、KeyChain、File,以及基于數(shù)據(jù)庫(kù)的無(wú)數(shù)子方案。因此,當(dāng)有需要持久化的需求的時(shí)候,我們首先考慮的是應(yīng)該采用什么手段去進(jìn)行持久化。
NSUserDefault
一般來(lái)說(shuō),小規(guī)模數(shù)據(jù),弱業(yè)務(wù)相關(guān)數(shù)據(jù),都可以放到NSUserDefault里面,內(nèi)容比較多的數(shù)據(jù),強(qiáng)業(yè)務(wù)相關(guān)的數(shù)據(jù)就不太適合NSUserDefault了。另外我想吐槽的是,天貓這個(gè)App其實(shí)是沒(méi)有一個(gè)經(jīng)過(guò)設(shè)計(jì)的數(shù)據(jù)持久層的。然后天貓里面的持久化方案就很混亂,我就見(jiàn)到過(guò)有些業(yè)務(wù)線(xiàn)會(huì)把大部分業(yè)務(wù)數(shù)據(jù)都塞到NSUserDefault里面去,當(dāng)時(shí)看代碼的時(shí)候我特么就直接跪了。。。問(wèn)起來(lái)為什么這么做?結(jié)果說(shuō)因?yàn)閷?xiě)起來(lái)方便~你妹。。。
keychain
Keychain是蘋(píng)果提供的帶有可逆加密的存儲(chǔ)機(jī)制,普遍用在各種存密碼的需求上。另外,由于App卸載只要系統(tǒng)不重裝,Keychain中的數(shù)據(jù)依舊能夠得到保留,以及可被iCloud同步的特性,大家都會(huì)在這里存儲(chǔ)用戶(hù)唯一標(biāo)識(shí)串。所以有需要加密、需要存iCloud的敏感小數(shù)據(jù),一般都會(huì)放在Keychain。
文件存儲(chǔ)
文件存儲(chǔ)包括了Plist、archive、Stream等方式,一般結(jié)構(gòu)化的數(shù)據(jù)或者需要方便查詢(xún)的數(shù)據(jù),都會(huì)以Plist的方式去持久化。Archive方式適合存儲(chǔ)平時(shí)不太經(jīng)常使用但很大量的數(shù)據(jù),或者讀取之后希望直接對(duì)象化的數(shù)據(jù),因?yàn)锳rchive會(huì)將對(duì)象及其對(duì)象關(guān)系序列化,以至于讀取數(shù)據(jù)的時(shí)候需要Decode很花時(shí)間,Decode的過(guò)程可以是解壓,也可以是對(duì)象化,這個(gè)可以根據(jù)具體中的實(shí)現(xiàn)來(lái)決定。Stream就是一般的文件存儲(chǔ)了,一般用來(lái)存存圖片啊啥的,適用于比較經(jīng)常使用,然而數(shù)據(jù)量又不算非常大的那種。
數(shù)據(jù)庫(kù)存儲(chǔ)
數(shù)據(jù)庫(kù)存儲(chǔ)的話(huà),花樣就比較多了。蘋(píng)果自帶了一個(gè)Core Data,當(dāng)然業(yè)界也有無(wú)數(shù)替代方案可選,不過(guò)真正用在iOS領(lǐng)域的除了Core Data外,就是FMDB比較多了。數(shù)據(jù)庫(kù)方案主要是為了便于增刪改查,當(dāng)數(shù)據(jù)有狀態(tài)和類(lèi)別的時(shí)候最好還是采用數(shù)據(jù)庫(kù)方案比較好,而且尤其是當(dāng)這些狀態(tài)和類(lèi)別都是強(qiáng)業(yè)務(wù)相關(guān)的時(shí)候,就更加要采用數(shù)據(jù)庫(kù)方案了。因?yàn)槟悴豢赡芡ㄟ^(guò)文件系統(tǒng)遍歷文件去甄別你需要獲取的屬于某個(gè)狀態(tài)或類(lèi)別的數(shù)據(jù),這么做成本就太大了。當(dāng)然,特別大量的數(shù)據(jù)也不適合直接存儲(chǔ)數(shù)據(jù)庫(kù),比如圖片或者文章這樣的數(shù)據(jù),一般來(lái)說(shuō),都是數(shù)據(jù)庫(kù)存一個(gè)文件名,然后這個(gè)文件名指向的是某個(gè)圖片或者文章的文件。如果真的要做全文索引這種需求,建議最好還是掛個(gè)API丟到服務(wù)端去做。
總的說(shuō)一下
NSUserDefault、Keychain、File這些持久化方案都非常簡(jiǎn)單基礎(chǔ),分清楚什么時(shí)候用什么就可以了,不要像天貓那樣亂寫(xiě)就好。而且在這之上并不會(huì)有更復(fù)雜的衍生需求,如果真的要針對(duì)它們寫(xiě)文章,無(wú)非就是寫(xiě)怎么儲(chǔ)存怎么讀取,這個(gè)大家隨便Google一下就有了,我就不浪費(fèi)筆墨了。由于大多數(shù)衍生復(fù)雜需求都是通過(guò)采用基于數(shù)據(jù)庫(kù)的持久化方案去滿(mǎn)足,所以這篇文章的重點(diǎn)就數(shù)據(jù)庫(kù)相關(guān)的架構(gòu)方案設(shè)計(jì)和實(shí)現(xiàn)。如果文章中有哪些問(wèn)題我沒(méi)有寫(xiě)到的,大家可以在評(píng)論區(qū)提問(wèn),我會(huì)一一解答或者直接把遺漏的內(nèi)容補(bǔ)充在文章中。
持久層實(shí)現(xiàn)時(shí)要注意的隔離
在設(shè)計(jì)持久層架構(gòu)的時(shí)候,我們要關(guān)注以下幾個(gè)方面的隔離:
持久層與業(yè)務(wù)層的隔離
數(shù)據(jù)庫(kù)讀寫(xiě)隔離
多線(xiàn)程控制導(dǎo)致的隔離
數(shù)據(jù)表達(dá)和數(shù)據(jù)操作的隔離
1. 持久層與業(yè)務(wù)層的隔離
關(guān)于Model
在具體講持久層下數(shù)據(jù)的處理之前,我覺(jué)得需要針對(duì)這個(gè)問(wèn)題做一個(gè)完整的分析。
在View層設(shè)計(jì)中我分別提到了胖Model和瘦Model的設(shè)計(jì)思路,而且告訴大家我更加傾向于胖Model的設(shè)計(jì)思路。在網(wǎng)絡(luò)層設(shè)計(jì)里面我使用了去Model化的思路設(shè)計(jì)了APIMananger與業(yè)務(wù)層的數(shù)據(jù)交互。這兩個(gè)看似矛盾的關(guān)于Model的設(shè)計(jì)思路在我接下來(lái)要提出的持久層方案中其實(shí)是并不矛盾,而且是相互配合的。在網(wǎng)絡(luò)層設(shè)計(jì)這篇文章中,我對(duì)去Model化只給出了思路和做法,相關(guān)的解釋并不多,是因?yàn)橐忉屵@個(gè)問(wèn)題涉及面會(huì)比較廣,寫(xiě)的時(shí)候并不認(rèn)為在那篇文章里做解釋是最好的時(shí)機(jī)。由于持久層在這里胖Model和去Model化都會(huì)涉及,所以我覺(jué)得在講持久層的時(shí)候解釋這個(gè)話(huà)題會(huì)比較好。
我在跟別的各種領(lǐng)域的架構(gòu)師交流的時(shí)候,發(fā)現(xiàn)大家都會(huì)或多或少地混用Model和Model Layer的概念,然后往往導(dǎo)致大家討論的問(wèn)題最后都不在一個(gè)點(diǎn)上,說(shuō)Model的時(shí)候他跟你說(shuō)Model Layer,那好吧,我就跟你說(shuō)Model Layer,結(jié)果他又在說(shuō)Model,于是問(wèn)題就討論不下去了。我覺(jué)得作為架構(gòu)師,如果不分清楚這兩個(gè)概念,肯定是會(huì)對(duì)你設(shè)計(jì)的架構(gòu)的質(zhì)量有很大影響的。
如果把Model說(shuō)成Data Model,然后跟Model Layer放在一起,這樣就能夠很容易區(qū)分概念了。
Data Model
Data Model這個(gè)術(shù)語(yǔ)針對(duì)的問(wèn)題領(lǐng)域是業(yè)務(wù)數(shù)據(jù)的建模,以及代碼中這一數(shù)據(jù)模型的表征方式。兩者相輔相承:因?yàn)闃I(yè)務(wù)數(shù)據(jù)的建模方案以及業(yè)務(wù)本身特點(diǎn),而最終決定了數(shù)據(jù)的表征方式。同樣操作一批數(shù)據(jù),你的數(shù)據(jù)建模方案基本都是細(xì)化業(yè)務(wù)問(wèn)題之后,抽象得出一個(gè)邏輯上的實(shí)體。在實(shí)現(xiàn)這個(gè)業(yè)務(wù)時(shí),你可以選擇不同的表征方式來(lái)表征這個(gè)邏輯上的實(shí)體,比如字節(jié)流(TCP包等),字符串流(JSON、XML等),對(duì)象流。對(duì)象流又分通用數(shù)據(jù)對(duì)象(NSDictionary等),業(yè)務(wù)數(shù)據(jù)對(duì)象(HomeCellModel等)。
前面已經(jīng)遍歷了所有的Data Model的形式。在習(xí)慣上,當(dāng)我們討論Model化時(shí),都是單指對(duì)象流中的業(yè)務(wù)數(shù)據(jù)對(duì)象這一種。然而去Model化就是指:更多地使用通用數(shù)據(jù)對(duì)象去表征數(shù)據(jù),業(yè)務(wù)數(shù)據(jù)對(duì)象不會(huì)在設(shè)計(jì)時(shí)被優(yōu)先考慮的一種設(shè)計(jì)傾向。這里的通用數(shù)據(jù)對(duì)象可以在某種程度上理解為范型。
Model Layer
Model Layer描述的問(wèn)題領(lǐng)域是如何對(duì)數(shù)據(jù)進(jìn)行增刪改查(CURD, Create Update Read Delete),和相關(guān)業(yè)務(wù)處理。一般來(lái)說(shuō)如果在Model Layer中采用瘦Model的設(shè)計(jì)思路的話(huà),就差不多到CURD為止了。胖Model還會(huì)關(guān)心如何為需要數(shù)據(jù)的上層提供除了增刪改查以外的服務(wù),并為他們提供相應(yīng)的解決方案。例如緩存、數(shù)據(jù)同步、弱業(yè)務(wù)處理等。
我的傾向
我更加傾向于去Model化的設(shè)計(jì),在網(wǎng)絡(luò)層我設(shè)計(jì)了reformer來(lái)實(shí)現(xiàn)去Model化。在持久層,我設(shè)計(jì)了Virtual Record來(lái)實(shí)現(xiàn)去Model化。
因?yàn)榫唧w的Model是一種很容易引入耦合的做法,在盡可能弱化Model概念的同時(shí),就能夠?yàn)橐霕I(yè)務(wù)和對(duì)接業(yè)務(wù)提供充分的空間。同時(shí),也能通過(guò)去Model的設(shè)計(jì)達(dá)到區(qū)分強(qiáng)弱業(yè)務(wù)的目的,這在將來(lái)的代碼遷移和維護(hù)中,是至關(guān)重要的。很多設(shè)計(jì)不好的架構(gòu),就在于架構(gòu)師并沒(méi)有認(rèn)識(shí)到區(qū)分強(qiáng)弱業(yè)務(wù)的重要性,所以就導(dǎo)致架構(gòu)腐化的速度很快,越來(lái)越難維護(hù)。
所以說(shuō)回來(lái),持久層與業(yè)務(wù)層之間的隔離,是通過(guò)強(qiáng)弱業(yè)務(wù)的隔離達(dá)到的。而Virtual Record正是因?yàn)檫@種去Model化的設(shè)計(jì),從而達(dá)到了強(qiáng)弱業(yè)務(wù)的隔離,進(jìn)而做到持久層與業(yè)務(wù)層之間既隔離同時(shí)又能交互的平衡。具體Virtual Record是什么樣的設(shè)計(jì),我在后面會(huì)給大家分析。
2. 數(shù)據(jù)庫(kù)讀寫(xiě)隔離
在網(wǎng)站的架構(gòu)中,對(duì)數(shù)據(jù)庫(kù)進(jìn)行讀寫(xiě)分離主要是為了提高響應(yīng)速度。在iOS應(yīng)用架構(gòu)中,對(duì)持久層進(jìn)行讀寫(xiě)隔離的設(shè)計(jì)主要是為了提高代碼的可維護(hù)性。這也是兩個(gè)領(lǐng)域要求架構(gòu)師在設(shè)計(jì)架構(gòu)時(shí)要求側(cè)重點(diǎn)不同的一個(gè)方面。
在這里我們所謂的讀寫(xiě)隔離并不是指將數(shù)據(jù)的讀操作和寫(xiě)操作做隔離。而是以某一條界限為準(zhǔn),在這個(gè)界限以外的所有數(shù)據(jù)模型,都是不可寫(xiě)不可修改,或者修改屬性的行為不影響數(shù)據(jù)庫(kù)中的數(shù)據(jù)。在這個(gè)界限以?xún)?nèi)的數(shù)據(jù)是可寫(xiě)可修改的。一般來(lái)說(shuō)我們?cè)谠O(shè)計(jì)時(shí)劃分的這個(gè)界限會(huì)和持久層與業(yè)務(wù)層之間的界限保持一致,也就是業(yè)務(wù)層從持久層拿到數(shù)據(jù)之后,都不可寫(xiě)不可修改,或業(yè)務(wù)層針對(duì)這一數(shù)據(jù)模型的寫(xiě)操作、修改操作都對(duì)數(shù)據(jù)庫(kù)文件中的內(nèi)容不產(chǎn)生作用。只有持久層中的操作才能夠?qū)?shù)據(jù)庫(kù)文件中的內(nèi)容產(chǎn)生作用。
在蘋(píng)果官方提供的持久層方案Core Data的架構(gòu)設(shè)計(jì)中,并沒(méi)有針對(duì)讀寫(xiě)作出隔離,數(shù)據(jù)的結(jié)果都是以NSManagedObject扔出。所以只要業(yè)務(wù)工程師稍微一不小心動(dòng)一下某個(gè)屬性,NSManagedObjectContext在save的時(shí)候就會(huì)把這個(gè)修改給存進(jìn)去了。另外,當(dāng)我們需要對(duì)所有的增刪改查操作做AOP的切片時(shí),Core Data技術(shù)棧的實(shí)現(xiàn)就會(huì)非常復(fù)雜。
整體上看,我覺(jué)得Core Data相對(duì)大部分需求而言是過(guò)度設(shè)計(jì)了。我當(dāng)時(shí)設(shè)計(jì)安居客聊天模塊的持久層時(shí)就采用了Core Data,然后為了讀寫(xiě)隔離,將所有扔出來(lái)的NSManagedObject都轉(zhuǎn)為了普通的對(duì)象。另外,由于聊天記錄的業(yè)務(wù)相當(dāng)復(fù)雜,使用Core Data之后為了完成需求不得不引入很多Hack的手段,這種做法在一定程度上降低了這個(gè)持久層的可維護(hù)性和提高了接手模塊的工程師的學(xué)習(xí)曲線(xiàn),這是不太好的。在天貓客戶(hù)端,我去的時(shí)候天貓這個(gè)App就已經(jīng)屬于基本毫無(wú)持久層可言了,比較混亂。只能依靠各個(gè)業(yè)務(wù)線(xiàn)各顯神通去解決數(shù)據(jù)持久化的需求,難以推動(dòng)統(tǒng)一的持久層方案,這對(duì)于項(xiàng)目維護(hù)尤其是跨業(yè)務(wù)項(xiàng)目合作來(lái)說(shuō),基本就和車(chē)禍現(xiàn)場(chǎng)沒(méi)啥區(qū)別。我現(xiàn)在已經(jīng)從天貓離職,讀者中若是有阿里人想升職想刷存在感拿3.75的,可以考慮給天貓搞個(gè)統(tǒng)一的持久層方案。
讀寫(xiě)隔離還能夠便于加入AOP切點(diǎn),因?yàn)獒槍?duì)數(shù)據(jù)庫(kù)的寫(xiě)操作被隔離到一個(gè)固定的地方,加AOP時(shí)就很容易在正確的地方放入切片。這個(gè)會(huì)在講到數(shù)據(jù)同步方案時(shí)看到應(yīng)用。
3. 多線(xiàn)程導(dǎo)致的隔離
Core Data
Core Data要求在多線(xiàn)程場(chǎng)景下,為異步操作再生成一個(gè)NSManagedObjectContext,然后設(shè)置它的ConcurrencyType為NSPrivateQueueConcurrencyType,最后把這個(gè)Context的parentContext設(shè)為Main線(xiàn)程下的Context。這相比于使用原始的SQLite去做多線(xiàn)程要輕松許多。只不過(guò)要注意的是,如果要傳遞NSManagedObject的時(shí)候,不能直接傳這個(gè)對(duì)象的指針,要傳NSManagedObjectID。這屬于多線(xiàn)程環(huán)境下對(duì)象傳遞的隔離,在進(jìn)行架構(gòu)設(shè)計(jì)的時(shí)候需要注意。
SQLite
純SQLite其實(shí)對(duì)于多線(xiàn)程倒是直接支持,SQLite庫(kù)提供了三種方式:Single Thread,Multi Thread,Serialized。
Single Thread模式不是線(xiàn)程安全的,不提供任何同步機(jī)制。Multi Thread模式要求database connection不能在多線(xiàn)程中共享,其他的在使用上就沒(méi)什么特殊限制了。Serialized模式顧名思義就是由一個(gè)串行隊(duì)列來(lái)執(zhí)行所有的操作,對(duì)于使用者來(lái)說(shuō)除了響應(yīng)速度會(huì)慢一些,基本上就沒(méi)什么限制了。大多數(shù)情況下SQLite的默認(rèn)模式是Serialized。
根據(jù)Core Data在多線(xiàn)程場(chǎng)景下的表現(xiàn),我覺(jué)得Core Data在使用SQLite作為數(shù)據(jù)載體時(shí),使用的應(yīng)該就是Multi Thread模式。SQLite在Multi Thread模式下使用的是讀寫(xiě)鎖,而且是針對(duì)整個(gè)數(shù)據(jù)庫(kù)加鎖,不是表鎖也不是行鎖,這一點(diǎn)需要提醒各位架構(gòu)師注意。如果對(duì)響應(yīng)速度要求很高的話(huà),建議開(kāi)一個(gè)輔助數(shù)據(jù)庫(kù),把一個(gè)大的寫(xiě)入任務(wù)先寫(xiě)入輔助數(shù)據(jù)庫(kù),然后拆成幾個(gè)小的寫(xiě)入任務(wù)見(jiàn)縫插針地隔一段時(shí)間往主數(shù)據(jù)庫(kù)中寫(xiě)入一次,寫(xiě)完之后再把輔助數(shù)據(jù)庫(kù)刪掉。
不過(guò)從實(shí)際經(jīng)驗(yàn)上看,本地App的持久化需求的讀寫(xiě)操作一般都不會(huì)大,只要注意好幾個(gè)點(diǎn)之后一般都不會(huì)影響用戶(hù)體驗(yàn)。因此相比于Multi Thread模式,Serialized模式我認(rèn)為是性?xún)r(jià)比比較高的一種選擇,代碼容易寫(xiě)容易維護(hù),性能損失不大。為了提高幾十毫秒的性能而犧牲代碼的維護(hù)性,我是覺(jué)得劃不來(lái)的。
Realm
關(guān)于Realm我還沒(méi)來(lái)得及仔細(xì)研究,所以說(shuō)不出什么來(lái)。
4. 數(shù)據(jù)表達(dá)和數(shù)據(jù)操作的隔離
這是最容易被忽視的一點(diǎn),數(shù)據(jù)表達(dá)和數(shù)據(jù)操作的隔離是否能夠做好,直接影響的是整個(gè)程序的可拓展性。
長(zhǎng)久以來(lái),我們都很習(xí)慣Active Record類(lèi)型的數(shù)據(jù)操作和表達(dá)方式,例如這樣:
- Record *record = [[Record alloc] init];
- record.data = @"data";
- [record save];
或者這種:
- Record *record = [[Record alloc] init];
- NSArray *result = [record fetchList];
簡(jiǎn)單說(shuō)就是,讓一個(gè)對(duì)象映射了一個(gè)數(shù)據(jù)庫(kù)里的表,然后針對(duì)這個(gè)對(duì)象做操作就等同于針對(duì)這個(gè)表以及這個(gè)對(duì)象所表達(dá)的數(shù)據(jù)做操作。這里有一個(gè)不好的地方就在于,這個(gè)Record既是數(shù)據(jù)庫(kù)中數(shù)據(jù)表的映射,又是這個(gè)表中某一條數(shù)據(jù)的映射。我見(jiàn)過(guò)很多框架(不僅限于iOS,包括Python, PHP等)都把這兩者混在一起去處理。如果按照這種不恰當(dāng)?shù)姆绞絹?lái)組織數(shù)據(jù)操作和數(shù)據(jù)表達(dá),在胖Model的實(shí)踐下會(huì)導(dǎo)致強(qiáng)弱業(yè)務(wù)難以區(qū)分從而造成非常大的困難。使用瘦Model這種實(shí)踐本身就是我認(rèn)為有缺點(diǎn)的,具體的我在開(kāi)篇中已經(jīng)講過(guò),這里就不細(xì)說(shuō)了。
強(qiáng)弱業(yè)務(wù)不能區(qū)分帶來(lái)的最大困難在于代碼復(fù)用和遷移,因?yàn)槌志脤又械膹?qiáng)業(yè)務(wù)對(duì)View層業(yè)務(wù)的高耦合是無(wú)法避免的,然而弱業(yè)務(wù)相對(duì)而言只對(duì)下層有耦合關(guān)系對(duì)上層并不存在耦合關(guān)系,當(dāng)我們做代碼遷移或者復(fù)用時(shí),往往希望復(fù)用的是弱業(yè)務(wù)而不是強(qiáng)業(yè)務(wù),若此時(shí)強(qiáng)弱業(yè)務(wù)分不開(kāi),代碼復(fù)用就無(wú)從談起,遷移時(shí)就倍加困難。
另外,數(shù)據(jù)操作和數(shù)據(jù)表達(dá)混在一起會(huì)導(dǎo)致的問(wèn)題在于:客觀(guān)情況下,數(shù)據(jù)在view層業(yè)務(wù)上的表達(dá)方式多種多樣,有可能是個(gè)View,也有可能是個(gè)別的什么對(duì)象。如果采用映射數(shù)據(jù)庫(kù)表的數(shù)據(jù)對(duì)象去映射數(shù)據(jù),那么這種多樣性就會(huì)被限制,實(shí)際編碼時(shí)每到使用數(shù)據(jù)的地方,就不得不多一層轉(zhuǎn)換。
我認(rèn)為之所以會(huì)產(chǎn)生這樣不好的做法原因在于,對(duì)象對(duì)數(shù)據(jù)表的映射和對(duì)象對(duì)數(shù)據(jù)表達(dá)的映射結(jié)果非常相似,尤其是在表達(dá)Column時(shí),他們幾乎就是一模一樣。在這里要做好針對(duì)數(shù)據(jù)表或是針對(duì)數(shù)據(jù)的映射要做的區(qū)分的關(guān)鍵要點(diǎn)是:這個(gè)映射對(duì)象的操作著手點(diǎn)相對(duì)數(shù)據(jù)表而言,是對(duì)內(nèi)還是對(duì)外操作。如果是對(duì)內(nèi)操作,那么這個(gè)操作范圍就僅限于當(dāng)前數(shù)據(jù)表,這些操作映射給數(shù)據(jù)表模型就比較合適。如果是對(duì)外操作,執(zhí)行這些操作時(shí)有可能涉及其他的數(shù)據(jù)表,那么這些操作就不應(yīng)該映射到數(shù)據(jù)表對(duì)象中。
因此實(shí)際操作中,我是以數(shù)據(jù)表為單位去針對(duì)操作進(jìn)行對(duì)象封裝,然后再針對(duì)數(shù)據(jù)記錄進(jìn)行對(duì)象封裝。數(shù)據(jù)表中的操作都是針對(duì)記錄的普通增刪改查操作,都是弱業(yè)務(wù)邏輯。數(shù)據(jù)記錄僅僅是數(shù)據(jù)的表達(dá)方式,這些操作最好交付給數(shù)據(jù)層分管強(qiáng)業(yè)務(wù)的對(duì)象去執(zhí)行。具體內(nèi)容我在下文還會(huì)繼續(xù)說(shuō)。
持久層與業(yè)務(wù)層的交互方式
說(shuō)到這里,就不得不說(shuō)CTPersistance和Virtual Record了。我會(huì)通過(guò)它來(lái)講解持久層與業(yè)務(wù)層之間的交互方式。
- -------------------------------------------
- | |
- | LogicA LogicB LogicC | -------------------------------> View Layer
- | \ / | |
- -------\-------/------------------|--------
- \ / |
- \ / Virtual | Virtual
- \ / Record | Record
- | |
- -----------|----------------------|--------
- | | | |
- Strong Logics | DataCenterA DataCenterB |
- | / \ | |
- -----------------|-------/-----\-------------------|-------| Data Logic Layer ---
- | / \ | | |
- Weak Logics | Table1 Table2 Table | |
- | \ / | | |
- --------\-----/-------------------|-------- |
- \ / | |--> Data Persistance Layer
- \ / Query Command | Query Command |
- | | |
- -----------|----------------------|-------- |
- | | | | |
- | | | | |
- | DatabaseA DatabaseB | Data Operation Layer ---
- | |
- | Database Pool |
- -------------------------------------------
我先解釋一下這個(gè)圖:持久層有專(zhuān)門(mén)負(fù)責(zé)對(duì)接View層模塊或業(yè)務(wù)的DataCenter,它們之間通過(guò)Record來(lái)進(jìn)行交互。DataCenter向上層提供業(yè)務(wù)友好的接口,這一般都是強(qiáng)業(yè)務(wù):比如根據(jù)用戶(hù)篩選條件返回符合要求的數(shù)據(jù)等。
然后DataCenter在這個(gè)接口里面調(diào)度各個(gè)Table,做一系列的業(yè)務(wù)邏輯,最終生成record對(duì)象,交付給View層業(yè)務(wù)。
DataCenter為了要完成View層交付的任務(wù),會(huì)涉及數(shù)據(jù)組裝和跨表的數(shù)據(jù)操作。數(shù)據(jù)組裝因?yàn)閂iew層要求的不同而不同,因此是強(qiáng)業(yè)務(wù)。跨表數(shù)據(jù)操作本質(zhì)上就是各單表數(shù)據(jù)操作的組合,DataCenter負(fù)責(zé)調(diào)度這些單表數(shù)據(jù)操作從而獲得想要的基礎(chǔ)數(shù)據(jù)用于組裝。那么,這時(shí)候單表的數(shù)據(jù)操作就屬于弱業(yè)務(wù),這些弱業(yè)務(wù)就由Table映射對(duì)象來(lái)完成。
Table對(duì)象通過(guò)QueryCommand來(lái)生成相應(yīng)的SQL語(yǔ)句,并交付給數(shù)據(jù)庫(kù)引擎去查詢(xún)獲得數(shù)據(jù),然后交付給DataCenter。
DataCenter 和 Virtual Record
提到Virtual Record之前必須先說(shuō)一下DataCenter。
DataCenter其實(shí)是一個(gè)業(yè)務(wù)對(duì)象,DataCenter是整個(gè)App中,持久層與業(yè)務(wù)層之間的膠水。它向業(yè)務(wù)層開(kāi)放業(yè)務(wù)友好的接口,然后通過(guò)調(diào)度各個(gè)持久層弱業(yè)務(wù)邏輯和數(shù)據(jù)記錄來(lái)完成強(qiáng)業(yè)務(wù)邏輯,并將生成的結(jié)果交付給業(yè)務(wù)層。由于DataCenter處在業(yè)務(wù)層和持久層之間,那么它執(zhí)行業(yè)務(wù)邏輯所需要的載體,就要既能夠被業(yè)務(wù)層理解,也能夠被持久層理解。
CTPersistanceTable就封裝了弱業(yè)務(wù)邏輯,由DataCenter調(diào)用,用于操作數(shù)據(jù)。而Virtual Record就是前面提到的一個(gè)既能夠被業(yè)務(wù)層理解,也能夠被持久層理解的數(shù)據(jù)載體。
Virtual Record事實(shí)上并不是一個(gè)對(duì)象,它只是一個(gè)protocol,這就是它Virtual的原因。一個(gè)對(duì)象只要實(shí)現(xiàn)了Virtual Record,它就可以直接被持久層當(dāng)作Record進(jìn)行操作,所以它也是一個(gè)Record。連起來(lái)就是Virtual Record了。所以,Virtual Record的實(shí)現(xiàn)者可以是任何對(duì)象,這個(gè)對(duì)象一般都是業(yè)務(wù)層對(duì)象。在業(yè)務(wù)層內(nèi),常見(jiàn)的數(shù)據(jù)表達(dá)方式一般都是View,所以一般來(lái)說(shuō)Virutal Record的實(shí)現(xiàn)者也都會(huì)是一個(gè)View對(duì)象。
我們回顧一下傳統(tǒng)的數(shù)據(jù)操作過(guò)程:一般都是先從數(shù)據(jù)庫(kù)中取出數(shù)據(jù),然后Model化成一個(gè)對(duì)象,然后再把這個(gè)模型丟到外面,讓Controller轉(zhuǎn)化成View,然后再執(zhí)行后面的操作。
Virtual Record也是一樣遵循類(lèi)似的步驟。唯一不同的是,整個(gè)過(guò)程中,它并不需要一個(gè)中間對(duì)象去做數(shù)據(jù)表達(dá),對(duì)于數(shù)據(jù)的不同表達(dá)方式,由各自Virtual Record的實(shí)現(xiàn)者自己完成,而不需要把這些代碼放到Controller,所以這就是一個(gè)去Model化的設(shè)計(jì)。如果未來(lái)針對(duì)這個(gè)數(shù)據(jù)轉(zhuǎn)化邏輯有復(fù)用的需求,直接復(fù)用Virtual Record就可以了,十分方便。
用好Virtual Record的關(guān)鍵在于DataCenter提供的接口對(duì)業(yè)務(wù)足夠友好,有充足的業(yè)務(wù)上下文環(huán)境。
所以DataCenter一般都是被Controller所持有,所以如果整個(gè)App就只有一個(gè)DataCenter,這其實(shí)并不是一個(gè)好事。我見(jiàn)過(guò)有很多App的持久層就是一個(gè)全局單例,所有持久化業(yè)務(wù)都走這個(gè)單例,這是一種很蛋疼的做法。DataCenter也是需要針對(duì)業(yè)務(wù)做高度分化的,每個(gè)大業(yè)務(wù)都要提供一個(gè)DataCenter,然后掛在相關(guān)Controller下交給Controller去調(diào)度。比如分化成SettingsDataCenter,ChatRoomDataCenter,ProfileDataCenter等,另外要要注意的是,幾個(gè)DataCenter之間最好不要有業(yè)務(wù)重疊。如果一個(gè)DataCenter的業(yè)務(wù)實(shí)在是大,那就再拆分成幾個(gè)小業(yè)務(wù)。如果單個(gè)小業(yè)務(wù)都很大了,那就拆成各個(gè)Category,具體的做法可以參考我的框架中CTPersistanceTable和CTPersistanceQueryCommand的實(shí)踐。
這么一來(lái),如果要遷移涉及持久層的強(qiáng)業(yè)務(wù),那就只需要遷移DataCenter即可。如果要遷移弱業(yè)務(wù),就只需要遷移CTPersistanceTable。
實(shí)際場(chǎng)景
假設(shè)業(yè)務(wù)層此時(shí)收集到了用戶(hù)的篩選條件:
- NSDictionary *filter = @{
- @"key1":@{
- @"minValue1":@(1),
- @"maxValue1":@(9),
- },
- @"key2":@{
- @"minValue2":@(1),
- @"maxValue2":@(9),
- },
- @"key3":@{
- @"minValue3":@(1),
- @"maxValue3":@(9),
- },
- };
然后ViewController調(diào)用DataCenter向業(yè)務(wù)層提供的接口,獲得數(shù)據(jù)直接展示:
- /* in view controller */
- NSArry *fetchedRecordList = [self.dataCenter fetchItemListWithFilter:filter]
- [self.dataList appendWithArray:fetchedRecordList];
- [self.tableView reloadData];
在View層要做的事情其實(shí)到這里就已經(jīng)結(jié)束了,此時(shí)我們回過(guò)頭再來(lái)看DataCenter如何實(shí)現(xiàn)這個(gè)業(yè)務(wù):
- /* in DataCenter */
- - (NSArray *)fetchItemListWithFilter:(NSDictionary *)filter
- {
- ...
- ...
- ...
- /*
- 解析filter獲得查詢(xún)所需要的數(shù)據(jù)
- whereCondition
- whereConditionParams
- 假設(shè)上面這兩個(gè)變量就是解析得到的變量
- */
- ...
- ...
- ...
- /* 告知Table對(duì)象查詢(xún)數(shù)據(jù)后需要轉(zhuǎn)化成的對(duì)象(可選,統(tǒng)一返回對(duì)象可以便于歸并來(lái)自不同表的數(shù)據(jù)) */
- self.itemATable.recordClass = [Item class];
- self.itemBTable.recordClass = [Item class];
- self.itemCTable.recordClass = [Item class];
- /* 通過(guò)Table對(duì)象獲取數(shù)據(jù),此時(shí)Table對(duì)象內(nèi)執(zhí)行的就是弱業(yè)務(wù)了 */
- NSArray *itemAList = [self.itemATable findAllWithWhereCondition:whereCondition conditionParams:whereConditionParams isDistinct:NO error:NULL];
- NSArray *itemBList = [self.itemBTable findAllWithWhereCondition:whereCondition conditionParams:whereConditionParams isDistinct:NO error:NULL];
- NSArray *itemCList = [self.itemCTable findAllWithWhereCondition:whereCondition conditionParams:whereConditionParams isDistinct:NO error:NULL];
- /* 組裝數(shù)據(jù) */
- NSMutableArray *resultList = [[NSMutableArray alloc] init];
- [resultList addObjectsFromArray:itemAList];
- [resultList addObjectsFromArray:itemBList];
- [resultList addObjectsFromArray:itemCList];
- return resultList;
- }
基本上差不多就是上面這樣的流程。
一般來(lái)說(shuō),架構(gòu)師設(shè)計(jì)得差的持久層,都沒(méi)有通過(guò)設(shè)計(jì)DataCenter和Table,去將強(qiáng)業(yè)務(wù)和弱業(yè)務(wù)分開(kāi)。通過(guò)設(shè)計(jì)DataCenter和Table對(duì)象,主要是便于代碼遷移。如果遷移強(qiáng)業(yè)務(wù),把DataCenter和Table一起拿走就可以,如果只是遷移弱業(yè)務(wù),拿走Table就可以了。
另外,通過(guò)代碼我希望向你強(qiáng)調(diào)一下這個(gè)概念:將Table和Record區(qū)分開(kāi),這個(gè)在我之前畫(huà)的架構(gòu)圖上已經(jīng)有所表現(xiàn),不過(guò)上文并沒(méi)有著重強(qiáng)調(diào)。其實(shí)很多別的架構(gòu)師在設(shè)計(jì)持久層框架的時(shí)候,也沒(méi)有將Table和Record區(qū)分開(kāi),對(duì)的,這里我說(shuō)的框架包括Core Data和FMDB,這個(gè)也不僅限于iOS領(lǐng)域,CodeIgniter、ThinkPHP、Yii、Flask這些也都沒(méi)有對(duì)這個(gè)做區(qū)分。(這里吐槽一下,話(huà)說(shuō)上文我還提到Core Data被過(guò)度設(shè)計(jì)了,事實(shí)上該設(shè)計(jì)的地方?jīng)]設(shè)計(jì)到,不該設(shè)計(jì)的地方各種設(shè)計(jì)往上堆...)
以上就是對(duì)Virtual Record這個(gè)設(shè)計(jì)的簡(jiǎn)單介紹,接下來(lái)我們就開(kāi)始討論不同場(chǎng)景下如何進(jìn)行交互了。
其中我們最為熟悉的一個(gè)場(chǎng)景是這樣的:經(jīng)過(guò)各種邏輯組裝出一個(gè)數(shù)據(jù)對(duì)象,然后把這個(gè)數(shù)據(jù)對(duì)象交付給持久層去處理。這種場(chǎng)景我稱(chēng)之為一對(duì)一的交互場(chǎng)景,這個(gè)交互場(chǎng)景的實(shí)現(xiàn)非常傳統(tǒng),就跟大家想得那樣,而且CTPersistance的test case里面都是這樣的,所以這里我就不多說(shuō)了。所以,既然你已經(jīng)知道有了一對(duì)一,那么順理成章地就也會(huì)有多對(duì)一,以及一對(duì)多的交互場(chǎng)景。
下面我會(huì)一一描述Virtual Record是如何發(fā)揮虛擬的優(yōu)勢(shì)去針對(duì)不同場(chǎng)景進(jìn)行交互的。
多對(duì)一場(chǎng)景下,業(yè)務(wù)層如何與持久層交互?
多對(duì)一場(chǎng)景其實(shí)有兩種理解,一種是一個(gè)記錄的數(shù)據(jù)由多個(gè)View的數(shù)據(jù)組成。例如一張用戶(hù)表包含用戶(hù)的所有資料。然后有的View只包含用戶(hù)昵稱(chēng)用戶(hù)頭像,有的對(duì)象只包含用戶(hù)ID用戶(hù)Token。然而這些數(shù)據(jù)都只存在一張用戶(hù)表中,所以這是一種多個(gè)對(duì)象的數(shù)據(jù)組成一個(gè)完整Record數(shù)據(jù)的場(chǎng)景,這是多對(duì)一場(chǎng)景的理解之一。
第二種理解是這樣的,例如一個(gè)ViewA對(duì)象包含了一個(gè)Record的所有信息,然后另一個(gè)ViewB對(duì)象其實(shí)也包含了一個(gè)Record的所有信息,這就是一種多個(gè)不同對(duì)象表達(dá)了一個(gè)Record數(shù)據(jù)的場(chǎng)景,這也是一種多對(duì)一場(chǎng)景的理解。
同時(shí),這里所謂的交互還分兩個(gè)方向:存和取。
其實(shí)這兩種理解的解決方案都是一樣的,Virtual Record的實(shí)現(xiàn)者通過(guò)實(shí)現(xiàn)Merge操作來(lái)完成record數(shù)據(jù)的匯總,從而實(shí)現(xiàn)存操作。任意Virtual Record的實(shí)現(xiàn)者通過(guò)Merge操作,就可以將自己的數(shù)據(jù)交付給其它不同的對(duì)象進(jìn)行表達(dá),從而實(shí)現(xiàn)取操作。具體的實(shí)現(xiàn)在下面有具體闡釋。
多對(duì)一場(chǎng)景下,如何進(jìn)行存操作?
提供了- (NSObject*)mergeRecord:(NSObject*)record shouldOverride:(BOOL)shouldOverride;這個(gè)方法。望文生義一下,就是一個(gè)record可以與另外一個(gè)record進(jìn)行merge。在shouldOverride為NO的情況下,任何一邊的nil都會(huì)被另外一邊不是nil的記錄覆蓋,如果merge過(guò)程中兩個(gè)對(duì)象都不含有這些空數(shù)據(jù),則根據(jù)shouldOverride來(lái)決定是否要讓參數(shù)中record的數(shù)據(jù)覆蓋自己本身的數(shù)據(jù),若shouldOverride為YES,則即便是nil,也會(huì)把已有的值覆蓋掉。這個(gè)方法會(huì)返回被Merge的這個(gè)對(duì)象,便于鏈?zhǔn)秸{(diào)用。
舉一個(gè)代碼樣例:
- /*
- 這里的RecordViewA, RecordViewB, RecordViewC都是符合且實(shí)現(xiàn)了- (NSObject*)mergeRecord:(NSObject*)record shouldOverride:(BOOL)shouldOverride方法。
- */
- RecordViewA *a;
- RecordViewB *b;
- RecordViewC *c;
收集a, b, c的值的邏輯,我就不寫(xiě)了~
...
[[a mergeRecord:b shouldOverride:YES] mergeRecord:c shouldOverride:YES];
[self.dataCenter saveRecord:a];
基本思路就是通過(guò)merge不同的record對(duì)象來(lái)達(dá)到獲取完整數(shù)據(jù)的目的,由于是Virtual Record,具體的實(shí)現(xiàn)都是由各自的View去決定。View是最了解自己屬性的對(duì)象了,因此它是有充要條件來(lái)把自己與持久層相關(guān)的數(shù)據(jù)取出并Merge的,那么這段湊數(shù)據(jù)的代碼,就相應(yīng)分散到了各個(gè)View對(duì)象中,Controller里面就能夠做到非常干凈,整體可維護(hù)性也就提高了。
如果采用傳統(tǒng)方式,ViewController或者DataCenter中就會(huì)散落很多用于湊數(shù)據(jù)的代碼,寫(xiě)的時(shí)候就會(huì)出現(xiàn)一大段用于合并的代碼,非常難看,還不容易維護(hù)。
多對(duì)一場(chǎng)景下,如何進(jìn)行取操作?
其實(shí)這樣的表述并不恰當(dāng),因?yàn)闊o(wú)論Virtual Record的實(shí)現(xiàn)如何,對(duì)象是誰(shuí),只要從數(shù)據(jù)庫(kù)里面取出數(shù)據(jù)來(lái),數(shù)據(jù)就都是能夠保證完整的。這里更準(zhǔn)確的表述是,取出數(shù)據(jù)之后,如何交付給不同的對(duì)象。其實(shí)還是用到上面提到的mergeRecord方法來(lái)處理。
- /*
- 這里的RecordViewA, RecordViewB, RecordViewC都是符合且實(shí)現(xiàn)了- (NSObject*)mergeRecord:(NSObject*)record shouldOverride:(BOOL)shouldOverride方法。
- */
- RecordViewA *a;
- RecordViewB *b = [[RecordViewB alloc] init];
- RecordViewC *c = [[RecordViewC alloc] init];
- a = [self.table findLatestRecordWithError:NULL];
- [b mergeRecord:a];
- [c mergeRecord:a];
- return @[a, b, c]
這樣就能很容易把a(bǔ)記錄的數(shù)據(jù)交給b和c了,代碼觀(guān)感同樣非常棒,而且容易寫(xiě)容易維護(hù)。
一對(duì)多場(chǎng)景下,業(yè)務(wù)層如何與持久層交互?
一對(duì)多場(chǎng)景也有兩種理解,其一是一個(gè)對(duì)象包含了多個(gè)表的數(shù)據(jù),另外一個(gè)是一個(gè)對(duì)象用于展示多種表的數(shù)據(jù),這個(gè)代碼樣例其實(shí)文章前面已經(jīng)有過(guò),這一節(jié)會(huì)著重強(qiáng)調(diào)一下。乍看之下兩者并沒(méi)有什么區(qū)別,所以我需要指出的是,前者強(qiáng)調(diào)的是包含,也就是這個(gè)對(duì)象是個(gè)大熔爐,由多個(gè)表的數(shù)據(jù)組成。
還是舉用戶(hù)列表的例子:
假設(shè)數(shù)據(jù)庫(kù)中用戶(hù)相關(guān)的表有多張。大多數(shù)情況是因?yàn)閱伪鞢olumn太多,所以為了提高維護(hù)性和查詢(xún)性能而進(jìn)行的縱切。
多說(shuō)一句,縱切在實(shí)際操作時(shí),大多都是根據(jù)業(yè)務(wù)場(chǎng)景去切分成多個(gè)不同的表,分別來(lái)表達(dá)用戶(hù)各業(yè)務(wù)相關(guān)的部分?jǐn)?shù)據(jù),所以縱切的結(jié)果就是把Column特別多的一張表拆成Column不那么多的好幾個(gè)表。雖然數(shù)據(jù)庫(kù)經(jīng)過(guò)了縱切,但是有的場(chǎng)景還是要展示完整數(shù)據(jù)的,比如用戶(hù)詳情頁(yè)。因此,這個(gè)用戶(hù)詳情頁(yè)的View就有可能包含用戶(hù)基礎(chǔ)信息表(用戶(hù)名、用戶(hù)ID、用戶(hù)Token等)、以及用戶(hù)詳細(xì)信息表(用戶(hù)郵箱地址、用戶(hù)手機(jī)號(hào)等)。這就是一對(duì)多的一個(gè)對(duì)象包含了多個(gè)表的數(shù)據(jù)的意思。
后者強(qiáng)調(diào)的是展示。舉個(gè)例子,數(shù)據(jù)庫(kù)中有三個(gè)表分別是:
二手房、新房、租房,它們?nèi)叩臄?shù)據(jù)分別存儲(chǔ)在三個(gè)表里面,這其實(shí)是一種橫切。
橫切也是一種數(shù)據(jù)庫(kù)的優(yōu)化手段,橫切與縱切不同的地方在于,橫切是在保留了這套數(shù)據(jù)的完整性的前提下進(jìn)行的切分,橫切的結(jié)果就是把一個(gè)原本數(shù)據(jù)量很大的表,分成了好幾個(gè)數(shù)據(jù)量不那么大的表。也就是原來(lái)三種房子都能用同一個(gè)表來(lái)存儲(chǔ),但是這樣數(shù)據(jù)量就太大了,數(shù)據(jù)庫(kù)響應(yīng)速度就會(huì)下降。所以根據(jù)房子的類(lèi)型拆成這三張表。橫切也有根據(jù)ID切的,比如根據(jù)ID取余的結(jié)果來(lái)決定分在哪些表里,這種做法比較廣泛,因?yàn)橥卣蛊饋?lái)方便,到時(shí)候數(shù)據(jù)表又大了,大不了除數(shù)也跟著再換一個(gè)更大的數(shù)罷了。其實(shí)根據(jù)類(lèi)型去橫切也可以,只是拓展的時(shí)候就不那么方便。
剛才扯遠(yuǎn)了現(xiàn)在我再扯回來(lái),這三張表在展示的時(shí)候,只是根據(jù)類(lèi)型的不同,界面才有稍許不同而已,所以還是會(huì)用同一張View去展示這三種數(shù)據(jù),這就是一對(duì)多的一個(gè)對(duì)象用于展示多種表的數(shù)據(jù)的意思。
一個(gè)對(duì)象包含了多個(gè)表的數(shù)據(jù)時(shí),如何進(jìn)行存取操作?
在進(jìn)行取操作時(shí),其實(shí)跟前面多對(duì)一的取操作是一樣的,用Merge操作就可以了。
- RecordViewA *a;
- a = [self.CasaTable findLatestRecordWithError:NULL];
- [a mergeRecord:[self.TaloyumTable findLatestRecordWithError:NULL] shouldOverride:YES];
- [a mergeRecord:[self.CasatwyTable findLatestRecordWithError:NULL] shouldOverride:YES];
- return a;
在進(jìn)行存操作時(shí),Virtual Record的要求實(shí)現(xiàn)者實(shí)現(xiàn)- (NSDictionary *)dictionaryRepresentationWithColumnInfo:(NSDictionary *)columnInfo tableName:(NSString *)tableName;這個(gè)方法,實(shí)現(xiàn)者可以根據(jù)傳入的columnInfo和tableName返回相應(yīng)的數(shù)據(jù),這樣就能夠把這一次存數(shù)據(jù)時(shí)關(guān)心的內(nèi)容提供給持久層了。代碼樣例就是這樣的:
- RecordViewA *a = ...... ;
- /*
- 由于有- (NSDictionary *)dictionaryRepresentationWithColumnInfo:(NSDictionary *)columnInfo tableName:(NSString *)tableName;的實(shí)現(xiàn),a對(duì)象自己會(huì)提供給不同的Table它們感興趣的內(nèi)容而存儲(chǔ)。
- 所以直接存就好了。
- */
- [self.CasaTable insertRecord:a error:NULL];
- [self.TaloyumTable insertRecord:a error:NULL];
- [self.CasatwyTable insertRecord:a error:NULL]
通過(guò)上面的存取案例,你會(huì)發(fā)現(xiàn)使用Virtual Record之后,代碼量一下子少掉很多,原本那些亂七八糟用于拼湊條件的代碼全部被分散進(jìn)了各個(gè)虛擬記錄的實(shí)現(xiàn)中去了,代碼維護(hù)因此就變得相當(dāng)方便。若是采用傳統(tǒng)做法,再存取之前少不了要寫(xiě)一大段邏輯,如果涉及代碼遷移,這大段邏輯就也得要跟著遷移過(guò)去,這就很蛋疼了。
一個(gè)對(duì)象用于展示多種表的數(shù)據(jù),如何進(jìn)行存取操作?
在這種情況下的存操作其實(shí)跟上面一樣,直接存。Virtual Record的實(shí)現(xiàn)者自己會(huì)根據(jù)要存入的表的信息組裝好數(shù)據(jù)提供給持久層。樣例代碼與上一小節(jié)的存操作中給出的一模一樣,我就不復(fù)制粘貼了。
取操作就不太一樣了,不過(guò)由于取出時(shí)的對(duì)象是唯一的(因?yàn)橐粚?duì)多嘛),代碼也一樣十分簡(jiǎn)單:
- ViewRecord *a;
- ViewRecord *b;
- ViewRecord *c;
- self.itemATable.recordClass = [ViewRecord class];
- self.itemBTable.recordClass = [ViewRecord class];
- self.itemCTable.recordClass = [ViewRecord class];
- [a = self.itemATable findLatestRecordWithError:NULL];
- [b = self.itemBTable findLatestRecordWithError:NULL];
- [c = self.itemCTable findLatestRecordWithError:NULL];
這里的a,b,c都是同一個(gè)View,然后itemATable,itemBTable,itemCTable分別是不同種類(lèi)的表。這個(gè)例子表示了一個(gè)對(duì)象如何用于展示不同類(lèi)型的數(shù)據(jù)。如果使用傳統(tǒng)方法,這里少不了要寫(xiě)很多適配代碼,但是使用Virtual Record之后,這些代碼都由各自實(shí)現(xiàn)者消化掉了,在執(zhí)行數(shù)據(jù)邏輯時(shí)可以無(wú)需關(guān)心適配邏輯。
多對(duì)多場(chǎng)景?
其實(shí)多對(duì)多場(chǎng)景就是上述這些一對(duì)多和多對(duì)一場(chǎng)景的排列組合,實(shí)現(xiàn)方式都是一模一樣的,我這里就也不多啰嗦了。
交互方案的總結(jié)
在交互方案的設(shè)計(jì)中,架構(gòu)師應(yīng)當(dāng)區(qū)分好強(qiáng)弱業(yè)務(wù),把傳統(tǒng)的Data Model區(qū)分成Table和Record,并由DataCenter去實(shí)現(xiàn)強(qiáng)業(yè)務(wù),Table去實(shí)現(xiàn)弱業(yè)務(wù)。在這里由于DataCenter是強(qiáng)業(yè)務(wù)相關(guān),所以在實(shí)際編碼中,業(yè)務(wù)工程師負(fù)責(zé)創(chuàng)建DataCenter,并向業(yè)務(wù)層提供業(yè)務(wù)友好的方法,然后再在DataCenter中操作Table來(lái)完成業(yè)務(wù)層交付的需求。區(qū)分強(qiáng)弱業(yè)務(wù),將Table和Record拆分開(kāi)的好處在于:
通過(guò)業(yè)務(wù)細(xì)分降低耦合度,使得代碼遷移和維護(hù)非常方便
通過(guò)拆解數(shù)據(jù)處理邏輯和數(shù)據(jù)表達(dá)形態(tài),使得代碼具有非常良好的可拓展性
做到讀寫(xiě)隔離,避免業(yè)務(wù)層的誤操作引入Bug
為Virtual Record這一設(shè)計(jì)思路的實(shí)踐提供基礎(chǔ),進(jìn)而實(shí)現(xiàn)更靈活,對(duì)業(yè)務(wù)更加友好的架構(gòu)
任何不區(qū)分強(qiáng)弱業(yè)務(wù)的架構(gòu)都是架構(gòu)師在耍流氓,嗯。
在具體與業(yè)務(wù)層交互時(shí),采用Virtual Record的設(shè)計(jì)思路來(lái)設(shè)計(jì)Record,由具體的業(yè)務(wù)對(duì)象來(lái)實(shí)現(xiàn)Virtual Record,并以它作為DataCenter和業(yè)務(wù)層之間的數(shù)據(jù)媒介進(jìn)行交互。而不是使用傳統(tǒng)的數(shù)據(jù)模型來(lái)與業(yè)務(wù)層做交互。
使用Virtual Record的好處在于:
將數(shù)據(jù)適配和數(shù)據(jù)轉(zhuǎn)化邏輯封裝到具體的Record實(shí)現(xiàn)中,可以使得代碼更加抽象簡(jiǎn)潔,代碼污染更少
數(shù)據(jù)遷移時(shí)只需要遷移Virtual Record相關(guān)方法即可,非常容易拆分
業(yè)務(wù)工程師實(shí)現(xiàn)業(yè)務(wù)邏輯時(shí),可以在不損失可維護(hù)性的前提下,極大提高業(yè)務(wù)實(shí)現(xiàn)的靈活性
這一部分還順便提了一下橫切和縱切的概念。本來(lái)是打算有一小節(jié)專(zhuān)門(mén)寫(xiě)數(shù)據(jù)庫(kù)性能優(yōu)化的,不過(guò)事實(shí)上移動(dòng)App場(chǎng)景下數(shù)據(jù)庫(kù)的性能優(yōu)化手段不像服務(wù)端那樣豐富多彩,很多牛逼技術(shù)和參數(shù)調(diào)優(yōu)手段想用也用不了。差不多就只剩下數(shù)據(jù)切片的手段比較有效了,所以性能優(yōu)化這塊感覺(jué)沒(méi)什么好寫(xiě)的。其實(shí)大家了解了切片的方式和場(chǎng)景,就足以根據(jù)自己的業(yè)務(wù)場(chǎng)景去做優(yōu)化了。再使用一下Instrument的Time Profile再配合SQLite提供的一些函數(shù),就足以找到慢在哪兒,然后去做性能調(diào)優(yōu)了。但如果我把這些也寫(xiě)出來(lái),就變成教你怎么使用工具,感覺(jué)這個(gè)太low寫(xiě)著也不起勁,大家有興趣搜使用手冊(cè)下來(lái)看就行。
數(shù)據(jù)庫(kù)版本遷移方案
一般來(lái)說(shuō),具有持久層的App同時(shí)都會(huì)附帶著有版本遷移的需求。當(dāng)一個(gè)用戶(hù)安裝了舊版本的App,此時(shí)更新App之后,若數(shù)據(jù)庫(kù)的表結(jié)構(gòu)需要更新,或者數(shù)據(jù)本身需要批量地進(jìn)行更新,此時(shí)就需要有版本遷移機(jī)制來(lái)進(jìn)行這些操作。然而版本遷移機(jī)制又要兼顧跨版本的遷移需求,所以基本上大方案也就只有一種:建立數(shù)據(jù)庫(kù)版本節(jié)點(diǎn),遷移的時(shí)候一個(gè)一個(gè)跑過(guò)去。
數(shù)據(jù)遷移事實(shí)上實(shí)現(xiàn)起來(lái)還是比較簡(jiǎn)單的,做好以下幾點(diǎn)問(wèn)題就不大了:
根據(jù)應(yīng)用的版本記錄每一版數(shù)據(jù)庫(kù)的改變,并將這些改變封裝成對(duì)象
記錄好當(dāng)前數(shù)據(jù)庫(kù)的版本,便于跟遷移記錄做比對(duì)
在啟動(dòng)數(shù)據(jù)庫(kù)時(shí)執(zhí)行遷移操作,如果遷移失敗,提供一些降級(jí)方案
CTPersistance在數(shù)據(jù)遷移方面,凡是對(duì)于數(shù)據(jù)庫(kù)原本沒(méi)有的數(shù)據(jù)表,如果要新增,在使用table的時(shí)候就會(huì)自動(dòng)創(chuàng)建。因此對(duì)于業(yè)務(wù)工程師來(lái)說(shuō),根本不需要額外多做什么事情,直接用就可以了。把這部分工作放到這里,也是為數(shù)據(jù)庫(kù)版本遷移節(jié)省了一些步驟。
CTPersistance也提供了Migrator。業(yè)務(wù)工程師可以自己針對(duì)某一個(gè)數(shù)據(jù)庫(kù)編寫(xiě)一個(gè)Migrator。這個(gè)Migrator務(wù)必派生自CTPersistanceMigrator,且符合,只要提供一個(gè)migrationStep的字典,以及記錄版本順序的數(shù)組。然后把你自己派生的Migrator的類(lèi)名和對(duì)應(yīng)關(guān)心的數(shù)據(jù)庫(kù)名寫(xiě)在CTPersistanceConfiguration.plist里面就可以。CTPersistance會(huì)在初始數(shù)據(jù)庫(kù)的時(shí)候,根據(jù)plist里面的配置對(duì)應(yīng)找到Migrator,并執(zhí)行數(shù)據(jù)庫(kù)版本遷移的邏輯。
在版本遷移時(shí)要注意的一點(diǎn)是性能問(wèn)題。我們一般都不會(huì)在主線(xiàn)程做版本遷移的事情,這自然不必說(shuō)。需要強(qiáng)調(diào)的是,SQLite本身是一個(gè)容錯(cuò)性非常強(qiáng)的數(shù)據(jù)庫(kù)引擎,因此差不多在執(zhí)行每一個(gè)SQL的時(shí)候,內(nèi)部都是走的一個(gè)Transaction。當(dāng)某一版的SQL數(shù)量特別多的時(shí)候,建議在版本遷移的方法里面自己建立一個(gè)Transaction,然后把相關(guān)的SQL都包起來(lái),這樣SQLite執(zhí)行這些SQL的時(shí)候速度就會(huì)快一點(diǎn)。
其他的似乎并沒(méi)有什么要額外強(qiáng)調(diào)的了,如果有沒(méi)說(shuō)到的地方,大家可以在評(píng)論區(qū)提出來(lái)。
數(shù)據(jù)同步方案
數(shù)據(jù)同步方案大致分兩種類(lèi)型,一種類(lèi)型是單向數(shù)據(jù)同步,另一種類(lèi)型是雙向數(shù)據(jù)同步。下面我會(huì)分別說(shuō)說(shuō)這兩種類(lèi)型的數(shù)據(jù)同步方案的設(shè)計(jì)。
單向數(shù)據(jù)同步
單向數(shù)據(jù)同步就是只把本地較新數(shù)據(jù)的操作同步到服務(wù)器,不會(huì)從服務(wù)器主動(dòng)拉取同步操作。
比如即時(shí)通訊應(yīng)用,一個(gè)設(shè)備在發(fā)出消息之后,需要等待服務(wù)器的返回去知道這個(gè)消息是否發(fā)送成功,是否取消成功,是否刪除成功。然后數(shù)據(jù)庫(kù)中記錄的數(shù)據(jù)就會(huì)隨著這些操作是否成功而改變狀態(tài)。但是如果換一臺(tái)設(shè)備繼續(xù)執(zhí)行操作,在這個(gè)新設(shè)備上只會(huì)拉取舊的數(shù)據(jù),比如聊天記錄這種。但對(duì)于舊的數(shù)據(jù)并沒(méi)有刪除或修改的需求,因此新設(shè)備也不會(huì)問(wèn)服務(wù)器索取數(shù)據(jù)同步的操作,所以稱(chēng)之為單向數(shù)據(jù)同步。
單向數(shù)據(jù)同步一般來(lái)說(shuō)也不需要有job去做定時(shí)更新的事情。如果一個(gè)操作遲遲沒(méi)有收到服務(wù)器的確認(rèn),那么在應(yīng)用這邊就可以認(rèn)為這個(gè)操作失敗,然后一般都是在界面上把這些失敗的操作展示出來(lái),然后讓用戶(hù)去勾選需要重試的操作,然后再重新發(fā)起請(qǐng)求。微信在消息發(fā)送失敗的時(shí)候,就是消息前面有個(gè)紅色的圈圈,里面有個(gè)感嘆號(hào),只有用戶(hù)點(diǎn)擊這個(gè)感嘆號(hào)的時(shí)候才重新發(fā)送消息,背后不會(huì)有個(gè)job一直一直跑。
所以細(xì)化需求之后,我們發(fā)現(xiàn)單向數(shù)據(jù)同步只需要做到能夠同步數(shù)據(jù)的狀態(tài)即可。
如何完成單向數(shù)據(jù)同步的需求
添加identifier
添加identifier的目的主要是為了解決客戶(hù)端數(shù)據(jù)的主鍵和服務(wù)端數(shù)據(jù)的主鍵不一致的問(wèn)題。由于是單向數(shù)據(jù)同步,所以數(shù)據(jù)的生產(chǎn)者只會(huì)是當(dāng)前設(shè)備,那么identifier也理所應(yīng)當(dāng)由設(shè)備生成。當(dāng)設(shè)備發(fā)起同步請(qǐng)求的時(shí)候,把identifier帶上,當(dāng)服務(wù)器完成任務(wù)返回?cái)?shù)據(jù)時(shí),也把這些identifier帶上。然后客戶(hù)端再根據(jù)服務(wù)端給到的identifier再更新本地?cái)?shù)據(jù)的狀態(tài)。identifier一般都會(huì)采用UUID字符串。
添加isDirty
isDirty主要是針對(duì)數(shù)據(jù)的插入和修改進(jìn)行標(biāo)識(shí)。當(dāng)本地新生成數(shù)據(jù)或者更新數(shù)據(jù)之后,收到服務(wù)器的確認(rèn)返回之前,isDirty置為YES。當(dāng)服務(wù)器的確認(rèn)包返回之后,再根據(jù)包里提供的identifier找到這條數(shù)據(jù),然后置為NO。這樣就完成了數(shù)據(jù)的同步。
然而這只是簡(jiǎn)單的場(chǎng)景,有一種比較極端的情況在于,當(dāng)請(qǐng)求發(fā)起到收到請(qǐng)求回復(fù)的這短短幾秒間,用戶(hù)又修改了數(shù)據(jù)。如果按照當(dāng)前的邏輯,在收到請(qǐng)求回復(fù)之后,這個(gè)又修改了的數(shù)據(jù)的isDirty會(huì)被置為NO,于是這個(gè)新的修改就永遠(yuǎn)無(wú)法同步到服務(wù)器了。這種極端情況的簡(jiǎn)單處理方案就是在發(fā)起請(qǐng)求到收到回復(fù)期間,界面上不允許用戶(hù)進(jìn)行修改。
如果希望做得比較細(xì)致,在發(fā)送同步請(qǐng)求期間依舊允許用戶(hù)修改的話(huà),就需要在數(shù)據(jù)庫(kù)額外增加一張DirtyList來(lái)記錄這些操作,這個(gè)表里至少要有兩個(gè)字段:identifier,primaryKey。然后每一次操作都分配一次identifier,那么新的修改操作就有了新的identifier。在進(jìn)行同步時(shí),根據(jù)primaryKey找到原數(shù)據(jù)表里的那條記錄,然后把數(shù)據(jù)連同identifier交給服務(wù)器。然后在服務(wù)器的確認(rèn)包回來(lái)之后,就只要拿出identifier再把這條操作記錄刪掉即可。這個(gè)表也可以直接服務(wù)于多個(gè)表,只是還需要額外添加一個(gè)tablename字段,方便發(fā)起同步請(qǐng)求的時(shí)候能夠找得到數(shù)據(jù)。
添加isDeleted
當(dāng)有數(shù)據(jù)同步的需求的時(shí)候,刪除操作就不能是簡(jiǎn)單的物理刪除了,而只是邏輯刪除,所謂邏輯刪除就是在數(shù)據(jù)庫(kù)里把這條記錄的isDeleted記為YES,只有當(dāng)服務(wù)器的確認(rèn)包返回之后,才會(huì)真正把這條記錄刪除。isDeleted和isDirty的區(qū)別在于:收到確認(rèn)包后,返回的identifier指向的數(shù)據(jù)如果是isDeleted,那么就要?jiǎng)h除這條數(shù)據(jù),如果指向的數(shù)據(jù)只是新插入的數(shù)據(jù)和更新的數(shù)據(jù),那么就只要修改狀態(tài)就行。插入數(shù)據(jù)和更新數(shù)據(jù)在收到數(shù)據(jù)包之后做的操作是相同的,所以就用isDirty來(lái)區(qū)分就足夠了。總之,這是根據(jù)收到確認(rèn)包之后的操作不同而做的區(qū)分。兩者都要有,缺一不可。
在請(qǐng)求的數(shù)據(jù)包中,添加dependencyIdentifier
在我看到的很多其它數(shù)據(jù)同步方案中,并沒(méi)有提供dependencyIdentifier,這會(huì)導(dǎo)致一個(gè)這樣的問(wèn)題:假設(shè)有兩次數(shù)據(jù)同步請(qǐng)求一起發(fā)出,A先發(fā),B后發(fā)。結(jié)果反而是B請(qǐng)求先到,A請(qǐng)求后到。如果A請(qǐng)求的一系列同步操作里面包含了插入某個(gè)對(duì)象的操作,B請(qǐng)求的一系列同步操作里面正好又刪除了這個(gè)對(duì)象,那么由于到達(dá)次序的先后問(wèn)題錯(cuò)亂,就導(dǎo)致這個(gè)數(shù)據(jù)沒(méi)辦法刪除。
這個(gè)在移動(dòng)設(shè)備的使用場(chǎng)景下是很容易發(fā)生的,移動(dòng)設(shè)備本身網(wǎng)絡(luò)環(huán)境就多變,先發(fā)的包反而后到,這種情況出現(xiàn)的幾率還是比較大的。所以在請(qǐng)求的數(shù)據(jù)包中,我們要帶上上一次請(qǐng)求時(shí)一系列identifier的其中一個(gè),就可以了。一般都是選擇上次請(qǐng)求里面最后的那一個(gè)操作的identifier,這樣就能表征上一次請(qǐng)求的操作了。
服務(wù)端這邊也要記錄最近的100個(gè)請(qǐng)求包里面的最后一個(gè)identifier。之所以是100條純屬只是拍腦袋定的數(shù)字,我覺(jué)得100條差不多就夠了,客戶(hù)端發(fā)請(qǐng)求的時(shí)候denpendency應(yīng)該不會(huì)涉及到前面100個(gè)包。服務(wù)端在收到同步請(qǐng)求包的時(shí)候,先看denpendencyIdentifier是否已被記錄,如果已經(jīng)被記錄了,那么就執(zhí)行這個(gè)包里面的操作。如果沒(méi)有被記錄,那就先放著再等等,等到條件滿(mǎn)足了再執(zhí)行,這樣就能解決這樣的問(wèn)題。
之所以不用更新時(shí)間而是identifier來(lái)做標(biāo)識(shí),是因?yàn)槿绻脮r(shí)間做標(biāo)識(shí)的話(huà),就是只能以客戶(hù)端發(fā)出數(shù)據(jù)包時(shí)候的時(shí)間為準(zhǔn)。但有時(shí)不同設(shè)備的時(shí)間不一定完全對(duì)得上,多少會(huì)差個(gè)幾秒幾毫秒,另外如果同時(shí)有兩個(gè)設(shè)備發(fā)起同步請(qǐng)求,這兩個(gè)包的時(shí)間就都是一樣的了。假設(shè)A1, B1是1號(hào)設(shè)備發(fā)送的請(qǐng)求,A2, B2,是2號(hào)設(shè)備發(fā)送的請(qǐng)求,如果用時(shí)間去區(qū)分,A1到了之后,B2說(shuō)不定就直接能夠執(zhí)行了,而A1還沒(méi)到服務(wù)器呢。
當(dāng)然,這也是一種極端情況,用時(shí)間的話(huà),服務(wù)器就只要記錄一個(gè)時(shí)間了,凡是依賴(lài)時(shí)間大于這個(gè)時(shí)間的,就都要再等等,實(shí)現(xiàn)起來(lái)就比較方便。但是為了保證bug盡可能少,我認(rèn)為依賴(lài)還是以identifier為準(zhǔn),這要比以時(shí)間為準(zhǔn)更好,而且實(shí)現(xiàn)起來(lái)其實(shí)也并沒(méi)有增加太多復(fù)雜度。
單向數(shù)據(jù)同步方案總結(jié)
改造的時(shí)候添加identifier,isDirty,isDeleted字段。如果在請(qǐng)求期間依舊允許對(duì)數(shù)據(jù)做操作,那么就要把identifier和primaryKey再放到一個(gè)新的表中
每次生成數(shù)據(jù)之后對(duì)應(yīng)生成一個(gè)identifier,然后只要是針對(duì)數(shù)據(jù)的操作,就修改一次isDirty或isDeleted,然后發(fā)起請(qǐng)求帶上identifier和操作指令去告知服務(wù)器執(zhí)行相關(guān)的操作。如果是復(fù)雜的同步方式,那么每一次修改數(shù)據(jù)時(shí)就新生成一次identifier,然后再發(fā)起請(qǐng)求帶上相關(guān)數(shù)據(jù)告知服務(wù)器。
服務(wù)器根據(jù)請(qǐng)求包的identifier等數(shù)據(jù)執(zhí)行操作,操作完畢回復(fù)給客戶(hù)端確認(rèn)
收到服務(wù)器的確認(rèn)包之后,根據(jù)服務(wù)器給到的identifier(有的時(shí)候也會(huì)有tablename,取決于你的具體實(shí)現(xiàn))找到對(duì)應(yīng)的記錄,如果是刪除操作,直接把數(shù)據(jù)刪除就好。如果是插入和更新操作,就把isDirty置為NO。如果有額外的表記錄了更新操作,直接把identifier對(duì)應(yīng)的這個(gè)操作記錄刪掉就行。
要注意的點(diǎn)
在使用表去記錄更新操作的時(shí)候,短時(shí)間之內(nèi)很有可能針對(duì)同一條數(shù)據(jù)進(jìn)行多次更新操作。因此在同步之前,最好能夠合并這些相同數(shù)據(jù)的更新操作,可以節(jié)約服務(wù)器的計(jì)算資源。當(dāng)然如果你服務(wù)器強(qiáng)大到不行,那就無(wú)所謂了。
雙向數(shù)據(jù)同步
雙向數(shù)據(jù)同步多見(jiàn)于筆記類(lèi)、日程類(lèi)應(yīng)用。對(duì)于一臺(tái)設(shè)備來(lái)說(shuō),不光自己會(huì)往上推數(shù)據(jù)同步的信息,自己也會(huì)問(wèn)服務(wù)器主動(dòng)索取數(shù)據(jù)同步的信息,所以稱(chēng)之為雙向數(shù)據(jù)同步。
舉個(gè)例子:當(dāng)一臺(tái)設(shè)備生成了某時(shí)間段的數(shù)據(jù)之后,到了另外一臺(tái)設(shè)備上,又修改了這些舊的歷史數(shù)據(jù)。此時(shí)再回到原來(lái)的設(shè)備上,這臺(tái)設(shè)備就需要主動(dòng)問(wèn)服務(wù)器索取是否舊的數(shù)據(jù)有修改,如果有,就要把這些操作下載下來(lái)同步到本地。
雙向數(shù)據(jù)同步實(shí)現(xiàn)上會(huì)比單向數(shù)據(jù)同步要復(fù)雜一些,而且有的時(shí)候還會(huì)存在實(shí)時(shí)同步的需求,比如協(xié)同編輯。由于本身方案就比較復(fù)雜,另外一定要兼顧業(yè)務(wù)工程師的上手難度(這主要看你這個(gè)架構(gòu)師的良心),所以要實(shí)現(xiàn)雙向數(shù)據(jù)同步方案的話(huà),還是很有意思比較有挑戰(zhàn)的。
如何完成雙向數(shù)據(jù)同步的需求
封裝操作對(duì)象
這個(gè)其實(shí)在單向數(shù)據(jù)同步時(shí)多少也涉及了一點(diǎn),但是由于單向數(shù)據(jù)同步的要求并不復(fù)雜,只要告訴服務(wù)器是什么數(shù)據(jù)然后要做什么事情就可以了,倒是沒(méi)必要將這種操作封裝。在雙向數(shù)據(jù)同步時(shí),你也得解析數(shù)據(jù)操作,所以互相之間要約定一個(gè)協(xié)議,通過(guò)封裝這個(gè)協(xié)議,就做到了針對(duì)操作對(duì)象的封裝。
這個(gè)協(xié)議應(yīng)當(dāng)包括:
操作的唯一標(biāo)識(shí)
數(shù)據(jù)的唯一標(biāo)識(shí)
操作的類(lèi)型
具體的數(shù)據(jù),主要是在Insert和Update的時(shí)候會(huì)用到
操作的依賴(lài)標(biāo)識(shí)
用戶(hù)執(zhí)行這項(xiàng)操作時(shí)的時(shí)間戳
分別解釋一下這6項(xiàng)的意義:
1. 操作的唯一標(biāo)識(shí)
這個(gè)跟單向同步方案時(shí)的作用一樣,也是在收到服務(wù)器的確認(rèn)包之后,能夠使得本地應(yīng)用找到對(duì)應(yīng)的操作并執(zhí)行確認(rèn)處理。
2. 數(shù)據(jù)的唯一標(biāo)識(shí)
在找到具體操作的時(shí)候執(zhí)行確認(rèn)邏輯的處理時(shí),都會(huì)涉及到對(duì)象本身的處理,更新也好刪除也好,都要在本地?cái)?shù)據(jù)庫(kù)有所體現(xiàn)。所以這個(gè)標(biāo)識(shí)就是用于找到對(duì)應(yīng)數(shù)據(jù)的。
3. 操作的類(lèi)型
操作的類(lèi)型就是Delete,Update,Insert,對(duì)應(yīng)不同的操作類(lèi)型,對(duì)本地?cái)?shù)據(jù)庫(kù)執(zhí)行的操作也會(huì)不一樣,所以用它來(lái)進(jìn)行標(biāo)識(shí)。
4. 具體的數(shù)據(jù)
當(dāng)更新的時(shí)候有Update或者Insert操作的時(shí)候,就需要有具體的數(shù)據(jù)參與了。這里的數(shù)據(jù)有的時(shí)候不見(jiàn)得是單條的數(shù)據(jù)內(nèi)容,有的時(shí)候也會(huì)是批量的數(shù)據(jù)。比如把所有10月1日之前的任務(wù)都標(biāo)記為已完成狀態(tài)。因此這里具體的數(shù)據(jù)如何表達(dá),也需要定一個(gè)協(xié)議,什么時(shí)候作為單條數(shù)據(jù)的內(nèi)容去執(zhí)行插入或更新操作,什么時(shí)候作為批量的更新去操作,這個(gè)自己根據(jù)實(shí)際業(yè)務(wù)需求去定義就行。
5. 操作的依賴(lài)標(biāo)識(shí)
跟前面提到的依賴(lài)標(biāo)識(shí)一樣,是為了防止先發(fā)的包后到后發(fā)的包先到這種極端情況。
6. 用戶(hù)執(zhí)行這項(xiàng)操作的時(shí)間戳
由于跨設(shè)備,又因?yàn)榕f數(shù)據(jù)也會(huì)被更新,因此在一定程度上就會(huì)出現(xiàn)沖突的可能。操作數(shù)據(jù)在從服務(wù)器同步下來(lái)之后,會(huì)存放在一個(gè)新的表中,這個(gè)表就是待操作數(shù)據(jù)表,在具體執(zhí)行這些操作的同時(shí)會(huì)跟待同步的數(shù)據(jù)表中的操作數(shù)據(jù)做比對(duì)。如果是針對(duì)同一條數(shù)據(jù)的操作,且這兩個(gè)操作存在沖突,那么就以時(shí)間戳來(lái)決定如何執(zhí)行。還有一種做法就是直接提交到界面告知用戶(hù),讓用戶(hù)做決定。
新增待操作數(shù)據(jù)表和待同步數(shù)據(jù)表
前面已經(jīng)部分提到這一點(diǎn)了。從服務(wù)器拉下來(lái)的同步操作列表,我們存在待執(zhí)行數(shù)據(jù)表中,操作完畢之后如果有告知服務(wù)器的需求,那就等于是走單向同步方案告知服務(wù)器。在執(zhí)行過(guò)程中,這些操作也要跟待同步數(shù)據(jù)表進(jìn)行匹配,看有沒(méi)有沖突,沒(méi)有沖突就繼續(xù)執(zhí)行,有沖突的話(huà)要么按照時(shí)間戳執(zhí)行,要么就告知用戶(hù)讓用戶(hù)做決定。在拉取待執(zhí)行操作列表的時(shí)候,也要把最后一次操作的identifier丟給服務(wù)器,這樣服務(wù)器才能返回相應(yīng)數(shù)據(jù)。
待同步數(shù)據(jù)表的作用其實(shí)也跟單向同步方案時(shí)候的作用類(lèi)似,就是防止在發(fā)送請(qǐng)求的時(shí)候用戶(hù)有操作,同時(shí)也是為解決沖突提供方便。在發(fā)起同步請(qǐng)求之前,我們都應(yīng)該先去查詢(xún)有沒(méi)有待執(zhí)行的列表,當(dāng)待執(zhí)行的操作列表同步完成之后,就可以刪除里面的記錄了,然后再把本地待同步的數(shù)據(jù)交給服務(wù)器。同步完成之后就可以把這些數(shù)據(jù)刪掉了。因此在正常情況下,只有在待操作和待執(zhí)行的操作間會(huì)存在沖突。有些從道理上講也算是沖突的事情,比如獲取待執(zhí)行的數(shù)據(jù)比較晚,但其中又和待同步中的操作有沖突,像這種極端情況我們其實(shí)也無(wú)解,只能由他去,不過(guò)這種情況也是屬于比較極端的情況,發(fā)生幾率不大。
何時(shí)從服務(wù)器拉取待執(zhí)行列表
每次要把本地?cái)?shù)據(jù)丟到服務(wù)器去同步之前,都要拉取一次待執(zhí)行列表,執(zhí)行完畢之后再上傳本地同步數(shù)據(jù)
每次進(jìn)入相關(guān)頁(yè)面的時(shí)候都更新一次,看有沒(méi)有新的操作
對(duì)實(shí)時(shí)性要求比較高的,要么客戶(hù)端本地起一個(gè)線(xiàn)程做輪詢(xún),要么服務(wù)器通過(guò)長(zhǎng)鏈接將待執(zhí)行操作推送過(guò)來(lái)
其它我暫時(shí)也想不到了,具體還是看需求吧
雙向數(shù)據(jù)同步方案總結(jié)
設(shè)計(jì)好同步協(xié)議,用于和服務(wù)端進(jìn)行交互,以及指導(dǎo)本地去執(zhí)行同步下來(lái)的操作
添加待執(zhí)行,待同步數(shù)據(jù)表記錄要執(zhí)行的操作和要同步的操作
要注意的點(diǎn)
我也見(jiàn)過(guò)有的方案是直接把SQL丟出去進(jìn)行同步的,我不建議這么做。最好還是將操作和數(shù)據(jù)分開(kāi),然后細(xì)化,否則檢測(cè)沖突的時(shí)候你就得去分析SQL了。要是這種實(shí)現(xiàn)中有什么bug,解這種bug的時(shí)候就要考慮前后兼容問(wèn)題,機(jī)制重建成本等,因?yàn)樨潏D一時(shí)偷懶,到最后其實(shí)得不償失。
總結(jié)
這篇文章主要是基于CTPersistance講了一下如何設(shè)計(jì)持久層的設(shè)計(jì)方案,以及數(shù)據(jù)遷移方案和數(shù)據(jù)同步方案。
著重強(qiáng)調(diào)了一下各種持久層方案在設(shè)計(jì)時(shí)要考慮的隔離,以及提出了Virtual Record這個(gè)設(shè)計(jì)思路,并對(duì)它做了一些解釋。然后在數(shù)據(jù)遷移方案設(shè)計(jì)時(shí)要考慮的一些點(diǎn)。在數(shù)據(jù)同步方案這一節(jié),分開(kāi)講了單向的數(shù)據(jù)同步方案和雙向的數(shù)據(jù)同步方案的設(shè)計(jì),然而具體實(shí)現(xiàn)還是要依照具體的業(yè)務(wù)需求來(lái)權(quán)衡。
希望大家覺(jué)得這些內(nèi)容對(duì)各自工作中遇到的問(wèn)題能夠有所價(jià)值,如果有問(wèn)題,歡迎在評(píng)論區(qū)討論。
另外,關(guān)于動(dòng)態(tài)部署方案,其實(shí)直到今天在iOS領(lǐng)域也并沒(méi)有特別好的動(dòng)態(tài)部署方案可以拿出來(lái),我覺(jué)得最靠譜的其實(shí)還是H5和Native的Hybrid方案。React Native在我看來(lái)相比于Hybrid還是有比較多的限制。關(guān)于Hybrid方案,我也提供了CTJSBridge這個(gè)庫(kù)去實(shí)現(xiàn)這方面的需求。在動(dòng)態(tài)部署方案這邊其實(shí)成文已經(jīng)很久,遲遲不發(fā)的原因還是因?yàn)橛X(jué)得當(dāng)時(shí)并沒(méi)有什么銀彈可以解決iOS App的動(dòng)態(tài)部署,另外也有一些問(wèn)題沒(méi)有考慮清楚。當(dāng)初想到的那些問(wèn)題現(xiàn)在我已經(jīng)確認(rèn)無(wú)解。當(dāng)初寫(xiě)的動(dòng)態(tài)部署方案我一直認(rèn)為它無(wú)法作為一個(gè)單獨(dú)的文章發(fā)布出來(lái),所以我就把這篇文章也放在這里,權(quán)當(dāng)給各位參考。