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

# ActiveRealmとは

[ActiveRealm](https://github.com/HituziANDO/ActiveRealm)とは、[Ruby on RailsのActiveRecord](https://railsguides.jp/active_record_basics.html)にインスパイアされたiOS向けActive Recordライブラリです。RailsのActiveRecordがとても便利なので、このiOS版がほしいと思ったのが開発のキッカケです。名前にも入っている通りDBは[Realm](https://realm.io/docs/objc/latest)に依存しています。

この記事は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インストールしてください。

```ruby
pod "ActiveRealm"
```

## Getting Started

### 1. Realmのセットアップ

最初にRealmを設定します。詳細は[コチラ](https://realm.io/docs/objc/latest/#configuring-a-realm)を参考にしてください。なお、マイグレーションはRealmのお作法の通りです。

```swift
let configuration = RLMRealmConfiguration.default()
// Something to do
RLMRealmConfiguration.setDefault(configuration)
```

### 2. Modelの実装

最初にARMObjectクラスを継承したクラスを実装します。ARMObjectは`uid`, `createdAt`, `updatedAt`の3つのプロパティを持っています。特に`uid`は主キーとして利用されます。ARMObjectの子クラスはDBに保存するデータのみを持つシンプルな構成になります。

ActiveRealmでは命名規則を設けています。この命名規則に従って実装することで、魔術的なコトを裏でやり、手数を減らしています。ARMObjectを継承したクラスのクラス名は必ず`ActiveRealm`プレフィックスを付けてください。

```swift
import ActiveRealm

class ActiveRealmAuthor: ARMObject {
    @objc dynamic var name          = ""
    @objc dynamic var age: NSNumber = 0
}
```

次にARMActiveRealmクラスを継承したクラスを実装します。このクラス名はARMObjectを継承したクラスのクラス名から`ActiveRealm`プレフィックスを取り除いたものにします。また、プロパティも同名・同型とします。

```swift
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します。

```swift
let alice = Author()
alice.name = "Alice"
alice.age = 28
alice.save()
```

#### findOrInitialize(_:)

指定したパラメータで検索し、一致するレコードが見つかった場合は、そのオブジェクトを返します。見つからなかった場合は、指定したパラメータで初期化したオブジェクトを生成します。まだ、この時点ではDBにレコード追加はされません。

```swift
let author = Author.findOrInitialize(["name": "Bob", "age": 55])
author.save()
```

#### findOrCreate(_:)

指定したパラメータで検索し、一致するレコードが見つかった場合は、そのオブジェクトを返します。見つからなかった場合は、指定したパラメータで初期化したオブジェクトを生成します。`findOrInitialize`と違い、新規オブジェクトの生成と同時にレコード追加もします。

```swift
let author = Author.findOrCreate(["name": "Bob", "age": 55])
```

### Read

#### all()

全件取得します。

```swift
let authors = Author.all()
```

#### first()

生成日(`createdAt`)が一番古いオブジェクトを取得します。

```swift
let author = Author.first()
```

#### first(limit:)

生成日が古いものから指定した件数分のオブジェクトを取得します。

```swift
let authors = Author.first(limit: 10)
```

#### last()

生成日が一番新しいオブジェクトを取得します。

```swift
let author = Author.last()
```

#### last(limit:)

生成日が新しいものから指定した件数分のオブジェクトを取得します。

```swift
let authors = Author.last(limit: 10)
```

#### find(ID:)

主キー(`uid`)で検索し、一致したオブジェクトを取得します。

```swift
let author = Author.find(ID: "XXXXXXXX-XXXX-4XXX-XXXX-XXXXXXXXXXXX")
```

#### find(_:)

指定したパラメータで検索し、一致するレコードが見つかった場合は、そのオブジェクトを取得します。複数件見つかった時は生成日が一番古いものを取得します。

```swift
let author = Author.find(["name": "Alice", "age": 28])
```

#### find(with:)

```swift
let author = Author.find(with: NSPredicate(format: "age > %d", 28))
```

#### findLast(_:)

指定したパラメータで検索し、一致するレコードが見つかった場合は、そのオブジェクトを取得します。複数件見つかった時は生成日が一番新しいものを取得します。

```swift
let author = Author.findLast(["name": "Alice", "age": 28])
```

#### findLast(with:)

```swift
let author = Author.findLast(with: NSPredicate(format: "age > %d", 28))
```

### Update

#### save()

```swift
let alice = Author.find(["name": "Alice"])
alice.age = 29
alice.save()
```

### Delete

#### destroy()

`destroy`メソッドはデフォルトでカスケード削除します。これはモデル間でリレーションが定義され、親子関係にあるオブジェクトのとき、親オブジェクトを削除したならば、関連する子オブジェクトも一緒に削除されます。

```swift
alice.destroy()
```

#### destroy(_:)

指定したパラメータで検索し、一致したオブジェクトを全件削除します。

```swift
Author.destroy(["name": "Alice"])
Author.destroy(with: NSPredicate(format: "name=%@", "Alice"))
```

#### destroyAll()

全件削除します。

```swift
Author.destroyAll()
```

もしカスケード削除したくないときは、次のメソッドを使います。

```swift
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

全件取得しコレクションに入れて返します。

```swift
let collection = Author.query.all
```

#### where(_:)

検索条件に一致したオブジェクトをコレクションに入れて返します。

```swift
let collection = Author.query.where(["name": "Alice"])
```

#### where(predicate:)

検索条件に一致したオブジェクトをコレクションに入れて返します。

```swift
let collection = Author.query.where(predicate: NSPredicate(format: "age > %d", 40))
```

### Collection

#### toArray

コレクション内のオブジェクトを`NSArray`に入れて取得します。
 
```swift
let collection = Author.query.all
let authors = collection.toArray
```

#### count

コレクション内のオブジェクトの件数を返します。

```swift
let collection = Author.query.all
let count = collection.count
```

#### first

コレクション内の先頭のオブジェクトを返します。

```swift
let collection = Author.query.all
let author = collection.first
```

#### first(limit:)

コレクション内のオブジェクトを先頭から指定された数だけ取得します。

```swift
let collection = Author.query.all
let authors = collection.first(limit: 5)
```

#### last

コレクション内の末尾のオブジェクトを返します。

```swift
let collection = Author.query.all
let author = collection.last
```

#### last(limit:)

コレクション内のオブジェクトを末尾から指定された数だけ取得します。

```swift
let collection = Author.query.all
let authors = collection.last(limit: 5)
```

#### order(_:ascending:)

コレクション内のオブジェクトを指定されたプロパティで昇順または降順にソートします。

```swift
let collection = Author.query.all
collection.order("age", ascending: true)
```

#### at(_:)

コレクション内の指定されたインデックスのオブジェクトを取得します。範囲外のインデックスを指定したときはnilを返します。

```swift
let collection = Author.query.all
let author = collection.at(1)
```

#### pluck(_:)

コレクション内のオブジェクトから指定したモデルのプロパティのみを抽出し、配列で取得します。

```swift
let collection = Author.query.all
let names = collection.pluck(["name"])
```

## Count

オブジェクトの件数を取得します。条件無しで全件数、条件が有るときは一致した件数を返します。

```swift
// 全件数の取得
let count = Author.count
```

```swift
// 条件に一致した件数
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`タイプをセットします。

```swift
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を付けたものにしなければなりません。

```swift
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`タイプをセットします。

```swift
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関係と同様の命名規則があります。

```swift
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と同じキーを指定することで、その関連オブジェクトを取得できます。

```swift
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なプロパティにしておくと便利です。

```swift
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`メソッドをオーバーライドします。

```swift
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に保存されません。

```swift
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()

```swift
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に変換します。

```swift
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](https://github.com/HituziANDO/ActiveRealm#conversion-methods)をご参照ください。

---
参考:
- Active Recordの基礎 https://railsguides.jp/active_record_basics.html
- Realm https://realm.io/docs/objc/latest