23、多数据库管理:多Database实例、跨库查询、数据库别名、动态数据库
说实话,多数据库这个话题,我当年刚接触Room的时候也觉得有点遥远。心想一个App能有一个数据库就不错了,搞那么多干嘛?直到我在一个企业级项目里,遇到了用户数据、日志数据、缓存数据混在一起的情况……那叫一个乱。后来我不得不把一个大库拆成多个小库,才真正体会到多数据库管理的价值。
今天我们就来聊聊,Room里怎么玩转多个数据库。说白了,就是怎么同时管理多个Database实例,怎么跨库查数据,怎么给数据库起别名,以及怎么动态创建数据库。
23.1 为什么需要多个数据库?
你可能会问:一个数据库不够用吗?嗯,大多数场景下确实够。但有些情况,拆开更清爽:
- 数据隔离:用户隐私数据和普通业务数据分开存储
- 生命周期不同:缓存数据可以随时清,但核心数据不能丢
- 多用户切换:每个用户一个数据库,切换时直接换库
- 模块化开发:不同功能模块维护自己的数据库
我在项目中遇到过最典型的场景,就是做IM聊天App。聊天记录数据量巨大,如果和用户配置信息放在同一个库里,每次备份恢复都特别慢。后来我把聊天记录单独拆成一个库,用户配置一个库,体验好了很多。
23.2 多Database实例的创建与管理
Room里创建多个数据库实例,其实很简单。就是分别定义不同的Database类,然后各自调用Room.databaseBuilder()。
但这里有个坑:每个Database实例都是独立的,它们各自管理自己的表,互不干扰。你想想看,这就像你家里有两个冰箱,一个放蔬菜,一个放饮料,各管各的。
23.2.1 定义两个Database
// 用户数据库
@Database(entities = [User::class], version = 1)
abstract class UserDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// 日志数据库
@Database(entities = [LogEntry::class], version = 1)
abstract class LogDatabase : RoomDatabase() {
abstract fun logDao(): LogDao
}
23.2.2 创建实例
val userDb = Room.databaseBuilder(
context.applicationContext,
UserDatabase::class.java,
"user_db.db" // 数据库文件名
).build()
val logDb = Room.databaseBuilder(
context.applicationContext,
LogDatabase::class.java,
"log_db.db"
).build()
23.3 跨库查询:怎么查两个库的数据?
跨库查询,说白了就是你想同时从两个数据库里拿数据。但Room本身不支持跨库的SQL查询,因为每个数据库是独立的SQLite文件。
那怎么办?我常用的做法有两种:
23.3.1 应用层合并
先查第一个库,再查第二个库,然后在代码里把结果合并。这是最直接的方法。
// 分别查询两个库
val users = userDb.userDao().getAllUsers()
val logs = logDb.logDao().getRecentLogs()
// 在应用层合并
data class UserWithLog(
val user: User,
val recentLogs: List<LogEntry>
)
// 手动关联
val result = users.map { user ->
UserWithLog(
user = user,
recentLogs = logs.filter { it.userId == user.id }
)
}
嗯,这里要注意:如果数据量很大,这种合并方式可能会有点慢。我建议在查询时就加好条件,尽量减少返回的数据量。
23.3.2 使用Room的@RawQuery
如果你真的需要在一个查询里跨库,可以用@RawQuery配合ATTACH DATABASE。这是SQLite本身支持的功能。
// 先附加另一个数据库
val supportSQLite = userDb.openHelper.writableDatabase
supportSQLite.execSQL("ATTACH DATABASE 'log_db.db' AS log_db")
// 然后就可以跨库查询了
@Dao
interface CrossDao {
@RawQuery(observedEntities = [User::class, LogEntry::class])
fun getUsersWithLogs(query: SupportSQLiteQuery): List<UserWithLog>
}
// 构建查询
val query = SimpleSQLiteQuery("""
SELECT u.*, l.*
FROM user_table u
LEFT JOIN log_db.log_entry l ON u.id = l.user_id
""")
val result = crossDao.getUsersWithLogs(query)
23.4 数据库别名:让代码更清晰
数据库别名,说白了就是给数据库文件起个外号。在Room里,这个别名就是你在databaseBuilder里指定的文件名。
但别小看这个命名。我见过有人把所有数据库都叫my_database.db,结果多个库混在一起,根本分不清哪个是哪个。
23.4.1 命名规范建议
| 数据库用途 | 推荐文件名 | 说明 |
|---|---|---|
| 用户数据 | user_data.db | 存储用户配置、个人信息 |
| 缓存数据 | cache_data.db | 可随时清除 |
| 日志数据 | log_data.db | 按时间分库 |
| 多用户 | user_{userId}.db | 每个用户独立 |
我个人习惯用模块名_用途.db的格式,比如chat_message.db、settings_config.db。这样一看文件名就知道是干什么的。
23.5 动态数据库:运行时创建和切换
动态数据库,就是你在App运行过程中,根据需要创建新的数据库,或者切换到不同的数据库。这个功能在多用户场景下特别实用。
23.5.1 动态创建数据库
class DatabaseManager(private val context: Context) {
private val databases = mutableMapOf<String, RoomDatabase>()
fun getDatabase(userId: String): UserDatabase {
val dbName = "user_${userId}.db"
// 如果已经创建过,直接返回
if (databases.containsKey(dbName)) {
return databases[dbName] as UserDatabase
}
// 动态创建新的数据库
val db = Room.databaseBuilder(
context.applicationContext,
UserDatabase::class.java,
dbName
).build()
databases[dbName] = db
return db
}
fun closeDatabase(userId: String) {
val dbName = "user_${userId}.db"
databases[dbName]?.close()
databases.remove(dbName)
}
}
23.5.2 动态切换数据库
切换数据库,说白了就是换一个Database实例。你可以用一个全局变量来记录当前使用的数据库。
class UserScopeDatabase(private val context: Context) {
private var currentDb: UserDatabase? = null
private var currentUserId: String? = null
fun switchToUser(userId: String) {
// 关闭旧的数据库
currentDb?.close()
// 创建新的数据库
currentDb = Room.databaseBuilder(
context.applicationContext,
UserDatabase::class.java,
"user_${userId}.db"
).build()
currentUserId = userId
}
fun getUserDao(): UserDao {
return currentDb?.userDao()
?: throw IllegalStateException("请先调用 switchToUser()")
}
}
23.6 多数据库架构图
下面这张图,是我自己总结的多数据库管理核心逻辑。你看一眼就能明白整体结构。
23.7 避坑指南与最佳实践
多数据库管理看着简单,但实际用起来有不少坑。我把自己踩过的坑总结一下:
- 数据库文件冲突:两个数据库用了同一个文件名,导致数据覆盖。后来我强制要求每个数据库文件名必须唯一。
- 线程安全问题:多个线程同时创建同一个数据库实例,导致崩溃。解决方案是用
ConcurrentHashMap加同步锁。 - 忘记关闭数据库:动态创建的数据库没有及时关闭,内存占用越来越高。我后来在
onDestroy或用户退出时统一清理。
- 用
Room.inMemoryDatabaseBuilder创建临时数据库,用完自动释放 - 数据库版本号统一管理,不要每个库各自为政
- 跨库查询尽量在应用层做,少用ATTACH,避免复杂性
- 多数据库的Migration也要分别处理,别漏了
好了,多数据库管理这块就聊这么多。说白了就是:拆库要合理,管理要规范,关闭要及时。你想想看,如果能把数据库拆得清清楚楚,后期维护起来会轻松很多。
公众号:蓝海资料掘金营,微信deep3321