最近做個項目是需要大量的本地數據交互保存持久化操作,由于是新項目所以我們打算使用比較新穎的框架來進行開發,最后經過篩選使用了Realm來作為本地數據操作框架。name我們為什么選擇realm呢?大部分的數據庫框架還是使用2000年的SQLite,大部分的移動應用還是直接或間接的使用SQLite來作為本地數據庫比如:FMDB、Couchbase Lite,Core Data,ORMLite,而Realm是專門為移動端設計的框架,最后我們經過比對選擇了Realm。
首先Realm 是一個跨平臺的移動數據庫引擎,其性能要優于 FMDB、Couchbase Lite,Core Data,ORMLite - 移動端數據庫性能比較, 我們可以在 Android 端 realm-javaKotlin也可以使用,iOS端:Realm-Cocoa,同時支持 OC 和 Swift兩種語言開發。使用操作簡單、性能優異、跨平臺、開發效率得到了大大提高(省去了數據模型與表存儲之間轉化的很多工作)、配備可視化數據庫查看工具。這些都滿足了我們項目的需要。
對于Realm的使用今天不在這里介紹,網上可以搜到很多具體的使用方法,也可以到官網文檔上查看Api。我們主要剖析下在項目開發過程中遇到到問題、疑難雜癥和解決的方案。
我們先來看下Realm不支持的地方及需要注意的地方:
1.不支持聯合主鍵
2.不支持自增長主鍵
3.不能跨線程共享realm實例,不同線程中,都要創建獨立的realm實例,只要配置(configuration)相同,它們操作的就是同一個實體數據庫。
4.存取只能以對象為單位,不能只查某個屬性,使用sql時,可以單獨查詢某個(幾個)獨立屬性,比如 select courseName from Courses where courseId = "001",而在realm中 + (RLMResults *)objectsWhere類似這種返回的是RLMResults對象。查詢相關函數,得到的都是對象的集合,相對不夠靈活。
5.被查詢的RLMResults中的對象,任何的修改都會被直接同步到數據庫中,所以對對象的修改都必須被包裹在beginWriteTransaction中,Swift要包裹在try! Realm().write { }中,使用時要注意。
例如:
let results = SXRealm.queryByAll(DetailModel.self)
let item = results[0]
try! Realm().write {//修改數據,必須在此操作中,否則會造成Crash。
item.uploadStatus = 2
item.uploadFailedDes = "上傳失?。?
}
6.RLMResults與線程問題,在主線程查出來的數據,如果在其他線程被訪問是不允許的,運行時會報錯。
例如:
//這種是錯誤的,只能訪問同一線程的realm數據。
RLMResults *results = [Course objectsWhere:@"courseId = '001'"];
Course *getCourse = [results objectAtIndex:0];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%@",results);
NSLog(@"%@",getCourse.courseName);
});
7.auto-updating機制,十分方便,并保證了數據的實時性,但是在個別情況下,也許這種機制并不需要,可能會導致一些意外,所以需要注意。(OC舉例)
RLMRealm *realm = [RLMRealm defaultRealm];
Course *course = [[Course alloc] init];
course.courseId = @"001";
course.courseName = @"語文";
[realm transactionWithBlock:^{
[realm addObject:course];
}];
Course *getCourse1 = [Course objectsWhere:@"courseId = '001'"].firstObject;
NSLog(@"%@",getCourse1);
[realm transactionWithBlock:^{
getCourse1.courseName = @"體育";
}];
NSLog(@"%@",course);
(1)第一次查詢后,result中有一條記錄,后面即便沒有執行重新查詢,新加入的數據,自動就被同步到了result中。
RLMRealm *realm = [RLMRealm defaultRealm];
Course *course = [[Course alloc] init];
course.courseId = @"001";
course.courseName = @"語文";
[realm beginWriteTransaction];
[Course createOrUpdateInDefaultRealmWithValue:course];
[realm commitWriteTransaction];
RLMResults *result = [Course allObjects];
NSLog(@"%@",result);
Course *course2 = [[Course alloc] init];
course2.courseId = @"002";
course2.courseName = @"數學";
[realm beginWriteTransaction];
[Course createOrUpdateInDefaultRealmWithValue:course2];
[realm commitWriteTransaction];
NSLog(@"%@",result);
(2)開始查詢出課程id為001的課程模型getCourse1、getCourse2的課程名為語文,后面僅對getCourse2進行修改后,getCourse1的屬性也被自動同步更新了。
RLMRealm *realm = [RLMRealm defaultRealm];
Course *course = [[Course alloc] init];
course.courseId = @"001";
course.courseName = @"語文";
[realm beginWriteTransaction];
[Course createOrUpdateInDefaultRealmWithValue:course];
[realm commitWriteTransaction];
Course *getCourse1 = [Course objectsWhere:@"courseId = '001'"].firstObject;
NSLog(@"%@",getCourse1);
Course *getCourse2 = [Course objectsWhere:@"courseId = '001'"].firstObject;
[realm beginWriteTransaction];
getCourse2.courseName = @"體育";
[realm commitWriteTransaction];
NSLog(@"%@",getCourse1);
(3).在別的線程中的修改,也會被同步過來
Course *getCourse1 = [Course objectsWhere:@"courseId = '001'"].firstObject;
NSLog(@"%@",getCourse1);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
RLMRealm *realm = [RLMRealm defaultRealm];
Course *getCourse2 = [Course objectsWhere:@"courseId = '001'"].firstObject;
[realm beginWriteTransaction];
getCourse2.courseName = @"體育";
[realm commitWriteTransaction];
NSLog(@"%@",getCourse2);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"%@",getCourse1);
});
});
8.從realm數據庫讀取出的數據模型,setter/getter方法會失效,集成realmObject的實力類setter/getter方法會失效,當賦值的時候不會走set方法。
到這里我們已經對Realm有了一定的了解,也熟悉了它的機制。
下面來說下在開發項目的時候具體碰到的問題:
一.數據解析轉換存儲,反轉換問題
由于項目中操作數據轉換的地方多,需要Json轉Model存入realm,獲取realm數據Model轉換成Json,但是realmSwift只支持把json轉換成realm所需的存儲Model,而不支持反轉。而Android的realm卻可以,這讓我很苦惱,而我又不想手動一二個一個來轉換,1是我們數據量太多,我覺得這種太耗費精力2是也覺得這樣做有些low,于是乎遇到了瓶頸,逛各種技術論壇也沒有找到解決方案。靜下心來開始思考看HandyJson和realm的源碼,最后發現原來realm的數據類型是它自己定義的數組類型,而不是繼承iOSSwift的數據類型,這就造成HandyJson解析庫識別不了這些數據類型,最后導致沒辦法數據相互轉換。
解決方案:
1.建立數據Model的時候需要在BaseModel里添加兩個方法函數解決list解析
import Foundation
import RealmSwift
import Realm
import HandyJSON
class BaseRLMObject: Object, NSCopying {
func copy(with zone: NSZone? = nil) -> Any {
return type(of: self).init()
}
//這個父類添加的屬性,子類解析不會賦值,因此在子類各自添加
// @objc dynamic var primaryKey = UUID().uuidString
// override static func primaryKey() -> String? {
// return "primaryKey"
// }
//解析的Array數據添加到realm方法 例如:請求的Array數據需要添加到realm List數據庫時調用
//注意點:realmlist直接.append(objectsIn:)添加swift數組的時候,是可以添加到realmlist中的,原因realmlist數組能夠識別swift數組類型,但是反之就不行
func addRealmData(){
}
//realm List數據傳遞給正常的Array方法 例如:realm List數據轉換成model Array時調用
//注意點:swift數組直接.append(contentsOf:)添加realmlist的時候,是添加不到正常數組里的,原因正常的swift數組不識別realmlist類型,但是反之就可以
func addOriginalData(){
}
}
2.子類需要繼承父類,然后實現這兩個方法,并且相同數組key屬性都需要創建兩個(一個是Json轉換Realm數據需要,一個是Realm數據轉換Json需要),每層都需要實現。
3.需要在HandyJson的ignoredProperties中忽略正常的list數據,否則會在realm數據庫的字段表中出現該字段。
4.如果Bool型、Int型、Float型、Double型是需要非可空值的形式,則不需要特殊處理,但是如果這四種類型的數據是可空值形式,則需要特殊處理,轉換成String類型。原因是Bool、Int、Float、Double的可空值形式是RealmOptional<類型>(),解析庫識別不了realm自己定義的數據類型。
具體代碼:
import Foundation
import RealmSwift
import Realm
import HandyJSON
class PhotoModel : BaseRLMObject, HandyJSON {
@objc dynamic var primaryKey = UUID().uuidString
override static func primaryKey() -> String? {
return "primaryKey"
}
// let id = RealmOptional<Int>()
@objc dynamic var id: String? = nil
// let vehicleId = RealmOptional<Int>()
@objc dynamic var type: String? = nil
@objc dynamic var delFlag:Bool = false // 刪除標記
let damageInfoList_realm: List<DamageInfoModel> = List<EQSDamageInfoModel>()//損傷點
var damageInfoList: [DamageInfoModel] = []
override static func ignoredProperties() -> [String] {
return ["damageInfoList"]
}
override func addRealmData() {
for item in self.damageInfoList {
item.addRealmData()
}
if self.damageInfoList_realm.count > 0 && self.damageInfoList.count > 0 {
self.damageInfoList_realm.removeAll()
}
self.damageInfoList_realm.append(objectsIn: self.damageInfoList)
}
override func addOriginalData() {
if self.damageInfoList.count > 0 && self.damageInfoList_realm.count > 0{
self.damageInfoList.removeAll()
}
for item in self.damageInfoList_realm {
item.addOriginalData()
self.damageInfoList.append(item)
}
}
}
在使用的時候每次轉換都需要調用add方法
//添加到realm數據庫
if let object = JSONDeserializer<Model>.deserializeFrom(json: json) {
object.addRealmData()
SXRealm.addAsync(object)
}
//realm數據庫數據轉換成Json
let model = SXRealm.queryByPrimaryKey(DetailModel.self, primaryKey: detailModel.primaryKey)
guard model == nil else {
SXRealm.doWriteHandler {
model.addOriginalData()
}
let json = mode.toJSON()!
}
二.primaryKey主鍵問題
經過測試逐漸定義不能在父類基礎類定義,必須要在各個子類都要定義。Realm的機制可能是檢測到這個字段有值就不會重新自動賦值,所以說不能偷懶在父類定義。
//這個父類添加的屬性,子類解析不會賦值,因此在子類各自添加
@objc dynamic var primaryKey = UUID().uuidString
override static func primaryKey() -> String? {
return "primaryKey"
}
三.刪除對應數據問題
根據Realm提供的刪除方法,只能刪除該對象,卻不能刪除該對象相關聯的對象,這點感覺很坑,如果只刪除該對象后,其相關聯的對象就會變成臟數據,永遠保存在數據庫中,會造成體積越來越大。
解決方案:
1.采用代碼批量刪除方法,把該對象下邊的list中的數據循環刪除(先刪除子對象,再刪除外層對象)
func deleteOrganizationUpgradeRealm() {
let data = SXRealm.BGqueryByAll(OrganizationItem.self)
if data.count > 0 {
SXRealm.BGdelete(SXRealm.BGqueryByAll(ChildItem.self))
SXRealm.BGdelete(SXRealm.BGqueryByAll(OrganizationItem.self))
}
}
static func BGdelete<T: Object>(_ objects: Results<T>) {
try! Realm().write {
try! Realm().delete(objects)
}
}
2.采用遞歸方式刪除(對于復雜數據結構,但是數據量超級大的時候不建議使用此方法)
static func BGdeleteRealmCascadeObject(object:Object){
for property in object.objectSchema.properties {
if property.type == .object{
if property.isArray{
let list:RLMArray<AnyObject> = RLMArray(objectClassName: property.objectClassName!)
list.addObjects(object.value(forKeyPath: property.name) as! NSFastEnumeration)
for i in 0..<list.count {
deleteRealmCascadeObject(object: list.object(at: i) as! Object)
}
} else {
let object:SXRLMObject = object.value(forKeyPath: property.name) as! SXRLMObject
if !object.isInvalidated{
try! Realm().delete(object)
}
}
}
}
if !object.isInvalidated{
try! Realm().delete(object)
}
}
四.修改更新操作realm對象時,需要在寫入操作中實現,并且只能有一層寫入操作方法。
//在這如果做了doWrite操作,name在addOriginalData方法中就不能做都Write操作,否則Crash。
SXRealm.doWriteHandler {
model.addOriginalData()
}
static func doWriteHandler(_ clouse: @escaping ()->()) { // 這里用到了 Trailing 閉包
try! sharedInstance.write {
clouse()
}
}
五.realm數據對象不能帶alloc、new、copy、mutableCopy之類的跟iOS語言相關的關鍵字、前綴字段,否則會造成Crash。(這點感覺好蛋疼)那么我們只能夠跟之前操作list的時候一樣,同樣的原理做橋接。
解決方法:
//解析使用 realm 不能有new alloc "copy", "mutableCopy" 等關鍵字前綴字段
var newVehicleSuggestionPrice: String? = nil
var newVehicleNetPrice:String? = nil
@objc dynamic var vehicleSuggestionPrice_realm: String? = nil
@objc dynamic var vehicleNetPrice_realm: String? = nil
//忽略realm數據庫對應字段
override static func ignoredProperties() -> [String] {
return ["newVehicleSuggestionPrice","newVehicleNetPrice"]
}
//注意點:realmlist直接.append(objectsIn:)添加swift數組的時候,是可以添加到realmlist中的,原因realmlist數組能夠識別swift數組類型,但是反之就不行
override func addRealmData() {
self.vehicleSuggestionPrice_realm = self.newVehicleSuggestionPrice
self.vehicleNetPrice_realm = self.newVehicleNetPrice
}
//注意點:swift數組直接.append(contentsOf:)添加realmlist的時候,是添加不到正常數組里的,原因正常的swift數組不識別realmlist類型,但是反之就可以
override func addOriginalData() {
self.newVehicleSuggestionPrice = self.vehicleSuggestionPrice_realm
self.newVehicleNetPrice = self.vehicleNetPrice_realm
}
六.系統的數組和realm數組轉換問題
如果需要把系統的數組中的數據添加到realm數組中可以直接調用realm數組的.append(objectsIn: Sequence)方法
public func append<S: Sequence>(objectsIn objects: S) where S.Iterator.Element == Element {
for obj in objects {
_rlmArray.add(dynamicBridgeCast(fromSwift: obj) as AnyObject)
}
}
但是如果需要把realm數組中的數據添加到系統的數組中,就不能使用系統的.append(contentsOf: Sequence)方法,而需要自己遍歷循環一個一個添加
//list_realm:realm數組類型變量 list:系統的長長數組類型變量
for item in self.list_realm {
self.list.append(item)
}
七.description HandyJson解析問題
這個問題其實不是realm的問題,而是HandyJson的問題,HandyJson的時候對于Json中的description字段是解析不成功的,按照正常操作是需要進行一層轉換,但是又由于與realm的Model是同一個Model,兩者共同使用就造成了問題的出現,想要轉換的變量必須以var來修飾,而realm中則需要@objc dynamic var來修飾,因此就出現了這個問題
解決方法:
需要中間創建個變量進行橋接,在轉換的時候同時進行賦值操作轉換。
import Foundation
import RealmSwift
import Realm
import HandyJSON
class XXXModel: SXRLMObject, HandyJSON{
@objc dynamic var primaryKey = UUID().uuidString
override static func primaryKey() -> String? {
return "primaryKey"
}
//解析使用description關鍵字系統不支持
var sdescription: String = ""http://圖片描述
@objc dynamic var description_realm: String = ""http://圖片描述
func mapping(mapper: HelpingMapper) {
// specify 'description' field in json map to 'sdescription' property in object
mapper <<<
self.sdescription <-- "description"
}
override static func ignoredProperties() -> [String] {
return ["sdescription"]
}
override func addRealmData() {
self.description_realm = self.sdescription
}
override func addOriginalData() {
self.sdescription = self.description_realm
}
}