Realmを使ってデータ管理【Swift編】-その3-


[Realmを使ってデータ管理【Swift編】-その1-](http://qiita.com/jtemplej/items/1c1b7204341ac01e561e)
[Realmを使ってデータ管理【Swift編】-その2-](http://qiita.com/jtemplej/items/0443571fdbffbcea3e7f)

その1ではデータの追加/更新/削除等の基本的な項目について
その2ではRelationやInmemory,Backgroundについて説明しました。

その3ではMigrationとNotificationについて説明します。
引き続き前々回、前回に定義したBookクラスを使用します。

## Migration

### Migration時に何もしないケース.
賢いRealmさん、Migrationもいい感じにやってくれると思いきや
これがくせ者で、手動でいい感じに管理しないと駄目だったりします。
そう、容赦なくExceptionを投げるんですよ、Realmならね。

ではBookクラスを例に説明していきます。
Bookクラスで既にオブジェクトを保存した後に、Bookクラスに新しいカラムを追加しましょう。

```swift:Book.swift
import Foundation

import Realm

// V2
class Book : RLMObject {
	dynamic var isbn = ""
	dynamic var name = ""
	dynamic var price = 0
	dynamic var publisher = Publisher()
	// thumbnailを表示したいのでimageURLを追加
	dynamic var imgURL = ""

    class func find(isbn:String) -> Book? {
        let result = Book.objectsWithPredicate(NSPredicate(format: "isbn = %@", isbn))
        if let books = result {
            return books.firstObject() as? Book
        }
        return nil
    }
    
    override class func primaryKey() -> String {
        return "isbn"
    }
}
```

変更した後にオブジェクトを追加しようとすると即Exceptionを投げ返してきます。

```swift
// この時点ですごい勢いで投げてくる.
let realm = RLMRealm.defaultRealm()
```

エラーメッセージを見てみると
Terminating app due to uncaught exception 'RLMException', reason: 'Column count does not match interface - migration required'
migrationしてよと言われます。

そう、Modelの追加や変更した場合には必ずMigraitonが必要になるんです。
では実際にMigrationをしてみましょう。

```swift
// Migrationの内容.
// returnで新しいSchemaVersionを返す.
// まだ一度もMigrationを実行していない場合はoldSchemaVersionには0が入り、
// 一度でも実行している場合はそのときのschemaVersionが入ります.
// 今回は何もしないので新しいSchemaVersionのみ返してます.
let migrationBlock: RLMMigrationBlock = { migration, oldSchemaVersion in
	return 1
}
// Migrationを実行.
RLMRealm.migrateDefaultRealmWithBlock(migrationBlock)

// Migration実行後に呼び出す
// そうしないとExceptionを投げ返される...
let realm = RLMRealm.defaultRealm()
        
let book = Book()
book.isbn = "999992"
book.name = "realm migration tutorial"
book.price = 2000
book.imgURL = "http://hogehoge/sample.jpg"
realm.beginWriteTransaction()
realm.addOrUpdateObject(book)
realm.commitWriteTransaction()
        
let book_v2 = Book.objectsWhere("isbn = '999992'")
for result in book_v2 {
	println("book isbn:\((result as Book).isbn)")       // book isbn:999992
	println("book name:\((result as Book).name)")       // book name:realm migration tutorial
	println("book imageURL:\((result as Book).imgURL)") // book imageURL:http://hogehoge/sample.jpg
}

```

これで同期が取れたのでBookオブジェクトが使えるようになりました。

### Migration時に特定の処理を行う.
Migration時に特定のオブジェクトに対して処理を行うことも出来ます。
次はBookオブジェクトにdebugTextというカラムを追加して
Migration時にisbn, name, price, imagURLの値を半角スペースでつなげた値を入れるようにしてみましょう。
debugTextというネーミングセンスのなさは無視してください。


```swift:Book.swift
import Foundation

import Realm

// V3
class Book : RLMObject {
	dynamic var isbn = ""
	dynamic var name = ""
	dynamic var price = 0
	dynamic var publisher = Publisher()
	dynamic var imgURL = ""

	// デバッグ用.
	dynamic var debugText = ""

    class func find(isbn:String) -> Book? {
        let result = Book.objectsWithPredicate(NSPredicate(format: "isbn = %@", isbn))
        if let books = result {
            return books.firstObject() as? Book
        }
        return nil
    }
    
    override class func primaryKey() -> String {
        return "isbn"
    }
}
```

```swift
// Migrationの内容
// 先ほどはschemaVersionに1を返したので今回は異なる値を返す.
let migrationBlock: RLMMigrationBlock = { migration, oldSchemaVersion in
	if oldSchemaVersion < 2 {
		// 処理を行いたいクラス名を渡すと、oldObjectとnewObjectに値が入る.
		migration.enumerateObjects(Book.className()) { oldObject, newObject in
			let bookV3 = newObject as Book
			let isbn = oldObject["isbn"] as String
			let name = oldObject["name"] as String
			let price = oldObject["price"] as Int
			let imgURL = oldObject["imgURL"] as String
			// ここで新しく定義したdebugTextに値を格納.
			// 既存のオブジェクトに対して更新が走る.
			bookV3.debugText = "\(isbn) \(name) \(price) \(imgURL)"
		}
	}
	return 2
}
// Migrationを実行.
RLMRealm.migrateDefaultRealmWithBlock(migrationBlock)
        
let realm = RLMRealm.defaultRealm()
let book_v3 = Book.objectsWhere("isbn = '999992'")

// debugTextに先ほどの値が入っていることを確認.
for result in book_v3 {
	// book debugText:999992 realm migration tutorial 2000 http://hogehoge/sample.jpg
	println("book debugText:\((result as Book).debugText)") 
}
```

このように渡されたmigration.enumerateObjectsに処理したいクラス名を渡すと
既存のオブジェクトが帰ってくるので様々な処理をすることが可能です。

## Notification
オブジェクトに変更があった際に通知を受け取ることが出来ます。
色々調べましたがどうも変更があったことを受け取れるだけで
何が追加/更新/削除されたかは受け取れません。。。おしい！

```swift
import UIKit

import Realm

class ViewController: UIViewController {
    private var notificationToken : RLMNotificationToken?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

	// ボタンが押されたら実行.
    @IBAction func notificationTest(sender:AnyObject) {
        self.notificationToken = RLMRealm.defaultRealm().addNotificationBlock{ note, realm in
            // 追加/更新/削除が終わった後に呼び出される.
            // ただし、何が追加されたかは解らない....
            // noteにはnotificatoinNameが入る
            // 下記ケースだとRLMRealmDidChangeNotificationが入ってくる.
            println(note); // RLMRealmDidChangeNotification
        }
        let realm = RLMRealm.defaultRealm()
        let book = Book()
        book.isbn = "999991"
        book.name = "realm notification sample"
        book.price = 1000
        
        realm.beginWriteTransaction()
        realm.addOrUpdateObject(book)
        realm.commitWriteTransaction() // 実行後に1回目のnotificationが呼び出される.
        
        realm.beginWriteTransaction()
        realm.deleteObject(book)
        realm.commitWriteTransaction() // 実行後に2回目のnotificationが呼び出される.
    }
}
```

## 色々触ってみた感想
まだv1.0になっていないため、色々不憫なところはありますが
大体の機能はそろっているので普通に使えます。
作ってくれた方々、本当にありがとう！

長々とその3まで続きましたが、今回の記事が
少しでもSwift開発者の手助けになれると光栄です。