Room - 基本使用及使用问题

Room的基本使用

一、基本设置

  1. 依赖配置
kotlin 复制代码
// build.gradle.kts
dependencies {
    val roomVersion = "2.6.1"
    implementation("androidx.room:room-runtime:$roomVersion")
    implementation("androidx.room:room-ktx:$roomVersion")  // Kotlin扩展
    kapt("androidx.room:room-compiler:$roomVersion")      // Kotlin注解处理器
}
  1. 基本组件
kotlin 复制代码
// 1. 实体类(Entity)
@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true)
    val uid: Int = 0,
    
    @ColumnInfo(name = "first_name")
    val firstName: String,
    
    @ColumnInfo(name = "last_name")
    val lastName: String,
    
    val age: Int
)

// 2. DAO(数据访问对象)
@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAll(): Flow<List<User>>  // 使用Kotlin Flow
    
    @Insert
    suspend fun insert(user: User)   // 协程支持
    
    @Delete
    suspend fun delete(user: User)
    
    @Update
    suspend fun update(user: User)
    
    // 复杂查询示例
    @Query("SELECT * FROM users WHERE age > :minAge")
    fun getUsersOlderThan(minAge: Int): Flow<List<User>>
}

// 3. 数据库
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    
    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null
        
        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

二、Repository模式实现

kotlin 复制代码
// Repository层
class UserRepository(private val userDao: UserDao) {
    // 使用Flow获取所有用户
    val allUsers: Flow<List<User>> = userDao.getAll()
    
    // 使用协程插入用户
    suspend fun insert(user: User) {
        withContext(Dispatchers.IO) {
            userDao.insert(user)
        }
    }
    
    // 使用协程更新用户
    suspend fun update(user: User) {
        withContext(Dispatchers.IO) {
            userDao.update(user)
        }
    }
    
    // 按年龄查询用户
    fun getUsersOlderThan(age: Int): Flow<List<User>> = 
        userDao.getUsersOlderThan(age)
}

三、ViewModel实现

kotlin 复制代码
class UserViewModel(application: Application) : AndroidViewModel(application) {
    private val repository: UserRepository
    val allUsers: Flow<List<User>>
    
    init {
        val userDao = AppDatabase.getDatabase(application).userDao()
        repository = UserRepository(userDao)
        allUsers = repository.allUsers
    }
    
    // 使用viewModelScope处理协程
    fun insert(user: User) = viewModelScope.launch {
        repository.insert(user)
    }
    
    fun getUsersOlderThan(age: Int): Flow<List<User>> = 
        repository.getUsersOlderThan(age)
}

四、Activity/Fragment中使用

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    private val userViewModel: UserViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // 使用Flow收集数据
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                userViewModel.allUsers.collect { users ->
                    // 更新UI
                    updateUI(users)
                }
            }
        }
        
        // 添加用户示例
        binding.addButton.setOnClickListener {
            val user = User(
                firstName = "John",
                lastName = "Doe",
                age = 25
            )
            userViewModel.insert(user)
        }
    }
}

五、高级用法

  1. 数据库迁移
kotlin 复制代码
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(
            "ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0"
        )
    }
}

// 在数据库构建器中使用
Room.databaseBuilder(context, AppDatabase::class.java, "database-name")
    .addMigrations(MIGRATION_1_2)
    .build()
  1. 类型转换器
kotlin 复制代码
class DateConverter {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }
    
    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time
    }
}

// 在数据库中使用
@Database(entities = [User::class], version = 1)
@TypeConverters(DateConverter::class)
abstract class AppDatabase : RoomDatabase()
  1. 关系查询
kotlin 复制代码
// 一对多关系
@Entity
data class User(
    @PrimaryKey val userId: Int,
    val name: String
)

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = User::class,
            parentColumns = ["userId"],
            childColumns = ["userId"]
        )
    ]
)
data class Book(
    @PrimaryKey val bookId: Int,
    val userId: Int,
    val title: String
)

data class UserWithBooks(
    @Embedded val user: User,
    @Relation(
        parentColumn = "userId",
        entityColumn = "userId"
    )
    val books: List<Book>
)

@Dao
interface UserDao {
    @Transaction
    @Query("SELECT * FROM users")
    fun getUsersWithBooks(): Flow<List<UserWithBooks>>
}

让我用一个图书馆的例子来形象地解释这段代码。

一、实体关系解释

想象一个图书馆的场景:

scss 复制代码
图书馆会员(User)
    - 会员号(userId)
    - 会员名(name)

图书(Book)
    - 图书编号(bookId)
    - 借阅者会员号(userId)
    - 书名(title)

二、关系图解

erDiagram User ||--o{ Book : "借阅" User { int userId PK string name } Book { int bookId PK int userId FK string title }

三、具体例子

kotlin 复制代码
// 1. 用户表
val users = listOf(
    User(userId = 1, name = "张三"),
    User(userId = 2, name = "李四")
)

// 2. 图书表
val books = listOf(
    Book(bookId = 1, userId = 1, title = "Java编程"),
    Book(bookId = 2, userId = 1, title = "Kotlin入门"),
    Book(bookId = 3, userId = 2, title = "Android开发")
)

// 3. 关联查询结果
val usersWithBooks = listOf(
    UserWithBooks(
        user = User(userId = 1, name = "张三"),
        books = listOf(
            Book(bookId = 1, userId = 1, title = "Java编程"),
            Book(bookId = 2, userId = 1, title = "Kotlin入门")
        )
    ),
    UserWithBooks(
        user = User(userId = 2, name = "李四"),
        books = listOf(
            Book(bookId = 3, userId = 2, title = "Android开发")
        )
    )
)

四、注解说明

  1. @Entity 注解
kotlin 复制代码
@Entity
data class User(...)  // 相当于创建会员登记表

@Entity(foreignKeys = [...])
data class Book(...)  // 创建图书表,并与会员表建立关联
  1. @ForeignKey 注解
kotlin 复制代码
foreignKeys = [
    ForeignKey(
        entity = User::class,        // 关联到会员表
        parentColumns = ["userId"],  // 会员表的会员号
        childColumns = ["userId"]    // 图书表的借阅者会员号
    )
]

这就像图书馆的借书规则:

  • 书只能借给已注册的会员
  • 每本书都要记录借阅者的会员号
  1. @Embedded 和 @Relation 注解
kotlin 复制代码
data class UserWithBooks(
    @Embedded val user: User,        // 嵌入会员信息
    @Relation(
        parentColumn = "userId",     // 会员表的会员号
        entityColumn = "userId"      // 图书表的借阅者会员号
    )
    val books: List<Book>           // 该会员借的所有书
)

五、查询过程图解

graph TD A[开始查询] --> B[查询所有用户] B --> C[对每个用户查询其借阅的书] C --> D[组装UserWithBooks对象] D --> E[返回结果]

六、实际使用示例

kotlin 复制代码
class LibraryRepository(private val userDao: UserDao) {
    // 获取所有会员及其借阅的图书
    fun getAllUsersWithBooks(): Flow<List<UserWithBooks>> = 
        userDao.getUsersWithBooks()
}

// 在ViewModel中使用
class LibraryViewModel(private val repository: LibraryRepository) : ViewModel() {
    val usersWithBooks = repository.getAllUsersWithBooks()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

// 在UI中显示
@Composable
fun LibraryScreen(viewModel: LibraryViewModel) {
    val usersWithBooks by viewModel.usersWithBooks.collectAsState()
    
    LazyColumn {
        items(usersWithBooks) { userWithBooks ->
            Text("会员: ${userWithBooks.user.name}")
            userWithBooks.books.forEach { book ->
                Text("借阅的书: ${book.title}")
            }
        }
    }
}

七、输出示例

markdown 复制代码
会员: 张三
  - 借阅的书: Java编程
  - 借阅的书: Kotlin入门

会员: 李四
  - 借阅的书: Android开发

八、关键点总结

  1. 一对多关系
  • 一个用户可以借多本书
  • 一本书同时只能被一个用户借阅
  1. 外键约束
  • 确保图书表中的userId必须是用户表中存在的userId
  • 防止出现无效的借阅记录
  1. 关联查询
  • 自动处理表之间的关联
  • 返回结构化的数据
  1. 数据流
  • 使用Flow实现响应式查询
  • 数据变化时自动更新UI

这种关系模型在实际应用中非常常见,比如:

  • 订单系统(用户-订单)
  • 博客系统(作者-文章)
  • 教务系统(学生-课程)
  • 购物车系统(用户-商品)

通过这种方式,Room帮我们处理了复杂的数据关系,使得代码更加清晰和易于维护。

六、协程和Flow的使用

kotlin 复制代码
class UserRepository(private val userDao: UserDao) {
    // 使用Flow进行响应式编程
    val allUsers: Flow<List<User>> = userDao.getAll()
        .flowOn(Dispatchers.IO)        // 在IO线程执行数据库操作
        .catch { e -> Log.e("DB", "Error fetching users", e) }
    
    // 使用协程处理数据库操作
    suspend fun insertUser(user: User) = withContext(Dispatchers.IO) {
        try {
            userDao.insert(user)
        } catch (e: Exception) {
            Log.e("DB", "Error inserting user", e)
            throw e
        }
    }
    
    // 批量操作
    suspend fun insertUsers(users: List<User>) = withContext(Dispatchers.IO) {
        userDao.insertAll(users)
    }
}

七、性能优化

  1. 使用Paging 3
kotlin 复制代码
@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getPagingUsers(): PagingSource<Int, User>
}

class UserRepository(private val userDao: UserDao) {
    fun getPagedUsers(): Flow<PagingData<User>> {
        return Pager(
            config = PagingConfig(
                pageSize = 20,
                enablePlaceholders = true,
                maxSize = 100
            )
        ) {
            userDao.getPagingUsers()
        }.flow
    }
}
  1. 使用事务
kotlin 复制代码
@Dao
interface UserDao {
    @Transaction
    suspend fun updateUserAndBooks(user: User, books: List<Book>) {
        updateUser(user)
        deleteUserBooks(user.userId)
        insertBooks(books)
    }
}

八、测试

kotlin 复制代码
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
    private lateinit var db: AppDatabase
    private lateinit var userDao: UserDao
    
    @Before
    fun createDb() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
            .allowMainThreadQueries()
            .build()
        userDao = db.userDao()
    }
    
    @After
    fun closeDb() {
        db.close()
    }
    
    @Test
    fun insertAndGetUser() = runTest {
        val user = User(firstName = "Test", lastName = "User", age = 25)
        userDao.insert(user)
        
        val users = userDao.getAll().first()
        assertEquals(1, users.size)
        assertEquals("Test", users[0].firstName)
    }
}

九、最佳实践总结

  1. 架构建议
kotlin 复制代码
// 1. 使用单一数据源原则
class UserRepository(private val userDao: UserDao) {
    val users = userDao.getAll()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

// 2. 使用密封类处理状态
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

// 3. 使用扩展函数简化代码
fun AppDatabase.Companion.createDatabase(context: Context) =
    Room.databaseBuilder(context, AppDatabase::class.java, "database-name")
        .addMigrations(MIGRATION_1_2)
        .build()

Room的"ORM黑洞"问题

"ORM黑洞"是Room(以及其他ORM框架)中的一个常见性能问题,让我详细解释一下。

一、什么是"ORM黑洞"?

"ORM黑洞"主要指的是以下几个问题:

  1. N+1查询问题
kotlin 复制代码
// 假设有以下关系:一个用户有多本书
@Entity
data class User(
    @PrimaryKey val userId: Int,
    val name: String
)

@Entity
data class Book(
    @PrimaryKey val bookId: Int,
    val userId: Int,
    val title: String
)

// 错误示例:会导致N+1查询
@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAllUsers(): List<User>

    @Query("SELECT * FROM books WHERE userId = :userId")
    fun getBooksForUser(userId: Int): List<Book>
}

// 使用时:
val users = userDao.getAllUsers()           // 1次查询
users.forEach { user ->
    val books = userDao.getBooksForUser(user.userId)  // N次查询
}
  1. 过度映射
kotlin 复制代码
// 错误示例:获取全部字段但只使用部分字段
@Query("SELECT * FROM users")  // 获取所有字段
fun getAllUsers(): List<User>  // 实际上可能只需要name字段
  1. 级联加载
kotlin 复制代码
// 错误示例:过度的关系映射
data class UserWithBooksAndAuthors(
    @Embedded val user: User,
    @Relation(
        entity = Book::class,
        parentColumn = "userId",
        entityColumn = "userId"
    )
    val books: List<BookWithAuthor>  // 每本书还要加载作者信息
)

二、解决方案

  1. 使用联表查询替代N+1查询
kotlin 复制代码
// 好的做法:一次性查询所需数据
@Dao
interface UserDao {
    @Transaction
    @Query("""
        SELECT users.*, books.*
        FROM users
        LEFT JOIN books ON users.userId = books.userId
    """)
    fun getUsersWithBooks(): Flow<Map<User, List<Book>>>

    // 或者使用数据类封装
    data class UserWithBooks(
        @Embedded val user: User,
        @Relation(
            parentColumn = "userId",
            entityColumn = "userId"
        )
        val books: List<Book>
    )

    @Transaction
    @Query("SELECT * FROM users")
    fun getUsersWithBooks(): Flow<List<UserWithBooks>>
}
  1. 使用投影查询(只查询需要的字段)
kotlin 复制代码
// 好的做法:只查询需要的字段
data class UserNameTuple(
    @ColumnInfo(name = "name") val name: String
)

@Dao
interface UserDao {
    @Query("SELECT name FROM users")
    fun getUserNames(): Flow<List<UserNameTuple>>
}
  1. 使用分页加载
kotlin 复制代码
@Dao
interface UserDao {
    // 使用Paging 3
    @Query("SELECT * FROM users")
    fun getPagingUsers(): PagingSource<Int, User>
}

class UserRepository(private val userDao: UserDao) {
    fun getPagedUsers(): Flow<PagingData<User>> {
        return Pager(
            config = PagingConfig(
                pageSize = 20,
                enablePlaceholders = true
            )
        ) {
            userDao.getPagingUsers()
        }.flow
    }
}
  1. 优化关系查询
kotlin 复制代码
// 好的做法:按需加载关系数据
class UserRepository(private val userDao: UserDao) {
    // 基本信息和详细信息分开加载
    fun getUserBasicInfo(): Flow<List<User>> = userDao.getUsers()
    
    // 只在需要时才加载详细信息
    suspend fun getUserDetail(userId: Int): UserWithBooks {
        return userDao.getUserWithBooks(userId)
    }
}
  1. 使用缓存策略
kotlin 复制代码
class UserRepository(
    private val userDao: UserDao,
    private val userCache: UserCache
) {
    fun getUsers(): Flow<List<User>> = flow {
        // 先从缓存获取
        val cachedUsers = userCache.getUsers()
        if (cachedUsers.isNotEmpty()) {
            emit(cachedUsers)
        }
        
        // 再从数据库获取最新数据
        userDao.getUsers()
            .collect { users ->
                userCache.saveUsers(users)
                emit(users)
            }
    }
}
  1. 使用预加载
kotlin 复制代码
class UserViewModel(private val repository: UserRepository) : ViewModel() {
    init {
        // 预加载常用数据
        viewModelScope.launch {
            repository.preloadFrequentlyUsedData()
        }
    }
}

三、性能优化建议

  1. 使用索引优化查询
kotlin 复制代码
@Entity(
    indices = [
        Index(value = ["name"]),
        Index(value = ["userId", "type"], unique = true)
    ]
)
data class Book(
    @PrimaryKey val bookId: Int,
    val userId: Int,
    val name: String,
    val type: String
)
  1. 批量操作优化
kotlin 复制代码
@Dao
interface UserDao {
    @Transaction
    suspend fun updateUsersInBatch(users: List<User>) {
        users.chunked(100).forEach { batch ->
            updateUsers(batch)
        }
    }
}
  1. 异步操作优化
kotlin 复制代码
class UserRepository(private val userDao: UserDao) {
    // 使用Flow进行异步操作
    val users: Flow<List<User>> = userDao.getUsers()
        .flowOn(Dispatchers.IO)
        .distinctUntilChanged()
        .shareIn(
            scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
            started = SharingStarted.WhileSubscribed(5000),
            replay = 1
        )
}
  1. 内存优化
kotlin 复制代码
// 使用AutoClearedValue防止内存泄漏
class UserListFragment : Fragment() {
    private var binding by AutoClearedValue<FragmentUserListBinding>()
    
    // 使用viewModels()委托
    private val viewModel: UserViewModel by viewModels()
}

四、监控和调试

  1. SQL查询日志
kotlin 复制代码
Room.databaseBuilder(context, AppDatabase::class.java, "database-name")
    .setQueryCallback({ sqlQuery, bindArgs ->
        Log.d("Room", "SQL Query: $sqlQuery SQL Args: $bindArgs")
    }, Executors.newSingleThreadExecutor())
    .build()
  1. 性能监控
kotlin 复制代码
class DatabaseMetrics {
    private val queryDurations = mutableMapOf<String, Long>()
    
    fun trackQueryPerformance(queryName: String, duration: Long) {
        queryDurations[queryName] = duration
    }
    
    fun getSlowQueries(): List<Pair<String, Long>> {
        return queryDurations.filter { it.value > 16L }  // 16ms阈值
            .toList()
            .sortedByDescending { it.second }
    }
}

通过以上优化措施,可以有效避免"ORM黑洞"问题,提高应用性能。关键是:

  1. 避免N+1查询
  2. 只查询需要的数据
  3. 使用适当的缓存策略
  4. 实施分页加载
  5. 优化查询性能
  6. 监控和调试查询性能
相关推荐
一个 00 后的码农1 小时前
25环境工程研究生复试面试问题汇总 环境工程专业知识问题很全! 环境工程复试全流程攻略 环境工程考研复试调剂真题汇总
考研·面试·面试问题·复试调剂·面试真题·环境工程复试·考研资料
刘小炮吖i1 小时前
【面试】Java 之 String 系列 -- String 为什么不可变?
java·开发语言·面试
二川bro2 小时前
样式垂直居中,谁才是王者
前端·面试
XuanRanDev3 小时前
【构建工具】Gradle Kotlin DSL中的大小写陷阱:BuildConfigField
android·开发语言·kotlin
可可鸭~5 小时前
前端面试基础知识整理(一)
javascript·vue.js·学习·面试·elementui
一个 00 后的码农5 小时前
25风景园林研究生复试面试问题汇总 风景园林专业知识问题很全! 风景园林复试全流程攻略 风景园林考研复试调剂真题汇总
考研·面试·面试问题·复试调剂·面试真题·风景园林复试·复试面试
六个点5 小时前
路由hash和history的实现
前端·javascript·面试
傻小胖7 小时前
面试葵花宝典之React(持续更新中)
javascript·react.js·面试
攻城狮_Dream8 小时前
基于 Python 的项目管理系统开发
android·数据库·python