[Swift5] Realm5のよく使うところだけをサンプル付きでまとめてみた
Realm
概要
-
保存できる型
-
Bool
,Int
,Int8
,Int16
,Int32
,Int64
,Double
,Float
,String
,Date
,Data
.
-
-
CGFloat
で保存することは推奨されない(できなくはないが型がRealmで保証されていない) -
String
,Date
,Data
はOptional
指定 が可能。
その他の保存できる型で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])
- オブジェクトのインスタンスを生成した後、直接プロパティに対して値を代入して作成する方法
- オブジェクトをインスタンス化する際に
Dictionary
を用いて、初期化して作成する方法 - オブジェクトをインスタンス化する際に
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("エラーだよ")
}
}
Discussion