Android

[Android Architecture Components] Roomの自動生成コードを眺めてみる

この記事はSmartDrive Advent Calendar 2017の17日目の記事です

ここではAndroid Architecture ComponentsのSQLite向けLibraryのRoomについて書きます。

Roomの特徴

RoomはよくあるORMのようなQueryBuilderを使わず、生のQueryを記述し得られた結果を任意のクラスにmappingしてデータを取得します。

ここではそのmappingがどのように行われているか眺めてみます。

眺めてみる

まず以下のようなデータアクセスコードを記述してみます。

@Entity(
        tableName = UserEntity.tableName
)
class UserEntity(
        @PrimaryKey
        val id: Long,
        val name: String
) {
    companion object {
        const val tableName = "user_entity"
    }
}

@Dao
interface UserDao {
    @Query("select id, name from ${UserEntity.tableName}")
    fun findAll(): List<User>
}

data class User(
        val id: Long,
        val name: String
)

これをbuildすると以下のようなUserDaoの実装クラスUserDao_Impl.javaが作られます。

UserDao_Impl.java
public class UserDao_Impl implements UserDao {
  private final RoomDatabase __db;

  public UserDao_Impl(RoomDatabase __db) {
    this.__db = __db;
  }

  @Override
  public List<User> findAll() {
    final String _sql = "select id, name from user_entity";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    final Cursor _cursor = __db.query(_statement);
    try {
      final int _cursorIndexOfId = _cursor.getColumnIndexOrThrow("id");
      final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
      final List<User> _result = new ArrayList<User>(_cursor.getCount());
      while(_cursor.moveToNext()) {
        final User _item;
        final long _tmpId;
        _tmpId = _cursor.getLong(_cursorIndexOfId);
        final String _tmpName;
        _tmpName = _cursor.getString(_cursorIndexOfName);
        _item = new User(_tmpId,_tmpName);
        _result.add(_item);
      }
      return _result;
    } finally {
      _cursor.close();
      _statement.release();
    }
  }
}

かなり単純なコードが生成されましたね😋
cursorを使って必要なデータを拾って、対応するpropertyに突っ込んでオブジェクトを生成しているんですねー。なるほどー。ふむふむ。

Userクラスを変更する

ちなみにUserDaoはそのままでUserのpropertyを変更したらどうなるのでしょうか。
自動生成されたコードを眺めているとDaoとモデルの不整合をcompileで弾いてくれなさそうな不安がよぎります。
他のORMだとcompileの時点で間違いに気がつくのに、仮にRoomだとそれがわからないましてや実際にアプリを動かしてみるまでわからないとなってしまうと結構なつらみが。。。

どうなるのでしょう。
試してみます。

Userクラスに先ほどまでなかったageというpropertyを追加します。

data class User(
        val id: Long,
        val name: String,
        val age: Int
)

この状態でbuildしてみると。。。

Warning:(9, 1) 警告:  com.example.nobu.tableinheritanceexample.User has some fields [age] which are not returned by the query. If they are not supposed to be read from the result, you can mark them with @Ignore annotation. You can suppress this warning by annotating the method with @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH). Columns returned by the query: id, name. Fields in com.example.nobu.tableinheritanceexample.User: id, name, age.
Error:(9, 1) エラー: The columns returned by the query does not have the fields [age] in com.example.nobu.tableinheritanceexample.User even though they are annotated as non-null or primitive. Columns returned by the query: [id,name]

エラーが発生してbuildが落ちました。
ある程度Queryとの整合性を検査してくれているみたいです。
これでModelだけ変更してEntityやDaoの変更を忘れてしまっても気がつきそうですね😋

まとめ

  • RoomはQuery結果をいい感じにmappingしてくれている
  • build時にQueryとModelのmappingが検査される