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を選択。