Relations and Type Converters

Relationships are explicit in Room 234 . A reading note references its book with a foreign key, and a data class with @Embedded and @Relation describes "a book with its notes". Room fills it with two queries, so the DAO function in Reactive Queries carries @Transaction to give both the same snapshot.

data/local/Entities.kt: a one-to-many relation and a type converter (excerpt)Kotlin
@Entity(
  tableName = "notes",
  foreignKeys = [ForeignKey(entity = BookEntity::class, parentColumns = ["id"],
    childColumns = ["bookId"], onDelete = ForeignKey.CASCADE)],
  indices = [Index("bookId")],
)
data class NoteEntity(
  @PrimaryKey(autoGenerate = true) val noteId: Long = 0,
  val bookId: Int,
  val text: String,
  val createdAt: Instant,               // SQLite has no date type: see Converters
)
// 5.25.3 a one-to-many relation, assembled by Room from two queries
data class BookWithNotes(
  @Embedded val book: BookEntity,
  @Relation(parentColumns = ["id"], entityColumns = ["bookId"]) val notes: List<NoteEntity>,
)
class Converters {
  @ColumnTypeConverter fun toEpochMillis(value: Instant): Long = value.toEpochMilli()
  @ColumnTypeConverter fun fromEpochMillis(value: Long): Instant = Instant.ofEpochMilli(value)
}

Room 3.0 changed two details that older tutorials get wrong: @Relation takes arrays (parentColumns, entityColumns; Room 2.x had parentColumn), and converters are @ColumnTypeConverter. The Room 2.x form failed here with Cannot have empty 'parentColumns' in @Relation. CASCADE deletes a book's notes with it, and the index on bookId keeps that fast. Many-to-many relations add a cross-reference entity through @Relation(associateBy = Junction(...)). The converter stores each Instant as UTC epoch milliseconds (1790285143957 for the first note), and the notes screen formats it for display; never store local-time strings.