Room Database ใน Android 2026: Migrations, Relations และ Coroutines
เชี่ยวชาญ Room database ใน Android พร้อม migrations, entity relations, Kotlin coroutines และ Flow บทเรียนที่ครบถ้วนพร้อม patterns สำหรับ production ในปี 2026

Room database มอบ abstraction layer ที่แข็งแกร่งเหนือ SQLite ใน Android โดยจัดการ boilerplate code ที่ทำให้การใช้ SQLite โดยตรงเป็นเรื่องน่าเบื่อ ด้วย Room 2.6 และ Kotlin coroutines การสร้าง database layers ที่เป็น reactive และ type-safe กลายเป็นเรื่องง่ายขึ้นอย่างมาก
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 ที่เร็วขึ้น
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 บ่อยช่วยปรับปรุงประสิทธิภาพการอ่านแลกกับการเขียนที่ช้าลงเล็กน้อย
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
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<User>)
// Flow emits new data whenever the table changes
@Query("SELECT * FROM users WHERE is_active = 1 ORDER BY created_at DESC")
fun observeActiveUsers(): Flow<List<User>>
// 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
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 ในการทดสอบ
อย่าใช้ fallbackToDestructiveMigration() ใน production เด็ดขาด สิ่งนี้จะลบข้อมูลผู้ใช้ทั้งหมดเมื่อ schema เปลี่ยนแปลง ส่วนถัดไปจะกล่าวถึงการจัดการ migration อย่างถูกต้อง
การเขียน Database Migrations ที่ปลอดภัย
Migrations รักษาข้อมูลผู้ใช้เมื่อ schema เปลี่ยนแปลงระหว่างเวอร์ชันของแอป Room ตรวจสอบ migrations ในเวลา runtime โดยเปรียบเทียบ schema ที่คาดหวังกับ database ที่ถูก migrate แล้ว
Migration กำหนดคำสั่ง SQL ที่จำเป็นในการแปลง database จากเวอร์ชัน N เป็นเวอร์ชัน N+1
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 สำหรับการอัปเดตที่ตรงไปตรงมา
@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 : AutoMigrationSpecAuto-migrations ต้องการ exportSchema = true เพื่อเปรียบเทียบเวอร์ชัน สำหรับการเปลี่ยนชื่อคอลัมน์ การเปลี่ยนชื่อตาราง หรือการลบ ให้ระบุ AutoMigrationSpec พร้อม annotations ที่เหมาะสม
พร้อมที่จะพิชิตการสัมภาษณ์ Android แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
การสร้างโมเดล 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 เข้าด้วยกัน
@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<Post>
)
// 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<List<UserWithPosts>>Annotation @Transaction ป้องกันการอ่านที่ไม่สอดคล้องกันเมื่อ relation ครอบคลุมหลาย queries ภายใน
Many-to-Many: Users และ Tags
Relations แบบ many-to-many ต้องการตาราง junction (cross-reference) ที่เก็บคู่ของ foreign keys
@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<Tag>
)Parameter associateBy ระบุตาราง junction ที่เชื่อมโยง users กับ tags
การผสานรวม Room กับ ViewModel และ Repository
สถาปัตยกรรมที่สะอาดแยก database layer ออกจาก UI Pattern repository ทำให้ data sources เป็น abstract ในขณะที่ ViewModel เปิดเผย state ให้กับ Compose UI
class UserRepository(private val userDao: UserDao) {
val activeUsers: Flow<List<User>> = 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<List<User>> = 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
Room มี in-memory database builder สำหรับการทดสอบ: Room.inMemoryDatabaseBuilder() การทดสอบทำงานเร็วขึ้นและไม่เก็บข้อมูลระหว่างการรัน ทดสอบ migrations เสมอด้วย MigrationTestHelper จาก artifact room-testing
Type Converters สำหรับ Types ที่ซับซ้อน
Room เก็บเฉพาะ primitive types โดยธรรมชาติ Type converters แปลง types ที่ซับซ้อน (dates, enums, lists) เป็นรูปแบบที่สามารถจัดเก็บได้
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<String> <-> JSON string
@TypeConverter
fun fromStringList(list: List<String>?): String? =
list?.joinToString(separator = ",")
@TypeConverter
fun toStringList(data: String?): List<String>? =
data?.split(",")?.filter { it.isNotBlank() }
}
// Register in database class
@Database(...)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()สำหรับการ serialize JSON ของ objects ที่ซับซ้อน พิจารณาใช้ kotlinx.serialization แทนการจัดการ string แบบ manual
การจัดการ Database Operations ใน Coroutines Scope
Room DAO operations ควรทำงานบน IO dispatcher แม้ว่า Room จะจัดการ threading ภายในสำหรับ suspend functions การจัดการ error ในระดับ repository ช่วยให้โค้ดมีความ robust
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class UserRepository(private val userDao: UserDao) {
suspend fun createUser(email: String, displayName: String): Result<Long> {
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<T> ช่วยให้ 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 เพื่อเสริมความแข็งแกร่งให้กับแนวคิดเหล่านี้
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

เขียนโดย
Anthony Fillion-Mailletนักพัฒนาฟูลสแตก ผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 10 กรกฎาคม 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

Kotlin Flow vs StateFlow vs SharedFlow: คำถามสัมภาษณ์ Android ปี 2026
คำถาม Kotlin Flow vs StateFlow vs SharedFlow ที่ผู้สัมภาษณ์ Android ถามในปี 2026 พร้อมคำตอบชัดเจน ตารางเปรียบเทียบ และโค้ดที่พร้อมใช้งานจริง

Kotlin 2.3 สำหรับ Android: Name-Based Destructuring, KMP และคำถามสัมภาษณ์ 2026
คำถามสัมภาษณ์ Kotlin 2.3 สำหรับนักพัฒนา Android ในปี 2026 ครอบคลุม name-based destructuring, KMP, context parameters, Flow และ coroutines พร้อมตัวอย่างโค้ด

Kotlin Coroutines สำหรับ Android: คู่มือฉบับสมบูรณ์ 2026
คู่มือครบถ้วนเกี่ยวกับ Kotlin coroutines ในการพัฒนา Android: suspend functions, scopes, dispatchers และ patterns ขั้นสูง