Realm は、非常に便利ですが、タイプとしてサポートされていない情報を保持しようとすると少し面倒だったので、その方法を説明します。
Update(2020.11.1) 10進数表現のタイプという意味では、Decimal128 が Realm に正式に導入されました。
[Realm][Swift] Realm に Decimal が導入されました
Sponsor Link
追加したかったタイプ
家計簿アプリを作りたかったので、Realm が Decimal をサポートするのを待っています。
しばらくかかりそうなので、自分で Decimal 相当を組み込んでみることにしました。
Decimal
10の冪乗で処理されるため、10進数の計算で誤差が発生しないことが特徴です。ですので、金融関連のアプリケーションでは必須と言えるタイプになっています。
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 まで保持できるので、十分と判断しました。
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)に、初期残高を持たせるようにしたものです。
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の変更がないようにしました。
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 を定義すると便利
説明は以上です。
不明な点やおかしな点ありましたら、ご連絡いただけるとありがたいです。
Sponsor Link