1. 程式人生 > Android開發 >Realm使用中碰到的問題(坑)及解決方案

Realm使用中碰到的問題(坑)及解決方案

realm

最近做個專案是需要大量的本地資料互動儲存持久化操作,由於是新專案所以我們打算使用比較新穎的框架來進行開發,最後經過篩選使用了Realm來作為本地資料操作框架。name我們為什麼選擇realm呢?大部分的資料庫框架還是使用2000年的SQLite,大部分的移動應用還是直接或間接的使用SQLite來作為本地資料庫比如:FMDB、Couchbase Lite,CoreData,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,^{
        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解析庫識別不了這些資料型別,最後導致沒辦法資料相互轉換。

realm資料型別

解決方案:

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 = ""//圖片描述
    @objc dynamic var description_realm: String = ""//圖片描述
    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
    }
}
複製程式碼
以上就是RealmSwift的一些特性和我們專案中實踐過程踩過的坑。如果之後使用過程中碰到問題,會持續更新。