# Room Database ใน Android 2026: Migrations, Relations และ Coroutines > เชี่ยวชาญ Room database ใน Android พร้อม migrations, entity relations, Kotlin coroutines และ Flow บทเรียนที่ครบถ้วนพร้อม patterns สำหรับ production ในปี 2026 - Published: 2026-07-10 - Updated: 2026-07-10 - Author: Anthony Fillion-Maillet - Tags: android, room, kotlin, sqlite, coroutines, database - Reading time: 12 min --- Room database มอบ abstraction layer ที่แข็งแกร่งเหนือ SQLite ใน Android โดยจัดการ boilerplate code ที่ทำให้การใช้ SQLite โดยตรงเป็นเรื่องน่าเบื่อ ด้วย Room 2.6 และ Kotlin coroutines การสร้าง database layers ที่เป็น reactive และ type-safe กลายเป็นเรื่องง่ายขึ้นอย่างมาก > **ฟีเจอร์หลักของ Room 2.6** > > Room 2.6 (stable ในปี 2026) นำเสนอการสนับสนุน migration ที่ปรับปรุงแล้ว ความเร็วในการประมวลผล KSP ที่เพิ่มขึ้น และการผสานรวม Flow ที่ดีขึ้น บทเรียนนี้ใช้ API ล่าสุดสำหรับโค้ดที่พร้อมใช้งานจริง ## การตั้งค่า Room Database ด้วย Kotlin KSP Room ต้องการสามองค์ประกอบหลัก: entities (ตาราง), DAOs (data access objects) และ database class ตัวประมวลผล annotation ของ KSP สร้างโค้ด implementation ในเวลา compile เพิ่ม dependencies ในไฟล์ `build.gradle.kts` ระดับ module KSP แทนที่ KAPT เป็นตัวประมวลผล annotation ที่แนะนำตั้งแต่ปี 2024 โดยให้เวลา build ที่เร็วขึ้น ```kotlin // build.gradle.kts (Module :app) plugins { id("com.google.devtools.ksp") version "2.1.0-1.0.29" } dependencies { val roomVersion = "2.6.1" implementation("androidx.room:room-runtime:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion") // Coroutines support ksp("androidx.room:room-compiler:$roomVersion") } ``` Artifact `room-ktx` มอบ coroutine extensions ที่อนุญาตให้ใช้ suspend functions ใน DAOs ## การกำหนด Entities พร้อม Primary Keys และ Indices Entities ถูกแมปโดยตรงกับตาราง SQLite แต่ละ entity class ต้องมีอย่างน้อยหนึ่ง primary key การเพิ่ม indices บนคอลัมน์ที่ถูก query บ่อยช่วยปรับปรุงประสิทธิภาพการอ่านแลกกับการเขียนที่ช้าลงเล็กน้อย ```kotlin // User.kt import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey @Entity( tableName = "users", indices = [ Index(value = ["email"], unique = true), // Unique constraint + index Index(value = ["created_at"]) // Index for sorting queries ] ) data class User( @PrimaryKey(autoGenerate = true) val id: Long = 0, val email: String, val displayName: String, @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(), val isActive: Boolean = true ) ``` Annotation `@ColumnInfo` ปรับแต่งชื่อคอลัมน์เมื่อชื่อ property ของ Kotlin แตกต่างจากชื่อคอลัมน์ database ที่ต้องการ ## การสร้าง DAOs ด้วย Coroutines และ Flow DAOs กำหนด operations ของ database Room 2.6 รองรับสาม async patterns: suspend functions สำหรับ operations แบบ one-shot, `Flow` สำหรับ reactive streams และ `ListenableFuture` สำหรับ Java interop ```kotlin // UserDao.kt import androidx.room.* import kotlinx.coroutines.flow.Flow @Dao interface UserDao { // One-shot insert - returns the generated ID @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUser(user: User): Long // Batch insert - efficient for multiple records @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertUsers(users: List) // Flow emits new data whenever the table changes @Query("SELECT * FROM users WHERE is_active = 1 ORDER BY created_at DESC") fun observeActiveUsers(): Flow> // Suspend function for one-shot read @Query("SELECT * FROM users WHERE id = :userId") suspend fun getUserById(userId: Long): User? // Update returns the number of affected rows @Update suspend fun updateUser(user: User): Int // Delete by primary key @Delete suspend fun deleteUser(user: User) // Custom delete query @Query("DELETE FROM users WHERE is_active = 0") suspend fun deleteInactiveUsers(): Int } ``` Queries ที่คืนค่า Flow จะ emit ผลลัพธ์ที่อัปเดตโดยอัตโนมัติเมื่อข้อมูลพื้นฐานเปลี่ยนแปลง สิ่งนี้กำจัด logic การ refresh แบบ manual ใน ViewModels ## การสร้าง Room Database Class Database class เชื่อมโยง entities และ DAOs เข้าด้วยกัน Pattern singleton ป้องกันการมีหลาย database instances ซึ่งอาจทำให้เกิดการรั่วไหลของ resource ```kotlin // AppDatabase.kt import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase @Database( entities = [User::class, Post::class], version = 1, exportSchema = true // Generates schema JSON for migration validation ) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao abstract fun postDao(): PostDao companion object { @Volatile private var INSTANCE: AppDatabase? = null fun getInstance(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_database" ) .fallbackToDestructiveMigration() // Only for development .build() INSTANCE = instance instance } } } } ``` การตั้งค่า `exportSchema = true` สร้างไฟล์ schema JSON ในโฟลเดอร์ `schemas/` ซึ่งมีประโยชน์สำหรับการตรวจสอบ migrations ในการทดสอบ > **กลยุทธ์ Migration สำหรับ Production** > > อย่าใช้ `fallbackToDestructiveMigration()` ใน production เด็ดขาด สิ่งนี้จะลบข้อมูลผู้ใช้ทั้งหมดเมื่อ schema เปลี่ยนแปลง ส่วนถัดไปจะกล่าวถึงการจัดการ migration อย่างถูกต้อง ## การเขียน Database Migrations ที่ปลอดภัย Migrations รักษาข้อมูลผู้ใช้เมื่อ schema เปลี่ยนแปลงระหว่างเวอร์ชันของแอป Room ตรวจสอบ migrations ในเวลา runtime โดยเปรียบเทียบ schema ที่คาดหวังกับ database ที่ถูก migrate แล้ว Migration กำหนดคำสั่ง SQL ที่จำเป็นในการแปลง database จากเวอร์ชัน N เป็นเวอร์ชัน N+1 ```kotlin // Migrations.kt import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase // Migration from version 1 to 2: add phone_number column val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE users ADD COLUMN phone_number TEXT") } } // Migration from version 2 to 3: add posts table with foreign key val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(""" CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """.trimIndent()) db.execSQL("CREATE INDEX index_posts_user_id ON posts(user_id)") } } // Register migrations in database builder fun getInstance(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_database" ) .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build() .also { INSTANCE = it } } } ``` Room รองรับการ chain migrations หากผู้ใช้อัพเกรดจากเวอร์ชัน 1 เป็นเวอร์ชัน 3 Room จะประมวลผล MIGRATION_1_2 จากนั้น MIGRATION_2_3 ตามลำดับ ## Auto-Migrations ใน Room 2.6 สำหรับการเปลี่ยนแปลง schema ที่ง่าย (เพิ่มคอลัมน์ ตาราง หรือ indices) Room สามารถสร้าง migrations โดยอัตโนมัติ สิ่งนี้ลด boilerplate สำหรับการอัปเดตที่ตรงไปตรงมา ```kotlin // AppDatabase.kt @Database( entities = [User::class, Post::class], version = 3, autoMigrations = [ AutoMigration(from = 1, to = 2), // Adding nullable column AutoMigration( from = 2, to = 3, spec = AutoMigration2To3::class // Complex changes need spec ) ], exportSchema = true ) abstract class AppDatabase : RoomDatabase() { // DAOs... } // Migration spec for changes that need hints @RenameColumn(tableName = "users", fromColumnName = "name", toColumnName = "display_name") class AutoMigration2To3 : AutoMigrationSpec ``` Auto-migrations ต้องการ `exportSchema = true` เพื่อเปรียบเทียบเวอร์ชัน สำหรับการเปลี่ยนชื่อคอลัมน์ การเปลี่ยนชื่อตาราง หรือการลบ ให้ระบุ `AutoMigrationSpec` พร้อม annotations ที่เหมาะสม ## การสร้างโมเดล Entity Relations ด้วย Embedded Objects Room รองรับสาม relation patterns: embedded objects, one-to-many และ many-to-many ไม่เหมือนกับ ORMs Room คืนค่าข้อมูลแบบ flat—relations ต้องการ queries ที่ชัดเจน ### One-to-Many: User กับ Posts User หนึ่งคนสามารถมีหลาย posts กำหนด relation โดยใช้ annotation `@Relation` ใน data class ที่รวมทั้งสอง entities เข้าด้วยกัน ```kotlin // Post.kt @Entity( tableName = "posts", foreignKeys = [ ForeignKey( entity = User::class, parentColumns = ["id"], childColumns = ["user_id"], onDelete = ForeignKey.CASCADE // Delete posts when user deleted ) ], indices = [Index("user_id")] // Required for foreign key performance ) data class Post( @PrimaryKey(autoGenerate = true) val id: Long = 0, @ColumnInfo(name = "user_id") val userId: Long, val title: String, val content: String, @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis() ) // UserWithPosts.kt - Relation container data class UserWithPosts( @Embedded val user: User, @Relation( parentColumn = "id", entityColumn = "user_id" ) val posts: List ) // In UserDao.kt @Transaction // Ensures atomic read of user + posts @Query("SELECT * FROM users WHERE id = :userId") suspend fun getUserWithPosts(userId: Long): UserWithPosts? @Transaction @Query("SELECT * FROM users WHERE is_active = 1") fun observeActiveUsersWithPosts(): Flow> ``` Annotation `@Transaction` ป้องกันการอ่านที่ไม่สอดคล้องกันเมื่อ relation ครอบคลุมหลาย queries ภายใน ### Many-to-Many: Users และ Tags Relations แบบ many-to-many ต้องการตาราง junction (cross-reference) ที่เก็บคู่ของ foreign keys ```kotlin // Tag.kt @Entity(tableName = "tags") data class Tag( @PrimaryKey(autoGenerate = true) val tagId: Long = 0, val name: String ) // UserTagCrossRef.kt - Junction table @Entity( tableName = "user_tag_cross_ref", primaryKeys = ["userId", "tagId"], foreignKeys = [ ForeignKey(entity = User::class, parentColumns = ["id"], childColumns = ["userId"], onDelete = ForeignKey.CASCADE), ForeignKey(entity = Tag::class, parentColumns = ["tagId"], childColumns = ["tagId"], onDelete = ForeignKey.CASCADE) ] ) data class UserTagCrossRef( val userId: Long, val tagId: Long ) // UserWithTags.kt data class UserWithTags( @Embedded val user: User, @Relation( parentColumn = "id", entityColumn = "tagId", associateBy = Junction(UserTagCrossRef::class) ) val tags: List ) ``` Parameter `associateBy` ระบุตาราง junction ที่เชื่อมโยง users กับ tags ## การผสานรวม Room กับ ViewModel และ Repository สถาปัตยกรรมที่สะอาดแยก database layer ออกจาก UI Pattern repository ทำให้ data sources เป็น abstract ในขณะที่ ViewModel เปิดเผย state ให้กับ Compose UI ```kotlin // UserRepository.kt class UserRepository(private val userDao: UserDao) { val activeUsers: Flow> = userDao.observeActiveUsers() suspend fun createUser(email: String, displayName: String): Long { val user = User(email = email, displayName = displayName) return userDao.insertUser(user) } suspend fun deactivateUser(userId: Long) { userDao.getUserById(userId)?.let { user -> userDao.updateUser(user.copy(isActive = false)) } } } // UserViewModel.kt class UserViewModel( private val repository: UserRepository ) : ViewModel() { val users: StateFlow> = repository.activeUsers .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() ) fun createUser(email: String, name: String) { viewModelScope.launch { repository.createUser(email, name) // No manual refresh needed - Flow emits automatically } } } ``` Operator `stateIn` แปลง cold `Flow` จาก Room เป็น hot `StateFlow` ที่เหมาะสำหรับการ collect ใน Compose สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับ coroutines patterns ดูคู่มือเกี่ยวกับ [Kotlin Coroutines](/blog/android/mastering-kotlin-coroutines) > **การทดสอบ Room Databases** > > Room มี in-memory database builder สำหรับการทดสอบ: `Room.inMemoryDatabaseBuilder()` การทดสอบทำงานเร็วขึ้นและไม่เก็บข้อมูลระหว่างการรัน ทดสอบ migrations เสมอด้วย `MigrationTestHelper` จาก artifact `room-testing` ## Type Converters สำหรับ Types ที่ซับซ้อน Room เก็บเฉพาะ primitive types โดยธรรมชาติ Type converters แปลง types ที่ซับซ้อน (dates, enums, lists) เป็นรูปแบบที่สามารถจัดเก็บได้ ```kotlin // Converters.kt import androidx.room.TypeConverter import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset class Converters { // Instant <-> Long (milliseconds) @TypeConverter fun fromInstant(instant: Instant?): Long? = instant?.toEpochMilli() @TypeConverter fun toInstant(millis: Long?): Instant? = millis?.let { Instant.ofEpochMilli(it) } // LocalDate <-> String (ISO format) @TypeConverter fun fromLocalDate(date: LocalDate?): String? = date?.toString() @TypeConverter fun toLocalDate(dateStr: String?): LocalDate? = dateStr?.let { LocalDate.parse(it) } // List <-> JSON string @TypeConverter fun fromStringList(list: List?): String? = list?.joinToString(separator = ",") @TypeConverter fun toStringList(data: String?): List? = data?.split(",")?.filter { it.isNotBlank() } } // Register in database class @Database(...) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() ``` สำหรับการ serialize JSON ของ objects ที่ซับซ้อน พิจารณาใช้ [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) แทนการจัดการ string แบบ manual ## การจัดการ Database Operations ใน Coroutines Scope Room DAO operations ควรทำงานบน IO dispatcher แม้ว่า Room จะจัดการ threading ภายในสำหรับ suspend functions การจัดการ error ในระดับ repository ช่วยให้โค้ดมีความ robust ```kotlin // UserRepository.kt with error handling import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class UserRepository(private val userDao: UserDao) { suspend fun createUser(email: String, displayName: String): Result { return withContext(Dispatchers.IO) { try { val id = userDao.insertUser( User(email = email, displayName = displayName) ) Result.success(id) } catch (e: SQLiteConstraintException) { // Unique constraint violation (duplicate email) Result.failure(DuplicateEmailException(email)) } } } suspend fun deleteAllInactive(): Int = withContext(Dispatchers.IO) { userDao.deleteInactiveUsers() } } class DuplicateEmailException(email: String) : Exception("Email already exists: $email") ``` การห่อผลลัพธ์ใน `Result` ช่วยให้ ViewModels จัดการ errors ได้อย่างสวยงามโดยไม่ต้องใช้ boilerplate try-catch ## สรุป การสร้าง Room database layer ที่พร้อมใช้งาน production เกี่ยวข้องกับแนวทางปฏิบัติที่สำคัญหลายประการ: - ใช้ KSP แทน KAPT เพื่อการ compile ที่เร็วขึ้นกับ Room 2.6 - เพิ่ม indices บนคอลัมน์ที่ใช้ใน WHERE clauses และ JOIN conditions - คืนค่า Flow จาก DAOs สำหรับการอัปเดต UI อัตโนมัติเมื่อข้อมูลเปลี่ยนแปลง - เขียน migrations อย่างชัดเจนสำหรับการเปลี่ยนแปลง schema—อย่าใช้ destructive migration ใน production - ใช้ `@Transaction` สำหรับ relation queries เพื่อให้แน่ใจว่าการอ่านมีความสอดคล้องกัน - ห่อ database operations ใน Result สำหรับการจัดการ error ที่สะอาดใน ViewModels - ทดสอบ migrations ด้วย `MigrationTestHelper` ก่อนปล่อยอัปเดต ฝึกฝน patterns เหล่านี้ด้วย [คำถามสัมภาษณ์ Room database](/technologies/android/interview-questions/android-room-database) เพื่อเสริมความแข็งแกร่งให้กับแนวคิดเหล่านี้ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/android/room-database-android-2026-migrations-relations-coroutines