🌱

RealmSwift 入門の巻 〜イラストでわかりやすく〜

2021/03/16に公開

Realmを使ってた際のメモ書を、イラスト付き で載っけておきます。
コピペで使えます。

大体は Realm公式に書いてあることだけど

RealmSwiftとは

Podsライブラリの一つ。
既存の保存方法に UserDefaults がありますが、なぜこちらではなく、 RealmSwiftを使う必要性があるのか。

UserDefaults は、IntString などの値をそのまま保存するのみです。
独自のクラス形式を保存することはできません。

RealmSwift は、独自のクラス形式の状態で保存できます。
また、オブジェクト同士の関係性(リレーション) も、同時に管理できます。

あと、サービスの Realm と連携できたり…(本記事では紹介しません)

Realmの作成

今回は UserProduct というクラスを作り、例を説明します。

import RealmSwift

class User: Object {
  @objc dynamic var id = ""
  @objc dynamic var name = ""
  @objc dynamic var biography = ""
  @objc dynamic var productsNum = 0
}


class Product: Object {
  @objc dynamic var id = ""
  @objc dynamic var title = ""
  @objc dynamic var itemDescription = ""
  @objc dynamic var createdAt = Date()
  @objc dynamic var likesNum = 0
  @objc dynamic var creator: User? //Realmモデルの中に、別のRealmモデルがある
}

オブジェクトの作成方法は、通常のそれと同じです。
(他の書き方もありますが、それに関しては別の記事を参考してね。)

let user = User()
user.id = "1"
user.name = "むらびと"
user.biography = "伝説の(せん)とうみん(ぞく)"


let product = Product()
product.id = "1"
product.title = "りんご"
product.itemDescription = "ぼくの島の特産物です。みかん求む。"
product.creator = user

Realmに保存

オブジェクトを生成したはいいですが、まだ保存をしていません。
なので、Realmに一度保存してみましょう。

やり方は簡単。さっき作ったオブジェクト を使うだけ。
writeトランザクション中に add しましょう。

let realm = try! Realm()

// 今回は user を追加してみる。
try! realm.write {
  realm.add(user)
}

以上!!
簡単ですね。

これで、userマネージドオブジェクトManagedObject という Realm管理状態 なりました。

Realmから検索・取得

では、早速登録したオブジェクトを取り出してみましょう。
クエリ検索というやつです。

取得時は Results<Element> になっています。
配列 なので、必要なオブジェクトを取り出しましょう。

クラス毎の全データ検索

let realm = try! Realm()

// 全データ検索
let results = realm.objects(User.self)
print(results)
Results<User> <0x155f08ad0> (
	[0] User {
		id = 1;
		name = むらびと;
		biography = 伝説の(せん)とうみん(ぞく);
		productsNum = 0;
	}
)

さらに条件で検索

let realm = try! Realm()

// 全データ検索
let results = realm.objects(User.self)

// 今回は、idで検索
let id = "1"
let predicate = NSPredicate(format: "id == %@", id)

if let user = results.filter(predicate).first {
  print(user)
} else {
  print("no data")
}
User {
	id = 1;
	name = むらびと;
	biography = 伝説の(せん)とうみん(ぞく);
	productsNum = 0;
}

条件検索も色々指定できます。
それについては、『NSPredicate realm 条件』🔎

Realmに更新

Realmに 保存したオブジェクト
Realmから 検索・取得したオブジェクト
これらは、マネージドオブジェクトManagedObject という Realm管理状態 となっています。

そのため、更新は writeトランザクションの中 で行う必要があります。

print(user.name) // -> むらびと

try! realm.write {
  user.name = "たぬ○ち"
}

実際に確認してみると…

let id = "1"
let predicate = NSPredicate(format: "id == %@", id)

let realm = try! Realm()
if let user = realm.objects(User.self).filter(predicate).first {
  print(user)
}
User {
	id = 1;
	name = たぬ○ち;
	biography = 伝説の(せん)とうみん(ぞく);
	productsNum = 0;
}

はい。変わってますね。
ちなみに、更新処理の書き方にも色々あります。

Realmから削除

削除する(単体)

writeトランザクション中に delete します。
これで、マネージドオブジェクトManagedObject 状態が解除され、Realmから完全に削除されます。

let realm = try! Realm()

try! realm.write {
  realm.delete(user)
}

全て削除する

全部消したい場合は deleteAll で全て消えます。

let realm = try! Realm()

try! realm.write {
  realm.deleteAll()
}

実際に確認してみると…

let id = "1"
let predicate = NSPredicate(format: "id == %@", id)

let realm = try! Realm()
if let user = realm.objects(User.self).filter(predicate).first {
  print(user)
} else {
  print("no data")
}
no data

消えています。大丈夫ですね。

ここからが本番

ここまで淡々と説明してきましたが、ある一点について全く説明していません。
それが、オブジェクト間の関係性(リレーション) です。

import RealmSwift

class User: Object {
  @objc dynamic var id = ""
  @objc dynamic var name = ""
  @objc dynamic var biography = ""
  @objc dynamic var productsNum = 0
}


class Product: Object {
  @objc dynamic var id = ""
  @objc dynamic var title = ""
  @objc dynamic var itemDescription = ""
  @objc dynamic var createdAt = Date()
  @objc dynamic var likesNum = 0
  @objc dynamic var creator: User? //Realmモデルの中に、別のRealmモデルがある
}

これらに書いてあるとおり、Product の中には User が格納できます
つまり、この2つには 関係性がある ということですね。

格納した状態で、実際にrealmに登録してみると…??

オブジェクト間の関係性(リレーション)

let user = User()
user.id = "1"
user.name = "むらびと"
user.biography = "伝説の(せん)とうみん(ぞく)"

let product = Product()
product.id = "1"
product.title = "りんご"
product.itemDescription = "ぼくの島の特産物です。みかん求む。"
product.creator = user


let realm = try! Realm()

// 今回は product を追加してみる。
try! realm.write {
  realm.add(product)// ← product だけでなく、中の user も別に登録される…??
}

Product の中には、 User が入っています。
この状態で、Product だけをRealmに追加すると、一体どうなるのでしょう…??

気になりますね…

気になるでしょう?

おや…???

productを追加しただけのはずですが、

結果としては、
productuser の両方が、Realm に保存されます

実際に確認してみると…

let id = "1"
let predicate = NSPredicate(format: "id == %@", id)

let realm = try! Realm()

// User が追加されているのか確認します。
if let user = realm.objects(User.self).filter(predicate).first {
  print("get user")
  print(user)
} else {
  print("no user")
}

// Product が追加されているのか確認します。
if let product = realm.objects(Product.self).filter(predicate).first {
  print("get product")
  print(product)
} else {
  print("no product")
}
get user
User {
	id = 1;
	name = むらびと;
	biography = 伝説の(せん)とうみん(ぞく);
	productsNum = 0;
}

get product
Product {
	id = 1;
	title = りんご;
	itemDescription = ぼくの島の特産物です。みかん求む。;
	createdAt = 2021-03-13 10:25:37 +0000;
	likesNum = 0;
	creator = User {
		id = 1;
		name = むらびと;
		biography = 伝説の(せん)とうみん(ぞく);
		productsNum = 0;
	};
}

productは、userとの関係性を維持したまま、両方とも保存されています。

未登録のRealmオブジェクト(アンマネージドオブジェクト)は、
writeトランザクション中の add で、中身ごと保存されます。

ただ、ここで一つ問題があります。
id="1"user既に保存されていた状態で、この操作を行った場合…どうなるでしょう?

答えは、同じidのuser が、複数存在することになります
これを防ぐためには…

プライマリキー(主キー)

まあ、簡単に言うと ID を設定するときに、同じIDを持つオブジェクトが、複数存在しないようにする ってこと。
ユーザーIDを作りたいときには必要ですね。

設定の方法は簡単。
Realmモデル内で、primaryKey() をオーバーライドするだけ。

import RealmSwift

class User: Object {
    @objc dynamic var id = ""
    @objc dynamic var name = ""
    @objc dynamic var emailAddress = ""
    @objc dynamic var biography = ""
    @objc dynamic var imageUrl = ""
    
    // オーバーライドし、プライマリキーにしたい変数名を返す。
    override static func primaryKey() -> String? {
        return "id"
    }
}
let user = User()
user.id = "1"
user.name = "むらびと"
user.biography = "伝説の(せん)とうみん(ぞく)"

let product = Product()
product.id = "1"
product.title = "りんご"
product.itemDescription = "ぼくの島の特産物です。みかん求む。"
product.creator = user


let realm = try! Realm()
try! realm.write {
  realm.add(product) // <- エラーの危険性
}

ただ、注意点が1つ。(だけじゃないけど)

保存されているRealmオブジェクトと、
同一のプライマリキーを持つオブジェクト を add しようとすると、落ちます。

reason: 'Attempting to create an object of type 'User' with an existing primary key value '1'.'

なお、中に入ってるRealmオブジェクトは問題ない模様。

結局のところ、
素直に あらかじめ新規保存・更新処理でも大丈夫にしておく必要がある

let realm = try! Realm()

try! realm.write {
  realm.add(product, update: .modified) // <- これで落ちない
}

はい。
これで「差分・新規」両方の保存ができます。

これで「知らないうちに、たくさん同じRealmが追加されてた!!」ってケースはないですね。

プライマリキーの注意点

プライマリキーに設定した値は、変更できないので注意。

reason: 'Primary key can't be changed after an object is inserted.'

Realm用のクラス編集時の注意点

マイグレーション

最初のうちは、Realm用クラスを作り、これを使い続けるため問題ないかとおもいます。
しかし…

import RealmSwift

class User: Object {
  @objc dynamic var id = ""
  @objc dynamic var name = ""
  @objc dynamic var nickname = "" // <-新しく追加した
  @objc dynamic var biography = ""
  @objc dynamic var productsNum = 0
}

class Product: Object {
  @objc dynamic var id = ""
  @objc dynamic var title = ""
  @objc dynamic var itemDescription = ""
  @objc dynamic var createdAt = Date()
  @objc dynamic var likesNum = 0
  @objc dynamic var creator: User?
}


let realm = try! Realm() // <- このまま Realm を呼び出すと、落ちる。

エラーコード

Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=io.realm Code=10 "Migration is required due to the following errors:
- Property 'User.nikname' has been added." UserInfo={NSLocalizedDescription=Migration is required due to the following errors:
- Property 'User.nikname' has been added., Error Code=10}

リリースした後など、 Realm用クラスに、変数を増やすなどの変更を加えたりした場合
次にアプリを起動した際、既に保存してあるデータとの型違いで 落ちます よね。
これは厄介ですね。

そういった場合はRealmを使用する前に 「マイグレーション」 という処理を行いましょう
Realm に保存してあるデータを、最新のクラスに合わせます。

let CurrentSchemaVersion = 1 // 現在の SchemaVersion を、どこかに管理しておくといい。

let config = Realm.Configuration(schemaVersion: CurrentSchemaVersion, migrationBlock: { migration, oldSchemaVersion in
  if (oldSchemaVersion < CurrentSchemaVersion) {
    // 必要に応じて、パラメーターを操作。
    // 変数を追加するだけなら、中の処理は不要
    
    // version 1
    if oldSchemaVersion < 1 {
      
      // 今回の例では、
      // Userクラスに "name" があれば、"nickname"にその値を入れる。
      migration.enumerateObjects(ofType: User.className()) { oldObject, newObject in
        if let name = oldObject?["name"] as? String {
          newObject!["nickname"] = name
        }
      }
      
    }

  }
})

Realm.Configuration.defaultConfiguration = config


// これでマイグレーション終了。
// Realm を呼び出しても、エラー落ちしない。
let realm = try! Realm()

oldObject …変更前のオブジェクト。ここからデータを参照する。
newObject …変更後のオブジェクト。変更したい値があれば、この中に入れる。

後からプライマリキーを追加した場合

既に使っていた値をプライマリキーとして登録した場合、
Realmに保存してあるデータが 既に重複していたら、落ちます

error: 'try!' expression unexpectedly raised an error: Error Domain=io.realm Code=1 "Primary key property 'User.id' has duplicate values after migration."

こうなっては仕方ありません。マイグレーション を行う時に、プライマリキーを固有の値に変更しましょう。

マイグレーションでそのデータ自体を削除する方法が見当たらないので、消したい場合は削除用のIDを振って、後に別の処理で削除したほうが良いでしょう。

いい方法があったらオシエテネ

ということでね

本記事では書いていないことが、結構ありますね…
(他の書き方、クエリ検索条件、1対Nの場合… など)

わかりやすさを重視しましたが、いかがだったでしょうか…?
というか、zenn ってこういう使い方で合ってるっけ…
初見の人でも、イメージだけでも掴めていただければ…!!

長々と書きました。
ここまで貴重なお時間を割き、お読み下さいまして誠にありがとうございました!!

Discussion