Help us understand the problem. What is going on with this article?

iOS向けActiveRecordライブラリ: ActiveRealmの使い方

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした