[Realm][Swift] 新しいタイプの導入

Realm がダイレクトにサポートしていないタイプを使う方法を説明します。

Realm は、非常に便利ですが、タイプとしてサポートされていない情報を保持しようとすると少し面倒だったので、その方法を説明します。

Update(2020.11.1) 10進数表現のタイプという意味では、Decimal128 が Realm に正式に導入されました。

[Realm][Swift] Realm に Decimal が導入されました

追加したかったタイプ

家計簿アプリを作りたかったので、Realm が Decimal をサポートするのを待っています。
しばらくかかりそうなので、自分で Decimal 相当を組み込んでみることにしました。

Decimal

10の冪乗で処理されるため、10進数の計算で誤差が発生しないことが特徴です。ですので、金融関連のアプリケーションでは必須と言えるタイプになっています。

MEMO
Double 等は、2の冪乗で処理され、有効桁数も限られているため、どうしても誤差が発生してしまいます。

独自タイプの定義

自分で Realm 自体に手を入れることは、あまり意味がないので、Realm でサポートしているタイプを組み合わせて保存することにしました。

金額を記憶させるタイプなので、"Currency" としました。

設計メモ
ドルでも、$1.23 などと、小数点以下の数字が出てくることがあります。
Web で調べたところ、小数点以下の数字のベースを補助通貨というそうで、ドルでは、セントが該当します。
大抵は、補助通貨は、ベース通貨の 1/100 である国が多いのですが、いくつかの国は、1/1000 までありました。

日本は、数少ない 1/1000 の補助通貨を持つ国です。円に対して、厘が、1/1000 となっています。

ということで、数字を 1000 倍して、Int64 に保存するよう設計しました。

Int64 は、9223372036854775807 (≒ 9.2e18) まで保持できて、その 1/1000 としても1.8e15 まで保持できるので、十分と判断しました。

Currency タイプ

public class Currency: Object {
  // smallest sub-currency is 1/1000, so storing .0001 woudl be enough for keeping precision (maybe too much for USD, EUR, ...)
  @objc dynamic var savedValue: Int64  // store value*1000, ex: $100 -> 100,000 for keeping decimal precision
  
  public required init() { // required from Object, use zero as default value
    self.savedValue = 0
  }
}

後から、必要に応じて、Decimal からの initializer や、Int64 からの initializer を追加します。

モデルに独自タイプを持たせる

以下では、Realm で定義していた 口座のモデル (Account)に、初期残高を持たせるようにしたものです。

Account Model in Realm with Currency

public class MCAccount: Object {
  @objc open dynamic var id:String
  @objc open dynamic var name:String = ""
  @objc open dynamic var initialAmount:Currency? // (1) 独自タイプ追加
  public let tags = List()
  @objc open dynamic var askedForDelete = false
  
  public override static func primaryKey() -> String? {
    return "id"
  }
  
  public required init() {
    self.id = UUID().uuidString
    super.init()
  }
}

(1) に初期残高を追加しました。よく考えれば、独自のタイプは、Realm での新しいタイプの Object になるので、この場合であれば、to-one relationship になるものでした。

to-one relationship

to-one relationship を追加して気付いたのですが、Realm では、to-one relationship は、optional で定義しないといけないという制限がありました。

そこで、optional にして定義しました。

# ここから、思ったより手間であることがわかり始めます。

optional

Realm がサポートしているタイプであれば、to-one relationship は単にプロパティということになるので、optional であることは必要ありません。

サポートしていないタイプを、プロパティのように持たせるために、to-one relationship という形で持たなければいけなくなりました。

アプリ設計者視点としては、そのプロパティを持つ Object (ここでは、Account) が初期化されたときに、Currency も初期化してくれて良いのです。

ですが、Realm 的な DB の観点からすると Object 間の関係を持っている要素を初期化等するときに、相手先の Object がすでに存在するかはわからないので、
optional にしてくれないと困る というのもよくわかります。

わかるのですが、optional としてしまうと、Account から Currency へのアクセスが常に optional かどうかのチェックが必要となり面倒です。

自分の場合は、すでにある程度コードを書いていたので、このような基本的なアクセスに変更が入るのは避けたいです。

wrapper

ということで、以下のような Wrapper を作って、できるだけアクセスAPIの変更がないようにしました。

example code

public class MCAccount: Object {
  @objc open dynamic var id:String
  @objc open dynamic var name:String = ""
  @objc open dynamic var internalInitialAmount:Currency? // (1) internalInitialAmount として、直接アクセスさせない
  @objc open dynamic var comment: String = ""
  
  var initialAmount:Currency {  // (2) initialAmount としては、computed property を定義して、これまでと同等のアクセスを用意
    get {
      if internalInitialAmount == nil {
        self.internalInitialAmount = Currency(int64: 0)
      }
      return self.internalInitialAmount!
    }
    set {
      internalInitialAmount = newValue
    }
  }
}

まとめ:独自タイプを Realm で定義したモデルに追加する

以下の手順で追加できます。

独自タイプを追加する
  • (必須)欲しいタイプを Object を継承して定義する
  • (必須)モデル要素からの to-one relationship としてモデルに追加する
  • (任意)wrapper accessor を定義すると便利

説明は以上です。
不明な点やおかしな点ありましたら、ご連絡いただけるとありがたいです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です