[Android/Java]Roomでローカルデータベースを操作する

# 環境

- androidx.room 2.1.0

# 基本

- `@Entity` でエンティティのクラスを作る。
- `@Dao` でエンティティを操作するクラスを作る。
- `RoomDatabase` を継承したクラスを作って、そのメンバーとして `@Dao` を指定したクラスを登録する
- `RoomDatabase` を継承したクラスをアプリから操作する

`RoomDatabase` を継承したクラスは、メインスレッドでは操作できない点に注意。

## `@Entity` クラスの例

```java
@Entity(indices = {@Index(value = {"name"}, unique = true)})
public class User {
	@PrimaryKey
	public long id;

	@ColumnInfo
	public String name;
}
```

## `@Dao` クラスの例

```java
@Dao
public abstract class UserDao {
	@Query("select id from user order by id desc limit 1")
	public abstract long getMaxId();

	@Insert(onConflict = OnConflictStrategy.IGNORE)
	abstract void insert(User user);

	@Query("delete from user where id = :id")
	abstract void delete(long id);
}
```

## `RoomDatabase` 継承クラスの例

```java
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
	public abstract UserDao userDao();
}
```

## DB インスタンスを生成する Singleton クラスの例

```java
public class AppDatabaseSingleton {
	private static AppDatabase instance = null;
	private AppDatabaseSingleton() {}

	public static AppDatabase getInstance(Context context) {
		if (instance != null) {
			return instance;
		}
		instance =  Room.databaseBuilder(context,
				AppDatabase.class, "app_database").build();
		return instance;
	}
}
```

## データベースの操作例

```java
new Thread(new Runnable() {
	@Override
	public void run() {
		AppDatabase db = AppDatabaseSingleton.getInstance(context);
		long maxId = db.UserDao().getMaxId();
		callback.onComplete(maxId);
	}
}).start();
```

# テーブル名を指定する

`@Entity` に `(tableName = {"テーブル名")` という感じで書く。指定しない場合はモデル名と同じ名前でテーブルが作られてしまうので、できれば指定したほうが良いと思う。

```java
@Entity(tableName = "users")
public class User {
    @PrimaryKey
    public int id;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;
}
```

# モデルに持ちたいがDBに保存したくない列を作る

列名の前に `@Ignore` を書く。

```java
@Entity
public class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}
```

# 複数カラムにユニークインデックスする

`@Entity` に `(indeces = {@Index(value = {"1列目", "2列目"}, unique = true)})` という感じで書く。

```java
@Entity(indices = {@Index(value = {"first_name", "last_name"},
        unique = true)})
public class User {
    @PrimaryKey
    public int id;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;
}
```

# 複数まとめて設定したい

カンマ区切りで書く

```java
@Entity(tableName = "users",
        indices = {@Index(value = {"first_name", "last_name"},
        unique = true)})
```

# INSERT IGNORE 的な事をしたい

まず基本的な事として、クエリの発行は `@Entity` ではなく `@Dao` で行う。

`@Insert` に `(onConflict = OnConflictStrategy.IGNORE)` を追記する。
入れ替えたい時は `(onConflict = OnConflictStrategy.REPLACE)` 。

```java
@Dao
public interface MyDao {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    public void insertUsers(User... users);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertBothUsers(User user1, User user2);

    @Insert
    public void insertUsersAndFriends(User user, List<User> friends);
}
```

# トランザクションしたい

トランザクションする場合、 `interface` から `abstract class` に変更する必要がある。
その上で、 `@Transaction` を対象メソッドに指定する。

```java
@Dao
abstract class UsersDao {
    @Transaction
    public void setLoggedInUser(User loggedInUser) {
        deleteUser(loggedInUser);
        insertUser(loggedInUser);
    }

    @Query("DELETE FROM users")
    abstract void deleteUser(User user);

    @Insert
    abstract void insertUser(User user);
}
```

# 後からテーブルや列の追加の追加を行いたい

SQLite の時と同様にバージョンを上げる。上げただけだと何もやってくれないので、何をするかは書く必要がある。
ていうか書かないと怒られるのである。

```
A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.
```

何をすれば良いかは親切にエラーメッセージに書いてあるので、端的に言えばそのとおりにするだけだ。
例えばバージョン3まで上げて、バージョン2で `Fruit` テーブルを追加し、バージョン3で `Book` テーブルに `pub_year` という `Integer` のカラムを追加した場合、以下のようになる。

```java
@Database(entities = {Fruit.class, Book.class}, version = 3)
public abstract class AppDatabase extends RoomDatabase {
	:
	public static AppDatabase getInstance(Context context) {
		if (instance != null) {
			return instance;
		}
		static final Migration MIGRATION_1_2 = new Migration(1, 2) {
			@Override
			public void migrate(@NonNull SupportSQLiteDatabase database) {
				database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
						+ "`name` TEXT, PRIMARY KEY(`id`))");
			}
		};
		static final Migration MIGRATION_2_3 = new Migration(2, 3) {
			@Override
			public void migrate(@NonNull SupportSQLiteDatabase database) {
				database.execSQL("ALTER TABLE Book "
						+ " ADD COLUMN pub_year INTEGER");
			}
		};
		instance =  Room.databaseBuilder(context,
				AppDatabase.class, "database-name")
				.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
				.build();
		return instance;
	}
}
```

この辺もうまくラップして欲しい気がするが、現状はわりと人力感が漂っている。
テーブルの作成SQLなどは、Roomの展開されたほうを参考にそのまま流用すると比較的楽かも知れない。
例えば、 `com.example.db.AppDatabase` というクラス名で `RoomDatabase` の継承クラスを作っていた場合は、 `app/build/generated/source/apt/debug/com/example/db/AppDatabase_Impl.java` に展開された内容が出力されている。
以下は `AppDatabase_Impl` の一例。

```java
@SuppressWarnings({"unchecked", "deprecation"})
public final class AppDatabase_Impl extends AppDatabase {
  @Override
  protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
    final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(2) {
      @Override
      public void createAllTables(SupportSQLiteDatabase _db) {
        _db.execSQL("CREATE TABLE IF NOT EXISTS `Fruit` (`id` INTEGER NOT NULL, `name` TEXT, PRIMARY KEY(`id`))");
        :
      }
      :
    }
  }
}
```

# 参考

- [Defining data using Room entities  |  Android Developers](https://developer.android.com/training/data-storage/room/defining-data)
- [Accessing data using Room DAOs  |  Android Developers](https://developer.android.com/training/data-storage/room/accessing-data)
- [Insert  |  Android Developers](https://developer.android.com/reference/androidx/room/Insert.html)
- [Migrating Room databases  |  Android Developers](https://developer.android.com/training/data-storage/room/migrating-db-versions#java)

# その他

Googleさんのググラビリティ低い名前の付け方、ちょっと何とかならないか…