Realm使うにあたってBaseDaoを書いてみた(idのauto increment, 作成・更新日時の記録対応)

# Realm使うにあたってBaseDaoを書いてみた(idのauto increment, 作成・更新日時の記録対応)
* 対象
* 前置き
* コード
* 使い方

## 対象
以下の要件に合う人向けの話です。

* RealmSwift で Dao 使いたい
* id は auto increment で必要だよね
* 作成日時もあったらいいなー
* 更新日時も必要だよねー
* 追加・更新はまとめたいよねー
* Dao はシングルトンがいいな

## 前置き
この前置きは読み飛ばし推奨。
久しぶりのアプリ開発。sqliteやだー(><)ORMが使いたいー(><)って思って探したらRealmってのが数年前からあったのね。DBを本格的に使うアプリ開発は久しぶりだから(swift時代に入って始めて)知らなかった。
ので、とりあえず使いやすいようにDaoを作ってみた。
モデルと言ったとき、Dao無しでモデルの中にsqlを含むのも多いし、もしかしたらそれが主流かも知れないけど、個人的にはModelとDaoは分けて書きたい。
で、Daoを1から実装してもいいけど結構前からRealmあるし誰か作ってるんじゃないかと思ったらあった。
https://qiita.com/HIkaruSato/items/d6b38a4882babb03f034

ただ、auto incrementに対応してるのは良いけど作成・更新日時に対応していなかったのでそこだけ拡張して使いやすいように修正したのが以下のコード。

## コード
下記のBaseModelsとBaseDaoの組み合わせでできます。

```BaseModels.swift
import RealmSwift

/// 全モデルの共通カラム・処理を記載するベースクラス
class BaseModels: RealmSwift.Object {
    /// ID
    @objc dynamic var id = -1
    /// 作成日
    @objc dynamic var created_at = Date()
    /// 更新日
    @objc dynamic var updated_at = Date()

    /// プライマリーキーの設定
    override static func primaryKey() -> String? {
        return "id"
    }
}
```

```BaseDao.swift
import RealmSwift

/// 全Daoのベース
class BaseDao <T : BaseModels> {
    /// Realmのインスタンスを取得
    let realm: Realm
    
    init() {
        /// Realmのインスタンス
        realm = try! Realm()
        // DB File の場所
        log?.debug("Realm File is : ", Realm.Configuration.defaultConfiguration.fileURL!)
    }
    
    /// 全件取得
    ///
    /// - Returns: 検索結果
    func findAll() -> Results<T> {
        return realm.objects(T.self)
    }
    
    
    /// 1件目のみ取得
    ///
    /// - Returns: 最初の1件取得
    func findFirst() -> T? {
        return findAll().first
    }
    
    /// 指定キーのレコードを取得
    ///
    /// - Parameter key: プライマリキーの値
    /// - Returns: 検索結果
    func findFirst(key: AnyObject) -> T? {
        return realm.object(ofType: T.self, forPrimaryKey: key)
    }
    
    /// 最後のレコードを取得
    ///
    /// - Returns: 最後の1件を取得
    func findLast() -> T? {
        return findAll().last
    }
    
    /// レコードを追加、または更新する。
    ///
    /// - Parameters:
    ///   - d: 更新レコード
    ///   - block: ブロック処理
    /// - Returns: 成功, 失敗 => true, false
    func createOrUpdate(d: T, updateFunc:(_ d: T) -> (T)) -> Bool {
        do {
            try realm.write {
                var updatingD = updateFunc(d)
                updatingD = setDefaultColumnValue(d: updatingD)
                realm.add(updatingD, update: true)
            }
            return true
        } catch let error as NSError {
            log?.debug(error.description)
        }
        return false
    }
    
    
    /// レコード削除
    ///
    /// - Parameter d: 削除レコード
    func delete(d: T) {
        do {
            try realm.write {
                realm.delete(d)
            }
        } catch let error as NSError {
            log?.debug(error.description)
        }
    }
    
    /// レコード全削除
    func deleteAll() {
        let objs = realm.objects(T.self)
        do {
            try realm.write {
                realm.delete(objs)
            }
        } catch let error as NSError {
            log?.debug(error.description)
        }
    }
    
    /// 新規主キー発行
    ///
    /// - Returns: 新id (primaryKey無いときはnil)
    private func newId() -> Int? {
        guard let key = T.primaryKey() else {
            //primaryKey未設定
            return nil
        }
        
        if let last = realm.objects(T.self).last,
            let lastId = last[key] as? Int {
            return lastId + 1
        } else {
            return 1
        }
    }
    
    
    /// デフォルトカラムの値を設定
    ///
    /// - Parameter d: 追加するレコード
    private func setDefaultColumnValue(d: T) -> (T) {
        if realm.isInWriteTransaction {
            if d.id <= 0 {
                d.id = self.newId()!
            }
            d.updated_at = Date()
        } else {
            try! realm.write {
                if d.id <= 0 {
                    d.id = self.newId()!
                }
                d.updated_at = Date()
            }
        }
        return d
    }
}

```

## 使い方
今回はUsersモデルにハッシュカラムだけ持たせた例で説明します。
Usersモデルはアプリのユーザー情報をのせる予定のテーブルなのでレコード数は0 or 1の想定です。

```Users.swift
import RealmSwift

/// ユーザーモデル
/// レコード数は 0 or 1
class Users: BaseModels {
    /// ユーザーハッシュ
    @objc dynamic var user_hash = ""
}
```

```UsersDao.swift
/// ユーザーモデルのDao
class UsersDao: BaseDao<Users> {
    
    // シングルトン化
    static var usersDao: UsersDao = {
        return UsersDao()
    }()
    
    // シングルトン化
    private override init() {
        super.init()
    }
    
    /// 自分のuser情報を取得、存在しない場合はnilを返す
    ///
    /// - Returns: user(自分)
    func findOne() -> Users? {
        // 全Usersの一番最初に登録されたやつを取得(idが一番若いやつ)
        return self.findFirst()
    }
    
    /// Userを追加または更新
    /// (レコード数0 or 1)
    ///
    /// - Parameters:
    ///   - hash: ユーザーhash
    func createOrUpdate(userHash: String?) {
        // 全Usersの一番最初に見つかったやつを取得(無ければ新規作成)
        let user: Users = self.findOne() ?? Users()
        
        /// update処理を記載する無名関数(try realm.write 内で更新しないとエラーになる)
        let updateFunc = {(user: Users) -> Users in
            if let _userHash = userHash {
                // nil で無いとき更新
                user.user_hash = _userHash
            }
            return user
        }
        
        // 更新処理実行
        _ = self.createOrUpdate(d: user, updateFunc: updateFunc)
    }
}

```

こんな感じですね。
モデル、Dao共にBaseを継承します。
Modelは特にないですね。Baseにある以外に必要なカラムを追加してください。
UsersDaoはcreateOrUpdateに注意があります。

```swift
/// update処理を記載する無名関数(try realm.write 内で更新しないとエラーになる)
let updateFunc = {(user: Users) -> Users in
    if let _userHash = userHash {
        // nil で無いとき更新
        user.user_hash = _userHash
    }
    return user
}
        
// 更新処理実行
_ = self.createOrUpdate(d: user, updateFunc: updateFunc)
```

コメントにもありますが try realm.write {} の外でモデルの中身を更新するとエラーになってしまいます。
もちろん更新処理を単純に try realm.write {} で囲っても動くんですが、それだとBaseDaoに createOrUpdate用意した意味が。。。もちろんidとか共通化するっていう意味はありますが。

ってことで、あんまり好きな使い方ではないけども更新処理を func で作って、BaseDaoの中の try realm.write {} で処理します。

Model と Dao ができたのでサービスやコントローラーなど使いたい場所で

```swift
let usersDao = UsersDao.usersDao
usersDao.createOrUpdate(userHash: "hoge")
```

と書けば実行できます。