GreenDAO 3.0で複数テーブルを結合する実践ガイド

GreenDAO 3.0ではアノテーション中心の記述でエンティティ間のリレーションが組めるようになった。しかし実際に1:1・1:N・N:Nを実装しようとすると「なぜnull?」「なぜStackOverflow?」という壁にぶち当たる。本記事では避けて通れない落とし穴と、それを回避するための実装パターンをまとめる。

1. 1対1(One-to-One)

例:User 1人につき Passport 1枚。

@Entity
public class User {
    @Id(autoincrement = true)
    private Long id;
    private String name;
    private Long passportId;          // FK

    @ToOne(joinProperty = "passportId")
    private Passport passport;        // セッター不要
}

@Entity
public class Passport {
    @Id
    private Long id;
    private String code;
}

ポイント

  • 外部キーpassportIdを明示し、@ToOne(joinProperty = "...")で紐付ける。
  • オブジェクトを直接セットせず、user.setPassportId(passport.getId())でIDを与える。
  • IDはLong型にしてnull許可。プリミティブlongだとsetId(null)でNPE。

2. 1対多(One-to-Many)

例:Department が複数の Employee を持つ。

@Entity
public class Department {
    @Id(autoincrement = true)
    private Long id;
    private String title;

    @ToMany(referencedJoinProperty = "deptId")
    private List<Employee> staff;     // セッター不要
}

@Entity
public class Employee {
    @Id(autoincrement = true)
    private Long id;
    private String fullName;
    private Long deptId;              // FK
}

挿入例

Department d = new Department();
d.setTitle("Sales");
deptDao.insert(d);

Employee e1 = new Employee();
e1.setFullName("Alice");
e1.setDeptId(d.getId());
empDao.insert(e1);

getStaff()を初めて呼んだ時点でLazyロードされる。デバッグ時はdept.getStaff()を必ず経由すること。

3. 自己結合(Self-referencing)

例:Category がサブカテゴリを複数持つ。

@Entity
public class Category {
    @Id(autoincrement = true)
    private Long id;
    private String label;
    private Long parentId;            // 自分へのFK

    @ToMany(referencedJoinProperty = "parentId")
    private List<Category> children;
}

parentId に親カテゴリの id を入れるだけで木構造を表現できる。

4. 多対多(Many-to-Many)

例:Student と Course は中間テーブル Enrollment を介して結合。

@Entity
public class Student {
    @Id(autoincrement = true)
    private Long id;
    private String name;

    @ToMany
    @JoinEntity(
        entity = Enrollment.class,
        sourceProperty = "studentId",
        targetProperty = "courseId"
    )
    private List<Course> courses;
}

@Entity
public class Course {
    @Id(autoincrement = true)
    private Long id;
    private String title;

    @ToMany
    @JoinEntity(
        entity = Enrollment.class,
        sourceProperty = "courseId",
        targetProperty = "studentId"
    )
    private List<Student> students;
}

@Entity
public class Enrollment {
    @Id(autoincrement = true)
    private Long id;
    private Long studentId;
    private Long courseId;
}

データ挿入

Student s = new Student(); s.setName("Ken"); studentDao.insert(s);
Course c = new Course();   c.setTitle("Math"); courseDao.insert(c);

Enrollment e = new Enrollment();
e.setStudentId(s.getId());
e.setCourseId(c.getId());
enrollmentDao.insert(e);

注意:toString()で相互参照すると無限ループするため、どちらか一方だけリストを出力するか、デバッグ用に専用のDTOを作るとよい。

5. トラブルシューティングまとめ

  • List<E>がnull:セッターは存在しない。getXxx()で初回アクセス時にロードされる。
  • StackOverflowError:双方向リレーションのtoString()で再帰呼び出し。片側を省略する。
  • 外部キー制約エラー:先に親を挿入してから子を挿入する順序を守る。
  • パフォーマンス:大量データならGreenDAO、小規模で直感的に扱いたいならRealmを選択。

タグ: GreenDAO Android ORM One-to-One One-to-Many Many-to-Many

5月16日 16:19 投稿