📱

[Swift5] Realm5のよく使うところだけをサンプル付きでまとめてみた

2020/12/03に公開

Realm

概要

  • 保存できる型

    • Bool, Int, Int8, Int16, Int32, Int64, Double, Float, String, Date, Data.
  • CGFloatで保存することは推奨されない(できなくはないが型がRealmで保証されていない)

  • String, Date, DataOptional指定 が可能。
    その他の保存できる型で Optional指定 をしたい場合は、RealmOptionalを利用する。RealmOptionalは「Int, Float, Double, Bool」に対応。
    RealmOptionalプロパティは必ずletでなければいけない。

RealmDatabaseの作成(CREATE DATABASE)

// デフォルトのデータベース
let realm = try! Realm()
 
// ファイルを指定したデータベース
let realm = try! Realm(fileURL: YOUR_REALM_FILE_PATH)
 
// インメモリで使用するようにconfigを適用したデータベース
let configration = Realm.Configuration(inMemoryIdentifier: "メモリ空間名")
let realm = try! Realm(configration: configration) 

RealmDatabaseの削除(DROP DATABASE)

// データベースまるまる削除
NSFileManager.defaultManager().removeItemAtURL(Realm.Configuration.defaultConfiguration.fileURL!)
 
// ファイルは残すが、中身を空っぽに
try! realm.write {
  realm.deleteAll()
}

モデルの定義(CREATE TABLE)

import RealmSwift

// Dog model
class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var owner: Person? // Properties can be optional
}

// Person model
class Person: Object {
    @objc dynamic var name = ""
    @objc dynamic var birthdate = Date(timeIntervalSince1970: 1)
    let dogs = List<Dog>()
}

モデルの継承

// Base Model
class Animal: Object {
    @objc dynamic var age = 0
}

// Models composed with Animal
class Duck: Object {
    @objc dynamic var animal: Animal? = nil
    @objc dynamic var name = ""
}
class Frog: Object {
    @objc dynamic var animal: Animal? = nil
    @objc dynamic var dateProp = Date()
}

// Usage
let duck = Duck(value: [
  "animal": [ 
    "age": 3
  ],
  "name": "Gustav"
	]
)

モデルプロパティの属性

Realmモデルに定義されるプロパティは@obj dynamic var属性を持たなければならない。ただしSwift4以降で利用することのできる@objcMembers修飾子をクラスに宣言している場合、プロパティは単にdynamic var属性で宣言することができる。

/// Swift4 以前
// Dog model
class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var owner: Person? // Properties can be optional
}

/// Swift4 以降
// Dog model
@objcMembers
class Dog: Object {
    dynamic var name = ""
    dynamic var owner: Person? // Properties can be optional
}

ただし、LinkingObjects, List, RealmOptional属性は動的なプロパティとして宣言することができないので、常にletで宣言しなければならない。

Type Non-optional Optional
Bool @objc dynamic var value = false let value = RealmOptional<Bool>()
Int @objc dynamic var value = 0 let value = RealmOptional<Int>()
Float @objc dynamic var value: Float = 0.0 let value = RealmOptional<Float>()
Double @objc dynamic var value: Double = 0.0 let value = RealmOptional<Double>()
String @objc dynamic var value = "" @objc dynamic var value: String? = nil
Data @objc dynamic var value = Data() @objc dynamic var value: Data? = nil
Date @objc dynamic var value = Date() @objc dynamic var value: Date? = nil
Object n/a @objc dynamic var value: Class?
List let value = List<Type>() n/a
LinkingObjects let value = LinkingObjects(fromType: Class.self, property: "property") n/a

プライマリキー

モデルを定義する時に primaryKeyメソッドをオーバーライドすることで、モデルの主キーを設定することができる。主キーを設定したオブジェクトをRealmに保存すると、後から変更することができない。

class Person: Object {
    @objc dynamic var id = 0
    @objc dynamic var name = ""

    override static func primaryKey() -> String? {
        return "id"
    }
}

インデックス

Realmでは各モデルでプロパティのインデックスをindexedPropertiesメソッドをオーバーライドすることで作成できる。インデックスを作成することで、等号演算、IN演算しを用いたクエリを高速に実行することができる。(※ インデックスを作成するため Realmファイルのサイズは大きくなる)

RealmではString, Int, Bool, Date型プロパティのインデックスに対応している。

class Book: Object {
    @objc dynamic var price = 0
    @objc dynamic var title = ""

    override static func indexedProperties() -> [String] {
        return ["title"]
    }
}

プロパティの無視

Realmに保存する必要のない変数(一時使用、一時的な変数)をignoredPropertiesメソッドをオーバーライドすることで設定できる。これを設定することでプロパティとして操作、利用することができるが、保存する際にデータは無視される。また、getterしか持たない変数は、自動的にモデル保存時に無視される。

class Person: Object {
    @objc dynamic var tmpID = 0
    var name: String { // read-only properties are automatically ignored
        return "\(firstName) \(lastName)"
    }
    @objc dynamic var firstName = ""
    @objc dynamic var lastName = ""

    override static func ignoredProperties() -> [String] {
        return ["tmpID"]
    }
}

モデルの書き込み

モデルを書き込むためには、モデルオブジェクトをインスタンス化してRealmに追加する必要がある。

class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var age = 0
}

// (1) Create a Dog object and then set its properties
var myDog = Dog()
myDog.name = "Rex"
myDog.age = 10

// (2) Create a Dog object from a dictionary
let myOtherDog = Dog(value: ["name" : "Pluto", "age": 3])

// (3) Create a Dog object from an array
let myThirdDog = Dog(value: ["Fido", 5])
  1. オブジェクトのインスタンスを生成した後、直接プロパティに対して値を代入して作成する方法
  2. オブジェクトをインスタンス化する際にDictionaryを用いて、初期化して作成する方法
  3. オブジェクトをインスタンス化する際にArrayを用いて、初期化して作成する方法
    (※ この場合、配列内要素の並びはモデルのプロパティ並びと同じでなければならない。またモデル保存時に無視されるプロパティやgetterのみのプロパティなどは無視する必要がある)

モデルオブジェクトを作成後、Realmに追加することができる。

// Get the default Realm
let realm = try! Realm()
// You only need to do this once (per thread)

// Add to the Realm inside a transaction
try! realm.write {
    realm.add(myDog)
}

Realmへの書き込みトランザクションが終了すると、他のスレッドでも追加した値を利用することができるようになる。このとき、複数の書き込みが同時進行で発生した場合は、他方の書き込みが終わるまでロックされる。これによって書き込みがロックされているスレッドもロックされることを注意する必要がある。

ただし、書き込みトランザクションが終了していない場合であっても、読み込みは可能である。

モデルの更新

モデル更新の方法は複数ある。

let author = ....
// Update an object with a transaction
try! realm.write {
    author.name = "Thomas Pynchon"
}
let persons = realm.objects(Person.self)
try! realm.write {
    persons.first?.setValue(true, forKey: "isFirst")
    // set each person's planet property to "Earth"
    persons.setValue("Earth", forKey: "planet")
}

プライマリキーを設定している場合は、プライマリキーによる更新を行うことができる。

// Creating a book with the same primary key as a previously saved book
let cheeseBook = Book()
cheeseBook.title = "Cheese recipes"
cheeseBook.price = 9000
cheeseBook.id = 1

// Updating book with id = 1
try! realm.write {
    realm.add(cheeseBook, update: .modified)
}

この時、主キー(id: 1)がすでに存在していれば単に更新処理が走る。主キーが存在しない場合は create と同等の処理が実行される。また、主キーと更新したい値のサブセットを引数にすることで差分更新を行うことができる。

// Assuming a "Book" with a primary key of `1` already exists.
try! realm.write {
    realm.create(Book.self, value: ["id": 1, "price": 9000.0], update: .modified)
    // the book's `title` property will remain unchanged.
}

ただし、主キーを定義していないモデルオブジェクトを更新する場合には update: の引数に .modified, .all を渡すことができない。また、.modified, .allを使った多重(コンフリクトが発生するような)書き込みの結果には注意が必要。

/// current DB recode
Book: ["id": 1, title: "Cheese recipes" "price": 9000.0]

/// create some write transactions ( cause conflict )
Book: ["id": 1, title: "Fruit recipes", price: 9000], update: .all
Book: ["id": 1, title: "Cheese recipes", price: 4000], update: .all

この場合、2つの更新が同時に発生するためコンフリクトが発生する。これらの書き込みがマージされた結果として得られる結果は、以下のどちらかとなる。

Book: ["id": 1, title: "Fruit recipes", price: 9000]
Book: ["id": 1, title: "Cheese recipes", price: 4000]

しかしupdate: .modifiedとした場合は、挙動が変わる。

/// current DB recode
Book: ["id": 1, title: "No.4" "price": 9000.0]

/// create some write transactions ( cause conflict )
Book: ["id": 1, title: "Fruit recipes", price: 9000], update: .modified
Book: ["id": 1, title: "Cheese recipes", price: 4000], update: .modified

この場合の書き込みがマージされた結果は以下のようになる

Book: ["id": 1, title: "Fruit recipes", price: 4000]

.all は完全置き換え、.modified は差分置き換えであることを注意する必要がある。

モデルの削除

書き込みトランザクションの中で以下を実行する。

// let cheeseBook = ... Book stored in Realm

// Delete an object with a transaction
try! realm.write {
    realm.delete(cheeseBook)
}

// Delete all objects from the realm
try! realm.write {
    realm.deleteAll()
}

モデルの変更通知

Realmのオブジェクトインスタンスは自動的に更新される。つまり、オブジェクトのプロパティを変更すると、同じオブジェクトを参照している他のインスタンスに、その変更が即座に反映される。Realmオブジェクトの更新通知を購読することで、対象のRealmオブジェクトに変更があった場合に通知を取得できる。

/// Model
class StepCounter: Object {
    @objc dynamic var steps = 0
}

/// Realm Object
let stepCounter = StepCounter()
let realm = try! Realm()
try! realm.write {
    realm.add(stepCounter)
}

/// NotificationToken
var token : NotificationToken?
token = stepCounter.observe { change in
    switch change {
      
      /// Realm Object is change
    	case .change(let properties):
      	for property in properties {
        	if property.name == "steps" && property.newValue as! Int > 1000 {
          	print("Congratulations, you've exceeded 1000 steps.")
            token = nil
          }
        }
      
      /// Realm Object cause error
    	case .error(let error):
        print("An error occurred: \(error)")
      
      /// Realm Object was deleted
    	case .deleted:
        print("The object was deleted.")
    }
}

モデル間のリレーション

  • 1:N, N:1
// Dog model
class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var owner: Person? // Properties can be optional
}

// Person model
class Person: Object {
    @objc dynamic var name = ""
    @objc dynamic var birthdate = Date(timeIntervalSince1970: 1)
    let dogs = List<Dog>()
}

/// to-one relation
let jim = Person()
let rex = Dog()
rex.owner = jim

/// Many-to-one
let someDogs = realm.objects(Dog.self).filter("name contains 'Fido'")
jim.dogs.append(objectsIn: someDogs)
jim.dogs.append(rex)
  • 双方向リレーション
    一般的にモデル同士のリレーションはリンクされてる側からインスタンスを参照することはできるが、逆方向からは辿ることができない。RealmではLinkingObjectsを用いることで、双方向リレーションを形成することができる。LinkingObjectsは特定のプロパティ(ここではPersonモデルのdogsプロパティ)から与えられたオブジェクトにリンクされているすべてのオブジェクトを取得することができる。
class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var age = 0
    let owners = LinkingObjects(fromType: Person.self, property: "dogs")
}

Appendix

JSONによるRecord作成

Realm は直接JSONをサポートしていないが、JSONSerialization の出力を用いでオブジェクトを生成することができる。JSON内にネストされてるオブジェクトや配列は、自動的にリレーションにマッピングされることに注意する必要がある。

この方法でオブジェクトを生成するときには、いくつかの注意点が存在する。

  • オブジェクトのプロパティ名とJSONのキー名が一致して、型が同じである必要があります。

  • floatプロパティは、floatをバックにしたNSNumbersで初期化する必要があります。

  • DateプロパティとDataプロパティは文字列から自動的に推論することはできませんが、Realm().create(_:value:update:)に渡す前に適切な型に変換しなければなりません。

  • 必須プロパティに JSON NULL (すなわち NSNull) が与えられた場合、例外がスローされます。

  • insert 時に必須のプロパティが与えられない場合は、例外がスローされます。

  • Realmは、Objectで定義されていないJSON内のプロパティを無視します。

// A Realm Object that represents a city
class City: Object {
    @objc dynamic var name = ""
    @objc dynamic var cityId = 0
    // other properties left out ...
}

let data = "{\"name\": \"San Francisco\", \"cityId\": 123}".data(using: .utf8)!
let realm = try! Realm()

// Insert from Data containing JSON
try! realm.write {
    let json = try! JSONSerialization.jsonObject(with: data, options: [])
    realm.create(City.self, value: json, update: .modified)
}

モデルオブジェクト更新時の設定

Realm では Realm に書き込まれたか否かで、更新方法が変わる場合がある。

  • アンマネージドオブジェクト
    Realm に書き込まれる前のオブジェクト( realm.write クロージャーの外でプロパティを変更できる)
  • マネージドオブジェクト
    Realm に書き込まれた後のオブジェクト (realm.write クロージャーの外でプロパティを変更できない)
@IBAction func tappedButton(_ sender: Any) {
   
  /*
   * この時点のpersonはRealmに書き込まれていないのでアンマネージドオブジェクト
   */
  let person = Person(value: ["name": "Yu", "age": 32])
  
  // 書き込まれていないので、realm.write内でなくてもプロパティの更新は問題なし
  person.mood = "Happy" 

  do {
      let realm = try Realm()
      try! realm.write {
        realm.add(person) // PersonをRealmに保存したことで、マネージドオブジェクトになる
        print("1回目成功だよ", person)
      }
  } catch {
      print("エラーだよ")
  }
  
  /*
   * この時点のPersonはマネージドオブジェクトなので、プロパティの更新はrealm.write内のみ可能
   */
  
  // person.mood = "sad" // 例外が発生する
  do {
      let realm = try Realm()
      try! realm.write {
          person.mood = "Sad" // プロパティの更新
          print("2回目成功だよ", person)
      }
  } catch {
      print("エラーだよ")
  }
}

一般的には、このように分離して処理を変更するべきであるが、以下のようにすることもできる。

@IBAction func tappedButton(_ sender: Any) {
   
  /*
   * この時点のpersonはRealmに書き込まれていないのでアンマネージドオブジェクト
   */
  let person = Person(value: ["name": "Yu", "age": 32])
  
  // 書き込まれていないので、realm.write内でなくてもプロパティの更新は問題なし
  person.mood = "Happy" 

  do {
      let realm = try Realm()
      try! realm.write {
        realm.add(person, updated: .modified) // PersonをRealmに保存したことで、マネージドオブジェクトになる
        print("1回目成功だよ", person)
      }
  } catch {
      print("エラーだよ")
  }
  
  /*
   * この時点のPersonはマネージドオブジェクトなので、プロパティの更新はrealm.write内のみ可能
   */
  
  // person.mood = "sad" // 例外が発生する
  do {
      let realm = try Realm()
      try! realm.write {
          realm.add(person, updated: .modified) "Sad" // プロパティの更新
          print("2回目成功だよ", person)
      }
  } catch {
      print("エラーだよ")
  }
}
GitHubで編集を提案

Discussion