
次世代モバイル向けデータベースRealmを使ってみる 第2回
-RealmSwiftを使ってみた-
前回はRealm Tech Talkの様子をレポートした。今回はインタフェースが刷新されたSwift版Realmを実際に使用し、その実力を体験していただこうと思う。
繁田卓二
■ Realm Swift
Realmは現在iOSとAndroidをサポートしており、Objective-C, Swift, Javaの3つの言語向けにインタフェースが提供されている。今回はリリースされて間もないRealm Swiftにフォーカスし解説したいと思う。 尚、インストールに関しては公式サイトのドキュメントに詳しく書かれているためここでは省略する。Framework形式のバイナリをダウンロードしプロジェクトに追加するか、CocoaPodsやCarthageなどのパッケージ管理ツールを使用してプロジェクトにRealmをインポートしておこう。
・Realm公式ドキュメント
https://realm.io/jp/docs/objc/latest/
■ 一般的なデータベースでは
Realmの解説の前に、一般的なリレーショナルデータベースを使用してアプリを構築する手順を考えてみよう。モバイルに限らず多くのデータベース製品ではDDL(Data Definition Language)とよばれる言語で、データの器となるテーブル群とフィールド情報、制約、リレーションなどを定義しなければならない。CREATE DATABASE文やCREATE TABLE文などがそうである。これらのDDLで事前にデータベースとテーブルを作成しておき、アプリケーションからSELECT文やINSERT文などのDML(Data Manipulation Language)を使用しデータをやりとりすることになる。
多くの開発環境ではデータベースやテーブルの作成は専用のGUIツールで設計ができたり、またO/Rマッパーなどのライブラリを使用することでDMLを書かなくてもデータ操作が行えるようになる。Xcodeでも古くからCoreDataというモデルレイヤーのフレームワークが標準でサポートされており、内蔵するエンティティエディタではテーブルとなるエンティティとフィールドをグラフィカルにデザインができ、また、エディタで作成したエンティティはモデルクラスファイルとして書き出すことができる。
図 Xcodeエンティティエディタ
Realmにはこういったエディタは存在しない。なぜなら直接モデルクラスを定義すれば良いからだ。モデルクラスの定義がテーブルの定義となり、プロパティ定義がカラム定義そのものとなる。モデルクラスを定義しておけばRealmインスタンスの初回生成時に自動でデータベースが初期化されるので、テーブルの作成や、クラスとのマッピングなど面倒な準備作業は一切不要というわけだ。CoreDataだとエディタ上のエンティティ設計とモデルクラスの実装に齟齬が起きたりすることがしばしば起こり得るがRealmではそういった心配も無い。
■ モデルの定義
それではRealm Swiftでモデルクラスを定義してみよう。モデルクラスはRealm Swiftが提供するObjectクラスを継承し、カラムとして定義したい内容をプロパティ宣言するだけで良い。今回は、別のモデルとの関連を持ったモデルクラスを設計してみよう。アルバムと写真という二つのテーブルを用意し、アルバムオブジェクトの中に複数の写真オブジェクトが入っている、いわゆる、One-To-Many(一対多)の関連だ。 これをRealm Swiftのモデルクラスとして定義するとこうなる。
import RealmSwift
// アルバムモデルクラス
class Album: Object {
dynamic var name = ""
dynamic var createdDate: NSDate?
let photos = List<Photo>()
}
// 写真モデルクラス
class Photo: Object {
dynamic var name = ""
dynamic var createdDate: NSDate?
var album: Album {
return linkingObjects(Album.self, forProperty: "photos").first!
}
}
・SwiftでのNSArrayとUIViewの記述
// 配列の生成
let array = NSArray()
// ソート
let newArray = array.sortedArrayUsingSelector("compare:")
// ビューの作成
let view = UIView(frame: CGRectMake(0, 0, 100, 100))
self.view.addSubview(view)
カラムはdynamic varとしてプロパティ宣言するだけだ。デフォルト値も設定できる。リレーションの親となるモデルにはphotosプロパティが定義されており、PhotoオブジェクトがList形式で格納されることになる。反対に子となるモデルには親のAlbumクラスを参照できるようlinkingObjects()メソッドで関連した親モデルインスタンスを返すように記述する。これで相互のリレーションが実現できる。
このあたりはRealmに限らずCoreDataやRailsなどアクティブ・レコードパターンでお馴染みの実装である。オブジェクト志向とリレーショナルデータベースの相性の良さを改めて体験できる内容であろう。
■ データ操作
次にデータの取得と更新の操作を見てみよう。まず共通のコードとしてRealmクラスのインスタンス取得処理を記述する必要がある。これはどのデータベース対して処理をおこなうかを決定する目的もあり、また、Realmクラス自体が各種データへの操作メソッドを持っているためインスタンスの生成は必須となる。
Realmクラスは引数でデータベースファイル名を渡すことにより個別にデータベースを分けることが可能だが、省略時にはデフォルトのファイル名が使用される。特に明示的に分ける必要が無い場合は引数なしで呼びだそう。 また、引数に inMemoryIdentifier を指定することによりより高速なインメモリデータベースとしての動作も可能だ。ただこの場合、当然ながらデータの永続化は行われずアプリのプロセスが終了したタイミングでデータも破棄される事に注意しよう。
// デフォルトのデータベースインスタンスを取得
let realm1 = Realm()
// 特定ファイルのデータベースインスタンスを取得
let realm2 = Realm(path: "another.realm")
// インメモリデータベースインスタンスを取得
let realm3 = Realm(inMemoryIdentifier:"メモリ空間名")
■ 参照
ではこのRealmインスタンスを使用しテーブルからレコードを取り出してみよう。データの取得はRealmインスタンスが持つobjects()メソッドを使用すればよい。引数にモデルクラスの型を渡すことで、そのテーブルが持つ全てのモデルが取得できる。
// アルバムテーブルに入っているAlbumオブジェクトを全て取り出す。
let albums = realm.objects(Album)
検索条件を指定したい場合は、filter()メソッドを使用し絞り込む。
ここでの検索条件はNSPredicateクラスで使用する述語がそのまま使用できる。
let albums = realm.objects(Album).filter("name contains '猫'")
取得したデータはCollectionTypeプロトコルに準拠したResultsクラスにラッピングされており配列同様にループで1件1件取り出すことが可能だ。取得したレコード全てが変数に展開されるわけではなくデータにアクセスした際に必要なレコードのみ展開されるため非常にメモリに優しい設計となっている。
// ループで1件ずつ取り出す
for album in albums {
println("\(album.name)")
}
■ 追加
次はデータの追加だ。一般的なリレーショナルデータベースにあるトランザクションの概念と同様にRealmでもレコードの追加や更新は明示的にトランザクションの開始、終了を呼び出す必要がある。更新処理は必ずそのトランザクション内で記述しなければならない。 レコードを新規追加するには対象のモデルクラスのインスタンスを生成し、プロパティをセットして add() メソッドにモデルオブジェクトを渡そう。
// Realmインスタンスの取得
let realm = Realm()
// トランザクションの開始
realm.beginWrite()
// データを作成
let album = Album()
// カラムをセット
album.name = "猫アルバム"
album.createdDate = NSDate()
// レコードを追加
realm.add(album)
// トランザクションの終了
realm.commitWrite()
また、write()メソッドに処理を記述したクロージャーを渡すことでも内部的にトランザクションの開始と終了が行われる。下記は前述のbeginWrite()~commitWrite()での処理と同等となる。
let realm = Realm()
// トランザクション
realm.write {
let album = Album()
album.name = "猫アルバム"
album.createdDate = NSDate()
realm.add(album)
■ 更新/削除
更新は取得したオブジェクトにプロパティを変更するだけだ。オブジェクトの生成と同じくトランザクション内での記述が必須となる。下記はnameが「猫アルバム」であるオブジェクトを1件取得し「猫アルバム2015」としてリネームする処理だ。
// フィルターで取得
let albums = realm.objects(Album).filter("name = '猫アルバム'")
// 1件取り出す
if let anAlbum = albums.first {
realm.write {
// nameを変更
anAlbum.name = "猫アルバム 2015"
}
}
オブジェクトを削除する際はrealm.delete()メソッドにオブジェクトを渡す。この時もトランザクション内での記述が必要となる。
// フィルターで取得
let albums = realm.objects(Album).filter("name = '猫アルバム'")
// 1件取り出す
if let anAlbum = albums.first {
realm.write {
// 削除
realm.delete(anAlbum);
}
}
■ 関連オブジェクトの追加
アルバムの中に関連した写真を追加する場合は、リレーション対象のプロパティがList形式であるため、直接append()メソッドで対象のオブジェクトを追加すればよい。
let albums = realm.objects(Album).filter("name = '猫アルバム'")
if let anAlbum = albums.first {
rea lm.write {
// 写真オブジェクトを生成
let photo = Photo()
photo.name = "はな"
photo.createdDate = NSDate()
// アルバムオブジェクトに追加
anAlbum.photos.append(photo)
}
}
■ まとめ
Realmは当初Objective-CとJavaのみで提供されており、Swiftから使用するにはブリッジングヘッダを使用し、Objective-Cの冗長なメソッド名で使用する必要があった。Realm Swiftではこれらが改善されており、より簡潔に、より直感的にデータベース処理が書けるようになっている。 Realmの基本仕様自体はアクティブ・レコードに慣れた開発者であればつまずくところは無いだろう。今回注目して頂きたかったのは導入のための準備がSQLiteやCoreDataに比べて格段に少ないことだ。さらに、パフォーマンスにおいても圧倒的に優れているとなると、もはやRealmを使わない手は無いだろう。今後さらなる機能追加も予定されているので是非この機会に体験していただきたい。
qnote最高技術責任者。猫とビールをこよなく愛するゆるキャラ系プログラマ。
Webアプリ開発からMacアプリ開発を経て現在はiOSアプリ開発に没頭。弱点はAndroid。