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)
⚠️ 注意:使用ATTACH DATABASE时,要确保被附加的数据库没有被其他连接占用。我曾经踩过这个坑,两个线程同时操作同一个数据库文件,结果崩溃了。建议加锁或者用单线程访问。

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.dbsettings_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 多数据库架构图

下面这张图,是我自己总结的多数据库管理核心逻辑。你看一眼就能明白整体结构。

多数据库管理核心架构 Android 应用层 Repository / ViewModel / Activity DatabaseManager(数据库管理器) 单例模式,管理所有数据库实例 UserDatabase user_data.db 用户配置、个人信息 LogDatabase log_data.db 操作日志、错误记录 CacheDatabase cache_data.db 临时缓存、网络数据 SQLite 底层存储 每个数据库对应一个独立的 .db 文件

23.7 避坑指南与最佳实践

多数据库管理看着简单,但实际用起来有不少坑。我把自己踩过的坑总结一下:

⚠️ 我曾经踩过的坑:
  • 数据库文件冲突:两个数据库用了同一个文件名,导致数据覆盖。后来我强制要求每个数据库文件名必须唯一。
  • 线程安全问题:多个线程同时创建同一个数据库实例,导致崩溃。解决方案是用ConcurrentHashMap加同步锁。
  • 忘记关闭数据库:动态创建的数据库没有及时关闭,内存占用越来越高。我后来在onDestroy或用户退出时统一清理。
💡 我的最佳实践:
  • Room.inMemoryDatabaseBuilder创建临时数据库,用完自动释放
  • 数据库版本号统一管理,不要每个库各自为政
  • 跨库查询尽量在应用层做,少用ATTACH,避免复杂性
  • 多数据库的Migration也要分别处理,别漏了

好了,多数据库管理这块就聊这么多。说白了就是:拆库要合理,管理要规范,关闭要及时。你想想看,如果能把数据库拆得清清楚楚,后期维护起来会轻松很多。


公众号:蓝海资料掘金营,微信deep3321