ActiveRealmとは
ActiveRealmとは、Ruby on RailsのActiveRecordにインスパイアされたiOS向けActive Recordライブラリです。RailsのActiveRecordがとても便利なので、このiOS版がほしいと思ったのが開発のキッカケです。名前にも入っている通りDBはRealmに依存しています。
この記事はActiveRealmの基本的な使い方の紹介です。
ActiveRealmの特徴
1. Objective-C製、Swiftでも利用可
ObjCで書かれており、Swiftからも利用できるようにしています。そのため、RealmもObjective-C版を利用しています。
2. Realm DBの利用
前述した通りRealmを利用します。
3. 多彩なREADメソッド
Realm自体もREAD操作は充実していますが、findOrInitialize(_:)
, first(limit:)
, where(_:orderedBy:ascending:limit:)
など、よく使いそうなREADメソッドを用意しています。
4. リレーションを定義していればカスケード削除可
公式のRealmはまだカスケード削除に対応していないようですが、ActiveRealmではモデル間でリレーションを定義していればカスケード削除します。
5. スレッドを跨いでActiveRealmオブジェクトを利用可
基本的にRealmのオブジェクトは生成されたスレッドでしか使えず、スレッドを跨いでオブジェクトを使い回すことができません。Realmを使ったことがある人ならば、この仕様に時々使いにくさを感じたことがあるかもしれません。ActiveRealmはこの使いにくさを回避するつくりになっています。
使い方
インストール
podインストールしてください。
pod "ActiveRealm"
Getting Started
1. Realmのセットアップ
最初にRealmを設定します。詳細はコチラを参考にしてください。なお、マイグレーションはRealmのお作法の通りです。
let configuration = RLMRealmConfiguration.default()
// Something to do
RLMRealmConfiguration.setDefault(configuration)
2. Modelの実装
最初にARMObjectクラスを継承したクラスを実装します。ARMObjectはuid
, createdAt
, updatedAt
の3つのプロパティを持っています。特にuid
は主キーとして利用されます。ARMObjectの子クラスはDBに保存するデータのみを持つシンプルな構成になります。
ActiveRealmでは命名規則を設けています。この命名規則に従って実装することで、魔術的なコトを裏でやり、手数を減らしています。ARMObjectを継承したクラスのクラス名は必ずActiveRealm
プレフィックスを付けてください。
import ActiveRealm
class ActiveRealmAuthor: ARMObject {
@objc dynamic var name = ""
@objc dynamic var age: NSNumber = 0
}
次にARMActiveRealmクラスを継承したクラスを実装します。このクラス名はARMObjectを継承したクラスのクラス名からActiveRealm
プレフィックスを取り除いたものにします。また、プロパティも同名・同型とします。
class Author: ARMActiveRealm {
@objc var name = ""
@objc var age: NSNumber = 0
}
このARMActiveRealmの子クラスにビジネスロジックを書いていく感じです。
なお、今のActiveRealmはint
, float
, double
, BOOL
などのプリミティブ型をサポートしていないため、代わりにNSNumber
を使ってください。この仕様は将来的には解消する考えです。
最小限のセットアップは以上です。
CRUD
Create
save()
save
メソッドは、主キーで検索し、まだDBにレコードが追加されていないときはINSERTし、既に存在するときはUPDATEします。
let alice = Author()
alice.name = "Alice"
alice.age = 28
alice.save()
findOrInitialize(_:)
指定したパラメータで検索し、一致するレコードが見つかった場合は、そのオブジェクトを返します。見つからなかった場合は、指定したパラメータで初期化したオブジェクトを生成します。まだ、この時点ではDBにレコード追加はされません。
let author = Author.findOrInitialize(["name": "Bob", "age": 55])
author.save()
findOrCreate(_:)
指定したパラメータで検索し、一致するレコードが見つかった場合は、そのオブジェクトを返します。見つからなかった場合は、指定したパラメータで初期化したオブジェクトを生成します。findOrInitialize
と違い、新規オブジェクトの生成と同時にレコード追加もします。
let author = Author.findOrCreate(["name": "Bob", "age": 55])
Read
all()
全件取得します。
let authors = Author.all()
first()
生成日(createdAt
)が一番古いオブジェクトを取得します。
let author = Author.first()
first(limit:)
生成日が古いものから指定した件数分のオブジェクトを取得します。
let authors = Author.first(limit: 10)
last()
生成日が一番新しいオブジェクトを取得します。
let author = Author.last()
last(limit:)
生成日が新しいものから指定した件数分のオブジェクトを取得します。
let authors = Author.last(limit: 10)
find(ID:)
主キー(uid
)で検索し、一致したオブジェクトを取得します。
let author = Author.find(ID: "XXXXXXXX-XXXX-4XXX-XXXX-XXXXXXXXXXXX")
find(_:)
指定したパラメータで検索し、一致するレコードが見つかった場合は、そのオブジェクトを取得します。複数件見つかった時は生成日が一番古いものを取得します。
let author = Author.find(["name": "Alice", "age": 28])
find(with:)
let author = Author.find(with: NSPredicate(format: "age > %d", 28))
findLast(_:)
指定したパラメータで検索し、一致するレコードが見つかった場合は、そのオブジェクトを取得します。複数件見つかった時は生成日が一番新しいものを取得します。
let author = Author.findLast(["name": "Alice", "age": 28])
findLast(with:)
let author = Author.findLast(with: NSPredicate(format: "age > %d", 28))
Update
save()
let alice = Author.find(["name": "Alice"])
alice.age = 29
alice.save()
Delete
destroy()
destroy
メソッドはデフォルトでカスケード削除します。これはモデル間でリレーションが定義され、親子関係にあるオブジェクトのとき、親オブジェクトを削除したならば、関連する子オブジェクトも一緒に削除されます。
alice.destroy()
destroy(_:)
指定したパラメータで検索し、一致したオブジェクトを全件削除します。
Author.destroy(["name": "Alice"])
Author.destroy(with: NSPredicate(format: "name=%@", "Alice"))
destroyAll()
全件削除します。
Author.destroyAll()
もしカスケード削除したくないときは、次のメソッドを使います。
alice.destroy(cascade: false)
Author.destroy(["name": "Alice"], cascade: false)
Author.destroy(with: NSPredicate(format: "name=%@", "Alice"), cascade: false)
Author.destroy(cascade: false)
Query
all
全件取得しコレクションに入れて返します。
let collection = Author.query.all
where(_:)
検索条件に一致したオブジェクトをコレクションに入れて返します。
let collection = Author.query.where(["name": "Alice"])
where(predicate:)
検索条件に一致したオブジェクトをコレクションに入れて返します。
let collection = Author.query.where(predicate: NSPredicate(format: "age > %d", 40))
Collection
toArray
コレクション内のオブジェクトをNSArray
に入れて取得します。
let collection = Author.query.all
let authors = collection.toArray
count
コレクション内のオブジェクトの件数を返します。
let collection = Author.query.all
let count = collection.count
first
コレクション内の先頭のオブジェクトを返します。
let collection = Author.query.all
let author = collection.first
first(limit:)
コレクション内のオブジェクトを先頭から指定された数だけ取得します。
let collection = Author.query.all
let authors = collection.first(limit: 5)
last
コレクション内の末尾のオブジェクトを返します。
let collection = Author.query.all
let author = collection.last
last(limit:)
コレクション内のオブジェクトを末尾から指定された数だけ取得します。
let collection = Author.query.all
let authors = collection.last(limit: 5)
order(_:ascending:)
コレクション内のオブジェクトを指定されたプロパティで昇順または降順にソートします。
let collection = Author.query.all
collection.order("age", ascending: true)
at(_:)
コレクション内の指定されたインデックスのオブジェクトを取得します。範囲外のインデックスを指定したときはnilを返します。
let collection = Author.query.all
let author = collection.at(1)
pluck(_:)
コレクション内のオブジェクトから指定したモデルのプロパティのみを抽出し、配列で取得します。
let collection = Author.query.all
let names = collection.pluck(["name"])
Count
オブジェクトの件数を取得します。条件無しで全件数、条件が有るときは一致した件数を返します。
// 全件数の取得
let count = Author.count
// 条件に一致した件数
let count1 = Author.query.where(["name": "Alice"]).count
let count2 = Author.query.where(predicate: NSPredicate(format: "age > %d", 40)).count
リレーション
モデル間でリレーションを定義することができます。リレーションはdefinedRelationships
メソッドをオーバーライドすることで定義します。
1対1関係
例として、1つのAuthorオブジェクトは1つのUserSettingsオブジェクトを持つとします。
最初に親であるAuthorクラスにおいて、ARMRelationshipオブジェクトに関連する子のクラスと.hasOne
タイプをセットします。
class ActiveRealmAuthor: ARMObject {
@objc dynamic var name = ""
@objc dynamic var age: NSNumber = 0
}
// Parent
class Author: ARMActiveRealm {
@objc var name = ""
@objc var age: NSNumber = 0
override class func definedRelationships() -> [String: ARMRelationship] {
return ["userSettings": ARMRelationship(with: UserSettings.self, type: .hasOne)]
}
}
次に子であるUserSettingsクラスにおいて、ARMInverseRelationshipオブジェクトに関連する親のクラスと.belongsTo
タイプをセットします。
また、外部キーとしてauthorID
プロパティを実装します。この外部キー名にも命名規則があり、親のクラス名の先頭1文字を小文字にし、末尾にIDを付けたものにしなければなりません。
class ActiveRealmUserSettings: ARMObject {
@objc dynamic var authorID = ""
@objc dynamic var notificationEnabled: NSNumber = false
}
// Child
class UserSettings: ARMActiveRealm {
@objc var authorID = ""
@objc var notificationEnabled: NSNumber = false
override class func definedRelationships() -> [String: ARMRelationship] {
return ["author": ARMInverseRelationship(with: Author.self, type: .belongsTo)]
}
}
1対多関係
例として、1つのArticleオブジェクトは複数のTagオブジェクトを持つとします。
最初に親であるArticleクラスにおいて、ARMRelationshipオブジェクトに関連する子のクラスと.hasMany
タイプをセットします。
class ActiveRealmArticle: ARMObject {
@objc dynamic var title = ""
@objc dynamic var text = ""
}
class Article: ARMActiveRealm {
@objc var title = ""
@objc var text = ""
override class func definedRelationships() -> [String: ARMRelationship] {
return ["tags": ARMRelationship(with: Tag.self, type: .hasMany)]
}
}
次に子であるTagクラスにおいて、ARMInverseRelationshipオブジェクトに関連する親のクラスと.belongsTo
タイプをセットします。
また、外部キーとしてarticleID
プロパティを実装します。この外部キー名も1対1関係と同様の命名規則があります。
class ActiveRealmTag: ARMObject {
@objc dynamic var articleID = ""
@objc dynamic var name = ""
}
class Tag: ARMActiveRealm {
@objc var articleID = ""
@objc var name = ""
override class func definedRelationships() -> [String: ARMRelationship] {
return ["article": ARMInverseRelationship(with: Article.self, type: .belongsTo)]
}
}
関連オブジェクトへのアクセス
関連オブジェクトへはrelations
プロパティを介してアクセスできます。relations
プロパティはDictionaryであり、definedRelationships
で返したDictionaryと同じキーを指定することで、その関連オブジェクトを取得できます。
let alice = Author.findOrCreate(["name": "Alice", "age": 28])
UserSettings.findOrCreate(["authorID": alice.uid, "notificationEnabled": true])
// One-to-One
// Use relations[key].object
if let settings = alice.relations["userSettings"]?.object as? UserSettings {
// Something to do.
}
let article = Article.findOrCreate(["title": "ActiveRealm User Guide",
"text": "ActiveRealm is a library for iOS."])
Tag.findOrCreate(["articleID": article.uid, "name": "Programming"])
Tag.findOrCreate(["articleID": article.uid, "name": "iOS"])
// One-to-Many
// Use relations[key].objects
if let tags = article.relations["tags"]?.objects as? [Tag] {
// Something to do.
}
// Inverse relationship
let tag = Tag.find(["articleID": article.uid])
if let article = tag.relations["article"]?.object as? Article {
// Something to do.
}
何度もrelations[key].object(s)を書くのは面倒なため、以下のようにread-onlyなプロパティにしておくと便利です。
class Author: ARMActiveRealm {
...
// The relation property. This property is just alias.
var userSettings: UserSettings? {
guard let userSettings = relations["userSettings"]?.object as? UserSettings else { return nil }
return userSettings
}
override class func definedRelationships() -> [String: ARMRelationship] {
return ["userSettings": ARMRelationship(with: UserSettings.self, type: .hasOne)]
}
class UserSettings: ARMActiveRealm {
...
// The relation property. This property is just alias.
var author: Author {
return relations["author"]?.object as! Author
}
override class func definedRelationships() -> [String: ARMRelationship] {
return ["author": ARMInverseRelationship(with: Author.self, type: .belongsTo)]
}
}
DBに保存したくないプロパティがあるとき
基本的にActiveRealmはモデルに定義したプロパティをすべて保存対象とします。もしDBに保存したくないプロパティがあるときは、ARMActiveRealmクラスのignoredProperties
メソッドをオーバーライドします。
class Author: ARMActiveRealm {
...
// A property ignored by ActiveRealm.
@objc var shortID: String {
return String(uid.split(separator: "-").first!)
}
override class func ignoredProperties() -> [String] {
return ["shortID"]
}
}
バリデーション
ActiveRealmはデータをDBに保存する前にバリデーションをすることができます。バリデーションを有効にしたい場合は、ARMActiveRealmクラスのvalidateBeforeSaving
メソッドをオーバーライドします。このメソッドがfalseを返した場合、データはDBに保存されません。
class Author: ARMActiveRealm {
@objc var name = ""
@objc var age: NSNumber = 0
override class func validateBeforeSaving(_ obj: Any) -> Bool {
let author = obj as! Author
// The name must not be empty.
return !author.name.isEmpty
}
}
変換メソッド
ActiveRealmにはモデルをDictionaryとJSONに変換するメソッドが備わっています。
asDictionary()
class Author: ARMActiveRealm {
@objc var name = ""
@objc var age: NSNumber = 0
}
let chris = Author.findOrCreate(["name": "Chris", "age": 32])
// Convert to a dictionary.
let dict = chris.asDictionary()
// => {
// age = 32;
// createdAt = "2019-06-07 07:45:05 +0000";
// name = Chris;
// uid = "D56F60E1-96C1-4083-A7D4-E216FF072DEA";
// updatedAt = "2019-06-07 07:45:05 +0000";
// }
asJSON(), asJSONString()
asJSON
メソッドはData
型のJSONに変換する一方で、asJSONString
メソッドはString
型のJSONに変換します。
class Author: ARMActiveRealm {
@objc var name = ""
@objc var age: NSNumber = 0
}
let chris = Author.findOrCreate(["name": "Chris", "age": 32])
// Convert to a JSON.
let json = chris.asJSONString()
// => {
// "age" : 32,
// "uid" : "66703A0B-5712-4631-83C4-DF52E1CCE15F",
// "updatedAt" : "2019-06-07 09:15:15 +0000",
// "name" : "Chris",
// "createdAt" : "2019-06-07 09:15:15 +0000"
// }
ここで紹介した以外にも、指定プロパティのみをDictionary/JSONに含めたり、指定プロパティを除外したりするオプションがあります。詳細はREADME#Conversion Methodsをご参照ください。
参考:
- Active Recordの基礎 https://railsguides.jp/active_record_basics.html
- Realm https://realm.io/docs/objc/latest