Room Database ใน Android 2026: Migrations, Relations และ Coroutines

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

Room Database Android Kotlin SQLite data persistence

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 ที่เร็วขึ้น

build.gradle.kts (Module :app)kotlin
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 บ่อยช่วยปรับปรุงประสิทธิภาพการอ่านแลกกับการเขียนที่ช้าลงเล็กน้อย

User.ktkotlin
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

UserDao.ktkotlin
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

AppDatabase.ktkotlin
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

Migrations.ktkotlin
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 สำหรับการอัปเดตที่ตรงไปตรงมา

AppDatabase.ktkotlin
@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 ที่เหมาะสม

พร้อมที่จะพิชิตการสัมภาษณ์ 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 เข้าด้วยกัน

Post.ktkotlin
@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

Tag.ktkotlin
@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

UserRepository.ktkotlin
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 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) เป็นรูปแบบที่สามารถจัดเก็บได้

Converters.ktkotlin
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

UserRepository.kt with error handlingkotlin
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

เขียนโดย

Anthony Fillion-Maillet

นักพัฒนาฟูลสแตก ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 10 กรกฎาคม 2569

แท็ก

#android
#room
#kotlin
#sqlite
#coroutines
#database

แชร์

บทความที่เกี่ยวข้อง