Room的基本使用
一、基本设置
- 依赖配置
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注解处理器
}
- 基本组件
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)
}
}
}
五、高级用法
- 数据库迁移
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()
- 类型转换器
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()
- 关系查询
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开发")
)
)
)
四、注解说明
- @Entity 注解
kotlin
@Entity
data class User(...) // 相当于创建会员登记表
@Entity(foreignKeys = [...])
data class Book(...) // 创建图书表,并与会员表建立关联
- @ForeignKey 注解
kotlin
foreignKeys = [
ForeignKey(
entity = User::class, // 关联到会员表
parentColumns = ["userId"], // 会员表的会员号
childColumns = ["userId"] // 图书表的借阅者会员号
)
]
这就像图书馆的借书规则:
- 书只能借给已注册的会员
- 每本书都要记录借阅者的会员号
- @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开发
八、关键点总结
- 一对多关系
- 一个用户可以借多本书
- 一本书同时只能被一个用户借阅
- 外键约束
- 确保图书表中的userId必须是用户表中存在的userId
- 防止出现无效的借阅记录
- 关联查询
- 自动处理表之间的关联
- 返回结构化的数据
- 数据流
- 使用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)
}
}
七、性能优化
- 使用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
}
}
- 使用事务
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)
}
}
九、最佳实践总结
- 架构建议
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黑洞"主要指的是以下几个问题:
- 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次查询
}
- 过度映射
kotlin
// 错误示例:获取全部字段但只使用部分字段
@Query("SELECT * FROM users") // 获取所有字段
fun getAllUsers(): List<User> // 实际上可能只需要name字段
- 级联加载
kotlin
// 错误示例:过度的关系映射
data class UserWithBooksAndAuthors(
@Embedded val user: User,
@Relation(
entity = Book::class,
parentColumn = "userId",
entityColumn = "userId"
)
val books: List<BookWithAuthor> // 每本书还要加载作者信息
)
二、解决方案
- 使用联表查询替代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>>
}
- 使用投影查询(只查询需要的字段)
kotlin
// 好的做法:只查询需要的字段
data class UserNameTuple(
@ColumnInfo(name = "name") val name: String
)
@Dao
interface UserDao {
@Query("SELECT name FROM users")
fun getUserNames(): Flow<List<UserNameTuple>>
}
- 使用分页加载
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
}
}
- 优化关系查询
kotlin
// 好的做法:按需加载关系数据
class UserRepository(private val userDao: UserDao) {
// 基本信息和详细信息分开加载
fun getUserBasicInfo(): Flow<List<User>> = userDao.getUsers()
// 只在需要时才加载详细信息
suspend fun getUserDetail(userId: Int): UserWithBooks {
return userDao.getUserWithBooks(userId)
}
}
- 使用缓存策略
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)
}
}
}
- 使用预加载
kotlin
class UserViewModel(private val repository: UserRepository) : ViewModel() {
init {
// 预加载常用数据
viewModelScope.launch {
repository.preloadFrequentlyUsedData()
}
}
}
三、性能优化建议
- 使用索引优化查询
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
)
- 批量操作优化
kotlin
@Dao
interface UserDao {
@Transaction
suspend fun updateUsersInBatch(users: List<User>) {
users.chunked(100).forEach { batch ->
updateUsers(batch)
}
}
}
- 异步操作优化
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
)
}
- 内存优化
kotlin
// 使用AutoClearedValue防止内存泄漏
class UserListFragment : Fragment() {
private var binding by AutoClearedValue<FragmentUserListBinding>()
// 使用viewModels()委托
private val viewModel: UserViewModel by viewModels()
}
四、监控和调试
- 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()
- 性能监控
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黑洞"问题,提高应用性能。关键是:
- 避免N+1查询
- 只查询需要的数据
- 使用适当的缓存策略
- 实施分页加载
- 优化查询性能
- 监控和调试查询性能