SysTecevo

Android RoomのTypeConverterとは? JavaでDate・Listを保存する方法とRoom 3との違い

初回掲載日:
最終更新日:

JavaのDateやListをRoomのTypeConverterでSQLiteの値へ変換する仕組み

結論から言うと、RoomのTypeConverterは、DateやListのようにSQLiteがそのまま扱えないJavaの型を、LongやStringなどの保存可能な型へ変換する仕組みです。JavaでRoom 2.xを使うなら、@TypeConverterと@TypeConvertersを中心に実装します。

この記事では、Dateをミリ秒のLongへ、ListをJSON文字列へ変換する実装を軸に、enumの保存形式、nullと空コレクションの違い、変換エラー、既存データへの影響まで整理します。Room 3.xの新しいandroidx.room3 APIは最後に比較し、Room 2.xのJavaコードと混同しないように説明します。

JavaのDateやListをTypeConverterでSQLite列へ変換しDAOで読み出す流れ
TypeConverterは、エンティティの値とSQLite列の値を相互変換します。

RoomのTypeConverterとは

RoomがエンティティをSQLiteへ保存するとき、フィールドはSQLiteが扱える型へ対応付けられます。DateやListのような型をそのまま保存できない場合に、変換メソッドを登録して保存時と読み出し時の変換方法を教えるのがTypeConverterです。

Java側の型保存形式の例特徴
DateLongエポックからのミリ秒。比較しやすい
List<String>JSON文字列構造を保持できるがSQL検索には不向き
enum安定した文字列またはコードordinalではなく互換性を意識する

TypeConverterは「データベースのMigrationを自動で行う機能」ではありません。保存形式を変える場合は、既存行を新形式へ移すMigrationや互換読み出しが必要になることがあります。

まず保存形式を決める

Converterを書く前に、SQLiteへ何を保存するかを決めます。DateならLongが扱いやすく、ListならJSON文字列が候補です。一方、Listの各要素を検索・集計したいなら、1列へJSONで詰め込むより別テーブルに正規化する設計が適しています。

RoomでDateをLong、ListをJSON文字列、enumを安定したコードや文字列で保存する比較
Date・List・enumで保存形式を選ぶときの比較です。

JavaでDateをLongとして保存する

Room 2.xでは、DateからLongへ変換するメソッドと、LongからDateへ戻すメソッドを用意します。nullを許容するフィールドなら、両方向でnullを返す処理を入れます。

import androidx.room.TypeConverter;
import java.util.Date;

public final class RoomConverters {
    private RoomConverters() {
    }

    @TypeConverter
    public static Long fromDate(Date value) {
        return value == null ? null : value.getTime();
    }

    @TypeConverter
    public static Date toDate(Long value) {
        return value == null ? null : new Date(value);
    }
}

エンティティ側は、たとえば次のようにConverterを適用します。

import androidx.room.Entity;
import androidx.room.PrimaryKey;
import androidx.room.TypeConverters;
import java.util.Date;

@Entity
@TypeConverters(RoomConverters.class)
public class Task {
    @PrimaryKey
    public long id;

    public Date dueAt;
}

DateをLongへする方式は、同じ時刻を一定の数値で保存できるのが利点です。ただしタイムゾーンをDateへ表示するときの扱いは別問題なので、保存時刻と表示ロケールを混同しないようにします。

List<String>をJSON文字列として保存する

文字列リストを1列へ保存する場合は、JSONなどの形式を使います。次の例はGsonを利用する場合の最小例です。Gsonの依存関係とバージョンは、実際のプロジェクトで採用しているライブラリに合わせてください。

import androidx.room.TypeConverter;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;

public final class ListConverters {
    private static final Gson GSON = new Gson();
    private static final Type LIST_TYPE =
            new TypeToken<List<String>>() { }.getType();

    private ListConverters() {
    }

    @TypeConverter
    public static String fromStringList(List<String> value) {
        return value == null ? null : GSON.toJson(value);
    }

    @TypeConverter
    public static List<String> toStringList(String value) {
        if (value == null) {
            return null;
        }
        try {
            return GSON.fromJson(value, LIST_TYPE);
        } catch (JsonParseException exception) {
            throw new IllegalArgumentException(
                    "Invalid List<String> JSON in the database", exception);
        }
    }
}

不正なJSONを空リストへ置き換える実装もありますが、データ破損を見逃すおそれがあります。運用上は、例外を記録して修復するのか、旧形式を読み取る互換処理を入れるのかを先に決めてください。

nullと空リストは別に扱う

  • null:値がない、未設定という意味にできる。
  • 空リスト:値は設定済みだが要素が0件という意味にできる。

この意味を曖昧にすると、復元後の画面表示や検索条件が変わります。Entity、Converter、UIのすべてで同じ方針を採用します。

enumはordinalではなく互換性を優先する

enumを整数のordinalで保存すると、列挙子の順番を変えたときに既存データの意味が変わります。文字列名も改名に弱いため、長期保存するデータなら独自の安定コードを用意する方法が安全です。

public enum TaskStatus {
    TODO("todo"),
    DONE("done");

    private final String code;

    TaskStatus(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

コードからenumへ戻すときに未知の値が来る可能性も考えます。アプリ更新前のデータや将来の値をどう扱うかを決めずに、単純なvalueOf()だけへ依存するのは危険です。

Converterの適用範囲

Room 2.xの@TypeConvertersは、指定場所によって適用範囲が変わります。広い範囲へ置けば便利ですが、同じ型に複数の変換規則があると意図しない適用や曖昧さにつながります。

配置場所主な適用範囲使いどころ
DatabaseDatabase配下のDAO・Entityなどアプリ全体で同じ規則
DAODAOのメソッドや引数特定DAOに限定
Entity / フィールド対象Entityまたはフィールド範囲を狭く保ちたい場合

公式APIでは、converterは列やフィールドの型変換に適用され、DAOの戻り値全体を任意の型へ変換する機能ではありません。どこへアノテーションを置くかは、保守性と既存コードへの影響を見て決めます。

よくあるエラーと確認順

Cannot figure out how to save this fieldが出た場合は、次の順番で確認すると切り分けやすくなります。

  1. 対象フィールドのJava型と、SQLiteで保存したい型を決める。
  2. 保存方向と読み出し方向の両方に、1引数・1戻り値のconverterメソッドがあるか確認する。
  3. @TypeConvertersの適用範囲がEntityや対象フィールドへ届いているか確認する。
  4. null、空リスト、不正なJSON、未知のenumコードの方針を確認する。
  5. 保存してから読み出す往復テストを実施する。
RoomのTypeConverterエラーを型、converter、適用範囲、nullや形式、往復テストの順に診断するフロー
エラーはconverterの有無だけでなく、適用範囲と保存形式まで確認します。

converterが認識されない

メソッドに@TypeConverterが付いているか、converterクラスを@TypeConvertersへ渡しているかを確認します。Databaseへ指定したつもりでも、別Databaseを使っているケースや、同じ型に複数の候補があるケースにも注意します。

nullでクラッシュする

変換メソッドがnullを受け取る可能性を考慮していないと、DateやJSON変換で例外が起きます。Entityのnull許容、Converterの戻り値、UIの初期値を一貫させます。

保存形式を変えたら既存データを読めない

たとえばDateをLongから文字列へ変更したり、ListのJSON形式を変えたりすると、既存列の値と新しいconverterの期待形式が合わなくなる可能性があります。旧形式も読める期間を設けるか、Migrationで変換してから新形式へ移行します。

TypeConverterとMigrationの違い

TypeConverterはアプリ内の型とSQLite列の間を変換します。Migrationはデータベースのスキーマや既存データを新しいバージョンへ移す仕組みです。

目的TypeConverterMigration
対象Java/Kotlinの型と列の値テーブル、列、既存行
例DateとLongの相互変換列追加、型変更、データ変換
役割通常の読み書きDBバージョン更新

converterを追加しただけで常にMigrationが必要になるわけではありません。しかし、既存列の保存形式や意味を変更するなら、Migrationを含めたデータ互換性の設計が必要です。Room Migrationについては、Room Migrationで既存データを維持する方法も参照してください。

Room 2.xとRoom 3.xの違い

Room 3.xはRoom 2.xの単なるバージョン番号更新ではありません。公式の移行ガイドでは、Room 3.0でパッケージ、コード生成、converterアノテーションなどが変わることが説明されています。

項目Room 2.xRoom 3.x
パッケージandroidx.roomandroidx.room3
型変換@TypeConverter / @TypeConverters@ColumnTypeConverter / @ColumnTypeConverters
中心言語・生成Javaを含む既存構成で利用Kotlin中心、KSP必須、Javaコード生成なし
移行判断既存Javaアプリで安定運用Kotlin/KMPなどの移行要件を確認

したがって、この記事のJava例はRoom 2.x向けです。Room 3.xへ移行する場合は、アノテーション名だけを置換せず、公式の移行手順、KSP、Kotlinコード生成、依存関係をまとめて確認してください。Room 3.0.3のリリースノートでも、androidx.room3やKotlinコード生成などの大きな変更が案内されています。

動作確認のチェックリスト

実装後は、converter単体とRoom経由の両方を確認します。次の項目は説明用の確認観点であり、この記事の作成環境でAndroid実機テストを実行したという意味ではありません。

  • Dateを保存して読み出し、ミリ秒が一致すること。
  • Listの通常値、空リスト、nullを往復できること。
  • 不正なJSONを検知し、決めた方針で扱えること。
  • enumの未知コードや名称変更を想定していること。
  • DAOから保存し、アプリ再起動後に読み出せること。
  • 保存形式を変更した場合はMigrationと既存データを確認すること。

FAQ

Q. Dateは文字列で保存してもよいですか?

可能ですが、並べ替えや範囲検索では、形式を厳密に固定できるLongの方が扱いやすいことがあります。表示用の文字列と保存用の値を分ける設計も検討します。

Q. Listは必ずTypeConverterで保存すべきですか?

1列へまとめて保存する要件なら候補になります。要素単位で検索・更新するなら、関連テーブルへ分ける方が適切です。

Q. Room 3.xではこのJavaコードをそのまま使えますか?

そのまま使えると考えないでください。Room 3.xはandroidx.room3、KSP、Kotlinコード生成、ColumnTypeConverter系のAPIが中心です。移行時は公式ガイドに沿って構成を確認します。

まとめ

RoomのTypeConverterは、Date・List・enumなどのJava型をSQLiteで保存できる値へ変換するための仕組みです。まず保存形式を決め、nullや空値、未知の値まで含めて往復変換を設計します。保存形式そのものを変える場合は、TypeConverterだけで解決せず、Migrationと既存データの互換性を確認してください。

今回のJava例はRoom 2.xの@TypeConverter / @TypeConvertersを前提にしています。Room 3.xはandroidx.room3と@ColumnTypeConverter系へ移行しているため、バージョンを確認してから実装方式を選ぶことが重要です。

公式情報