Islands in the byte stream

Technical notes by a software engineer

Orma v3.0.0-rc2 と内部設計の見直し

Orma v3.0.0-rc2をリリースしました。CHANGESはこちら:

Android-Orma/CHANGELOG.md at master · gfx/Android-Orma · GitHub

三行まとめ

  • あるモデルが複数の同じ型のモデルをdirect associationで持てるようになりました
  • 内部的に非常に大きな変更を行ったので新しいバージョンを検証してみてください
  • v3.0.0はもうしばらく寝かせてから来週あたりにリリース予定です

解説

このバージョンは、3.0.0-rc1と比較してただ1つのバグフィクスを含みます。しかし、この修正のために内部の設計の見直しが必要だったため、変更量が非常に大きいものになっています。

そもそもdirect associationとはなにか、このリリースで解決したかったことはなにかを説明します。

Direct Associations

OrmaモデルAが別のOrmaモデルBを直接カラムとして保持することを、Ormaでは "direct association" と言っています。

たとえば以下のようなモデルがあるとき、BlogEntryはauthor:Userのdirect associationを持つということです。

@Table
public class User {
  @PrimaryKey
  long id;

  @Column
  public String name;
}

public class BlogEntry {
  @PrimaryKey
  public long id;

  @Column
  User author;

  @Column
  String title;

  // ...
}

これをSELECTでデータを検索してモデルを構築する際は、JOINを使って関連するモデルのデータもすべてロードする、いわゆるeager loadを行います。Ormaはモデルに制約を課していないので、direct associationの際はeager loadする他ないのです ((lazy loadを行うにはSingleAssociation<T> を使ったindirect associationにすればできます。しかし、モデルのインターフェイスが変わってしまいます。)) 。まあ、eager loadはそれが起きると分かっていれば、N+1クエリも発生しないしクエリの結果も予想しやすいので便利なものではあります。

JOINs

さてOrma v2の時点では、BlogEntryを生成するためクエリは次のようなものでした。

SELECT BlogEntry.id, BlogEntry.author, BlogEntry.title,  User.id, User.name FROM BlogEntry
  LEFT OUTER JOIN User ON
    BlogEntry.author = User.id
    -- ...

fully-qualified nameで参照しているので、仮にBlogEntryとUserに共通するフィールド(たとえばid)があったとしても問題なくクエリを発行できます。

ところが、このやり方だとBlogEntryがさらに別のUserをdirect associationで保持したとたんうまく行かなくなります。たとえば、BlogEntryに User u2 というカラムに追加したとすると、SELECTの再は次のようなクエリが生成されます。 SELECT句のUser.id /* for author */User.id /* for u2 */ を区別する方法がないため、SQLiteに渡すとエラーになります。

SELECT BlogEntry.id, BlogEntry.author, BlogEntry.title, User.id /* for author */, User.name, User.id /* for u2 */, User.name FROM BlogEntry
  LEFT OUTER JOIN User ON
    BlogEntry.author = User.id
  LEFT OUTER JOIN User ON
    BlogEntry.u2 = User.id
    -- ...

Table Name Aliases

この問題を解決するには、それぞれのテーブルに対して関連するテーブル群においてユニークな別名(alias)を付ける必要があります。またそのことにより、カラム名を静的に生成できなくなります。たとえばUserがどのモデルから参照されているかによって、別名が変わる可能性があるからです。

この「関連するテーブル群においてユニークな別名を付ける」というのが Aliases というクラスで、たとえば "BlogEntry.author:User" のようなカラムに至るパスを与えると "u10" (User型のカラムなのでu)といった別名を割り当てます。これは同じパスに対しては常に同じ値を返すので、これを信頼してJOIN句やWHERE句のカラム名を構築していくことになります。

The Schema Classes (v3)

というわけでOrma v3 (rc2) におけるスキーマクラスはこんな感じになりました。

Item_Schema.java

v2での同じクラスは次のようになります。

Item_Schema.java

クラス変数として定義しているものがほとんどなくなり、多くがインスタンス変数になっているのが特徴です。 特に、カラム定義(ColumnDef)がstatic fieldではなくinstance fieldになったので、これらを使っているアプリケーションは変更が必要です。

また getSelectFromTableClause() が単一の文字列ではなくなったので、少し見通しも悪くなりました。しかしこれはやむを得ない変更です。

全体として、動的に構築するSQLが増えた分パフォーマンスが低下している可能性はあります。とはいえ内蔵のベンチマークだとほとんど差が見られないので、許容範囲だと思います。

さて以上のように、v3は内部的に大幅に変更を加えました。既存のテストはすべてパスしているので大丈夫だとは思いますが、不安定になっている可能性もあるので検証してみてください。v3.0.0正規版も10月半ばから下旬くらいにはリリースする予定です。