SPIDERPLUS Tech Blog

建設SaaS「スパイダープラス」のエンジニアとデザイナーのブログ

Realmを使った開発の長所・工夫のいるところを考えてみた

こんにちは。
スパイダープラスでiOSエンジニアとして働いているJです。モバイルアプリを開発している方にはなじみのある名前だと思いますが、iOSAndroid両方開発する時に使えるモバイルDBである「Realm」について、今回はお話したいと思います。

※なお、文中でスパイダープラスとカタカナの場合は会社のこと、SPIDERPLUSと英大文字の場合はサービスのことを指します。

 

 

1. Realmの概要

2010年度後半にAlexander Stigsen、Bjarne Christiansenという二人がTightDBというプロジェクト名でスタートして、2014年にRealmと名前を変更して現在まで使われています。
オープンソースデータベース管理システム(DBMS)として、特にモバイル環境を主なターゲットとしたデータベースです。

サポートしている言語は以下の通りです。

Realmの特徴

モバイルDBとして広く使われているSQLiteや、iOSでのオブジェクト管理フレームワークのCoreDataより作業速度が速いことが長所!従来の定型化されたデータベースとは異なり、NoSQLデータベースを目指し、データモデル構造自体がオブジェクトコンテナで構成されています。

引用: Realm Academy(https://academy.realm.io/kr/posts/realm-object-centric-present-day-database-mobile-applications/)

Realmで作業をする時、Realmに保存した内容を確認する際はRealm StudioやRealm Browserからデータを見ることができます!
ここまでお話したRealm StudioとRealm Browserについて、簡単におさらいすると

 

[引用: MongoDB(https://www.mongodb.com/docs/realm/studio/)]

[引用: App Store (https://apps.apple.com/us/app/realm-browser/id1007457278?mt=12)]

スパイダープラスでは「Realm Studio」を使っています。「Realm Studio」を使っている理由は次の「SPIDERPLUS開発にRealmを選択した理由」で続けてまいります!

2. スパイダープラスでの利用とその背景

※【再掲】文中でスパイダープラスとカタカナの場合は会社のこと、SPIDERPLUSと英大文字の場合はサービスのことを指します。

SPIDERPLUS開発での Realm

SPIDERPLUSのアプリは、基本的にユーザーが実際に使っているデータを、サーバーに保存して同期しています。

サーバーにデータを保存するため、そしてデータをアプリからサーバーに送るために、アプリでは Realm にデータを保存して管理します。

ではどのように Realm を管理しているのかを簡単に見てみます

下記のように Realm を管理するクラスを追加してデータの保存か、削除に必要なところからインスタンスを呼び出して使っています。

 

final class RealmManager {
    /// 共有インスタンス.
    static let shared = RealmManager()
    /// カラムの追加や削除修正後に再計算で自動変化するためのバージョン.
    static let schemaVersion: UInt64 = RealmSchemaVersion.current
    /// Realmのインスタンス.
    var realm: Realm
    /// Realmの生成
    static func realm() -> Realm {
        do {
            return try Realm(configuration: .auto)
        } catch {
            printError("error: \(error)")
        }
    }
    /// realmにデータを入れるためのトランザクション.
    static func write(action: (_ realm: Realm) -> Void) {
        let realm = realm()
        if realm.isInWriteTransaction {
            action(realm)
        } else {
            do {
                try realm.write {
                    action(realm)
                }
            } catch {
                printError("write error:", error)
            }
        }
    }
    /// 全てのオブジェクトを削除.
    func deleteAll() {
        try? realm.write {
            realm.deleteAll()
        }
    }
}

サーバーからのデータを保存する、サーバーに保存するためのモデルは下記のような形で追加して使ってます

上のような形式でローカルにデータを保存し、保存したデータをサーバーとやりとりをしながら管理をしています。

SPIDERPLUS開発に Realm を選択した理由

スパイダープラスでは、なぜ Realm を使っているのか、SQLite のように他のDBもある中で、なぜRealm を選択したのか。その理由を3つお話します。

理由の第1番目は性能の良さです。最近の iOS ではもっともよく使えるモバイル用の DB と思います。

第2番目はオフラインの状態でもアプリが利用されることです。
SPIDERPLUSは色々な建設現場で使われます。高層階や地下階、まだ住所もないような場所など、通信環境が良くない現場でもサービスを利用されることがあり、ユーザーのローカル環境にデータを保存・管理することを可能にするためにRealm を利用してます。
第3番目はライブラリに関するドキュメントが多いことです。開発する時の参考にできる資料を探しやすいのです。
もっといいサービスを提供するためには、より良いコードで開発が必要ですが、ライブラリに関する資料がないと、不具合が起きた時などにスムーズな対応が難しくなってしまいがちです。Realm は世界中で多くの開発者が利用しているため、ライブラリ開発に関するドキュメントが多いのが長所です。

 

3. 実装で注意・工夫したところ、苦労したところ

Realmの良いところ

直接 Realm を用いて開発する時に「いいな」と感じたことの中から3つ紹介します。

1. リアルタイムで保存ができる機能がある
   データの保存、更新、削除をする場合などに、画面のデータを変更しますが、すぐビューに反映したいケースがありますがあります。
   リアルタイムの機能を実装するには、ビューのライフサイクルを一回やり直さないとすぐ反映することができない場合もありますが、Realm では簡単にデータの変更と表示ができます。

2. embedded 形で保存→ Json の形で保存ができる
   一つのクラスで従属関係のクラスを追加する時は Object 系ではなく embedded 系で管理すれば従属関係として管理をもっと簡単にできます。
下記のようにコードをクラスを作成すれば画像のようにデータが保存されます。

class Person: Object {
    @Persisted var id: Int
    @Persisted var name: String
    @Persisted var sample: RealmSwift.List<SampleModel>
} class SampleModel: EmbeddedObject { @Persisted var id: Int @Persisted var SampleListContent: String }

3.  簡単なモデル関係での 1:1, 1:N は処理も簡単

簡単な 1:1 の関係は、 1 つの モデルに対して 1 つの Entity を持たせることができますが、1:N の関係の場合は、モデル に List という形で紐付けばできます。
SwiftUI では下記の例のように「LinkingObjects」を利用すれば簡単に構築が可能です。

*LinkingObjectsの例(LinkingObjectsのoriginPropertyが外来キーの役割をします。)

import Foundation
import RealmSwift

class Country: Object, ObjectKeyIdentifiable {
	@Persisted(primaryKey: true) var id: ObjectId
	@Persisted var name: String
	@Persisted var city: List

	convenience init(name: String) {
		self.init()
		self.name = name
	}
}
import Foundation
import RealmSwift

class City: Object, ObjectKeyIdentifiable {
	@Persisted(primaryKey: true) var id: ObjectId
	@Persisted var name: String
	@Persisted(originProperty: "city") var country: LinkingObjects
	
	convenience init {
		name: String
	} {
		self.init()
		self. name = name
	}
}

Realmの不便なところ

どんなものでもいいところもあれば、不便なところもあります。Realm にもまた、不便なところがあると感じました。実際に使って感じた不便なところと、その際に工夫していることや、気をつけていることについて3つ紹介します。

1. Realm Studio でデータを確認するやり方が不便

普段 Realm に保存されているデータを確認するための GUI として Realm Studio または Realm Browser を使っています。
Realmは、NoSQL データベースを目指している DB なので Mysql のようにクエリ文を利用してデータを取り出すのが難しいです。
Realm Studio からデータをフィルターするような感じにデータを取り出すことは可能です。
下の画像のようにデータがある時に、id が「24」のデータをフィルターしたいのであれば、画面の上部にあるテキストボックスに「id = 24」と入力すればフィルターできます。

   string のタイプの値をフィルターする時は「””」を付けて入力すればできます。

2. swift型とDBの型が異なる

 下の表のように swift で使っているタイプは大体同じように使えますが、配列形は別に合わせる作業が必要です。

Swift と Realm の比較

Swift Realm
Bool Bool
Int Int
Float Float
Double Double
String String
Data Data
Data Data
Double Double
Object Object?
Array List
  LinkingObjectse

 

上の表の通りに swift での array 型は、 Realm では List 型で使われています。

 

// 下記のように配列を表現をする方法が違います。
var sample1: [SampleModel]     //Swift
var sampleRealm: RealmSwift.List<SampleRealmData>    //Realm

そのままお互いにデータをやり取りするとコンパイルエラーが発生するのでお互いにタイプキャストが必要です。
そこで、それぞれの配列をキャストするための一例が以下です。

// Realm -> Swift
var sampleRealm: RealmSwift.List
let sampleRealmArray = Array(sampleRealm)<SampleRealmData>  //  sampleRealmはList型が、ArrayでキャストすることでSwiftのArrayで使える
// Swift -> Realm
var sample1: [SampleModel]
var sampleRealm: RealmSwift.List<SampleRealmData>
sampleRealm.append(objectsIn: sample1) // Array型をappend(objectIn:)に追加することでRealmのList型にデータを変換できる

そして Swift で使う Dictionary 型に対しても別のキャスト作業が必要です。
キャストする方法は以下のサンプルコードのようになります。
例)

import RealmSwift
import SwiftUI
final class SampleImageRealmData: Object {
    /// メタデータ
  var metadata: RealmSwift.List<Metadata>? // この部分 } /// Realm用Dictionaryタイプ public class Metadata: Object { @Persisted var key: String // Dictionaryタイプのkey @Persisted var value: String // Dictionaryタイプのvalue convenience init(key: String, value: String) { self.init() self.key = key self.value = value } } extension SampleImageRealmData { func convertRealmToModel() -> SampleImageData { let image = UIImage(data: image ?? Data()) var dic: [String: Any]? = [String: Any]() if let metaData = metadata { dic = metaData.reduce(into: [String: Any]()) { result, item in let key = item.key let value = item.value result[key] = value } } else { dic = nil } let sampleImageData = SampleImageData( image: image, size: size, uniformTypeIdentifier: uniformTypeIdentifier, metadata: dic, fileName: fileName, filePath: filePath, isFullAngle: isFullAngle ) return sampleImageData } }

「sampleImageData」を見ると「metadata」という変数があります。この変数の Swift でのタイプは Dictionary 型なので Realm では基本的に Dictionary のタイプがないため key、 value を List 型で保存するしかないです。

 

3. 保存されるデータが多量になるとデータが詰まるケースもある

もしDBに登録するデータが膨大ならば、保存する時に時間がかかってしまうこともあります。
よりよい方法について調査をしてますが、決定版を見つけるのはまだ難しいです。
そこで、提案として以下のように「asyncOpne」を利用して、並列処理として保存する方法を紹介します。

Realm.asyncOpen(configuration: Realm.Configuration.defaultConfiguration) { (result) in
    switch result {
    case .success(let realm):
        DispatchQueue.global().async {
            // 並列データベース作業を行う
            try? realm.write {
                realm.add(object)
            }
        }
    case .failure(let error):
        print("Failed to open Realm: \(error.localizedDescription)")
    }
}

この処理をするときには注意事項があります。
それは、一緒に保存するデータの一貫性に注意しないと保存ができないことです。並列処理をする間に、同じオブジェクトの更新や削除などの修正を行わないよう、注意が必要です。

最後に

今回はRealmについて語りましたが、モバイル開発をするのであれば、時々触る機会があるデータベースなので色々な使い方を引き続き勉強していくことになると思います。
SPIDERPLUSシーンを考えてみても、サービスの重要な機能を支えるには欠かせない特徴を備えている、と改めて思います。
今後もっと勉強して、より良いサービスを提供できるようにしたいと思います。
この記事をここまでご覧くださった方のお役に立てたなら幸いです。

最後に、スパイダープラスでは仲間を募集中です。
スパイダープラスにちょっと興味が出てきたなという方がいらっしゃったらお気軽にご連絡ください。最後までご覧くださり、ありがとうございます。