RoomDB - 你能连接来自不同数据库的表吗

RoomDB - Can you JOIN tables from different databases

我想知道 RoomDB 是否支持来自 不同 数据库的两个 table 的连接。

假设我有以下实体和数据库:

@Entity
data class Foo(
    @PrimaryKey
    val fooId: Long,
    val barId: Long,
    val fooText: String
)

@Dao
interface FooDao {
    ....
}

@Database(
    entities = [Foo::class],
    version = 1,
    exportSchema = false
)
abstract class FooDatabase : RoomDatabase() {
    abstract fun fooDao() : FooDao
}

val fooDatabase = Room
    .databaseBuilder(application, FooDatabase::class.java, "Foo.db")
    .fallbackToDestructiveMigration()
    .build()

@Entity
data class Bar(
    @PrimaryKey
    val id: Long,
    val barText: String
)

@Dao
interface BarDao {
    ....
}

@Database(
    entities = [Bar::class],
    version = 1,
    exportSchema = false
)
abstract class BarDatabase : RoomDatabase() {
    abstract fun barDao() : BarDao
}

val barDatabase = Room
    .databaseBuilder(application, BarDatabase::class.java, "Bar.db")
    .fallbackToDestructiveMigration()
    .build()

data class FooWithBar(
    @Embedded
    val foo: Foo,
    @Relation(
        parentColumn = "barId",
        entityColumn = "id"
    )
    val bar: Bar
)

如果 Foo table 与 Bar [=] 驻留在不同的数据库中,是否可以编写查询以获取连接模型 FooWithBar 41=]?

我知道如果我在同一数据库中同时拥有 FooBar 实体,我可以在 dao 中编写一个查询,如:

@Query("SELECT * FROM foo")
suspend fun getFooWithBar() : FooBar?

并且编译器会根据注释生成一个 SQL 查询,该查询将通过 Foo.barId -> Bar.id 关系.

但我不知道是否可以在不同数据库中跨 table 进行这样的连接。

我知道如果我将这些 table 托管在同一个数据库中,我可以实现这一点,但我想保持我的数据库独立。

我想保持我的数据库独立的事实是否表明有“气味”?

是否依赖领域模型?如果是这样,什么时候将域模型拆分为不同的数据库有哪些好的经验法则?

Does the fact I want to keep my db's seperate indicate a "smell"?

是的,尤其是 Room。它引入了复杂性和低效率。

Is it possible to write a query where I can get the join model FooWithBar if the Foo table resides in a different database than the Bar table?

对于您的简单示例是的,但从示例中可以看出没有实际的 SQL JOIN(没有附加),即您获取 Foo 对象并通过获取适当的 Bar 来模拟 JOIN (或酒吧)。

如果您尝试混合来自不同数据库的实体,那么您将遇到问题,例如

即使添加(编辑)Dao 没有问题,例如:-

@Query("SELECT * FROM foo JOIN bar ON foo.barId = bar.id")
fun getAllFooWithBar(): List<FooWithBar>

根据(来自 Android Studio 的屏幕截图)看起来不错:-

当你编译时你会得到这样的错误:-

E:\AndroidStudioApps\SO67981906KotlinRoomDate\app\build\tmp\kapt3\stubs\debug\a\a\so67981906kotlinroomdate\FooDao.java:22: error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such table: bar)
    public abstract void getAllFooWithBar();
                         ^E:\AndroidStudioApps\SO67981906KotlinRoomDate\app\build\tmp\kapt3\stubs\debug\a\a\so67981906kotlinroomdate\FooDao.java:22: error: Not sure how to convert a Cursor to this method's return type (void).
    public abstract void getAllFooWithBar();

在单个查询中获取需要来自两个数据库的表的任何内容将超出 Room 的范围,因为每个数据库只知道它自己的表。

但是,如果您将一个数据库附加到另一个数据库,那么您将在一个数据库中拥有两个数据库,但房间不理解它。所以你基本上必须恢复使用 SupportSQLiteDatabase(类似于使用原生 Android SQlite(但有一些限制))。

例子(很简单)

Foo实体

@Entity
data class Foo(
    @PrimaryKey
    val fooId: Long?,
    val barId: Long,
    val fooText: String
)
  • 基本相同

FooDao

@Dao
interface FooDao {
    @Insert
    fun insert(foo: Foo): Long
    @Query("SELECT * FROM foo")
    fun getAllFoos(): List<Foo>
    @Query("SELECT * FROM foo WHERE fooId=:fooId")
    fun getFooById(fooId: Long): Foo

    /* !!!!NO GO!!!!
    @Query("SELECT * FROM foo JOIN bar ON foo.barId = bar.id")
    fun getAllFooWithBar(): List<FooWithBar>
     */
}
  • 一些简单的道

FooDatabase

@Database(
    entities = [Foo::class],
    version = 1,
    exportSchema = false
)
abstract class FooDatabase : RoomDatabase() {
    abstract fun fooDao() : FooDao


    fun attachBar(context: Context): Boolean {
        var rv: Boolean = false
        if (instance != null) {
            val dbs = this.openHelper?.writableDatabase
            val barpath = context.getDatabasePath(BarDatabase.DBNAME)
            if (dbs != null) {
                dbs.execSQL("ATTACH DATABASE '$barpath' AS $BAR_SCHEMA_NAME")
                rv = true
            }
        }
        return rv
    }

    fun closeInstance() {
        if(instance == null) return
        if (this.isOpen()) {
            this.close()
        }
        instance = null
    }

    companion object {

        @Volatile
        private var instance: FooDatabase? = null
        fun getInstanceWithForceOption(context: Context, forceReopen: Boolean = false): FooDatabase {
            if (forceReopen) instance?.closeInstance()
            if (instance == null) {
                instance = Room.databaseBuilder(context,FooDatabase::class.java, DBNAME)
                    .allowMainThreadQueries()
                    .addCallback(FOO_CALLBACK)
                    .build()
            }
            return instance as FooDatabase
        }

        fun getInstance(context: Context): FooDatabase {
            return getInstanceWithForceOption(context, false)
        }

        val FOO_CALLBACK = object: RoomDatabase.Callback() {
            override fun onOpen(db: SupportSQLiteDatabase) {
                super.onOpen(db)
            }
            override fun onCreate(db: SupportSQLiteDatabase) {
                super.onCreate(db)
            }
        }
        const val DBNAME: String = "foo.db"
        const val BAR_SCHEMA_NAME = "bar_schema"
    }
}
  • 未使用回调,但如果始终通过附加访问,则可以在 onOpen 中完成附加

实体

@Entity
data class Bar(
    @PrimaryKey
    val id: Long?,
    val barText: String
)
  • 基本相同

BarDao

@Dao
interface BarDao {

    @Insert
    fun insert(bar: Bar): Long
    @Query("SELECT * FROM bar")
    fun getAllBars(): List<Bar>
    @Query("SELECT * FROM Bar WHERE id=:id")
    fun getBarById(id: Long): Bar
}

条形数据库

@Database(
    entities = [Bar::class],
    version = 1,
    exportSchema = false
)
abstract class BarDatabase : RoomDatabase() {
    abstract fun barDao() : BarDao

    fun closeInstance() {
        if (this.isOpen()) {
            this.close()
        }
        instance = null
    }

    companion object {
        @Volatile
        private var instance: BarDatabase? = null
        fun getInstanceWithForceOption(context: Context, forceReopen: Boolean = false): BarDatabase {
            if (forceReopen) instance?.closeInstance()
            if (instance == null) {
                instance = Room.databaseBuilder(context,BarDatabase::class.java, DBNAME)
                    .allowMainThreadQueries()
                    .addCallback(BAR_CALLBACK)
                    .build()
            }
            return instance as BarDatabase
        }

        fun getInstance(context: Context): BarDatabase {
            return getInstanceWithForceOption(context, false)
        }

        val BAR_CALLBACK = object: RoomDatabase.Callback() {
            override fun onOpen(db: SupportSQLiteDatabase) {
                super.onOpen(db)
            }
            override fun onCreate(db: SupportSQLiteDatabase) {
                super.onCreate(db)
            }
        }
        const val DBNAME: String = "bar.db"
    }
}
  • 再次回调什么都不做

FooWithBar

class FooWithBar {
    
    var foo: Foo
    var bar: Bar

    constructor(fooId: Long, fooDao: FooDao, barDao: BarDao) {
        this.foo = fooDao.getFooById(fooId)
        this.bar = barDao.getBarById(foo.barId)
    }
}
  • 由于您不能同时获得 Foo 和 Bar,这相当于通过 FooDatabase 获取 Foo 然后通过 BarDatabase 获取关联的 Bar 进行连接。

MainActivity 放在一起 :-

class MainActivity : AppCompatActivity() {

    lateinit var foodb: FooDatabase
    lateinit var fooDao: FooDao
    lateinit var bardb: BarDatabase
    lateinit var barDao: BarDao
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        foodb = FooDatabase.getInstance(this)
        fooDao = foodb.fooDao()
        bardb = BarDatabase.getInstance(this)
        barDao = bardb.barDao()

        /* Add some data */
        fooDao.insert(Foo(null,barDao.insert(Bar(null,"BAR1")),"FOO1"))
        barDao.insert(Bar(null,"BAR UNUSED"))
        fooDao.insert(Foo(null,barDao.insert(Bar(null,"BAR2")),"FOO2"))

        /* Get equivalent of join (simple) using the FooWithBar */
        val allFoosWithBars = mutableListOf<FooWithBar>()
        for(foo: Foo in fooDao.getAllFoos()) {
            allFoosWithBars.add(FooWithBar(foo.fooId!!,fooDao,barDao))
        }
        for(fwb: FooWithBar in allFoosWithBars) {
            Log.d("FOOBARINFO","Foo is ${fwb.foo.fooText} Bar is ${fwb.bar.barText}")
        }
        //* Done with the Bar database Room wise
        bardb.closeInstance()
        foodb.attachBar(this) //<<<<< ATTACHES the Bar database to the Foo

        /* Get a Supprort SQLite Database */
        var sdb = foodb.openHelper.writableDatabase

        /* Query Foo and the attached Bar */
        var csr = sdb.query("SELECT * FROM foo JOIN ${FooDatabase.BAR_SCHEMA_NAME}.bar ON foo.barId = ${FooDatabase.BAR_SCHEMA_NAME}.bar.id")
        DatabaseUtils.dumpCursor(csr)
        csr.close()
   }
}

结果

2021-06-16 16:35:04.045 D/FOOBARINFO: Foo is FOO1 Bar is BAR1
2021-06-16 16:35:04.045 D/FOOBARINFO: Foo is FOO2 Bar is BAR2




2021-06-16 16:35:04.092 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@ee9871b
2021-06-16 16:35:04.093 I/System.out: 0 {
2021-06-16 16:35:04.093 I/System.out:    fooId=1
2021-06-16 16:35:04.093 I/System.out:    barId=1
2021-06-16 16:35:04.093 I/System.out:    fooText=FOO1
2021-06-16 16:35:04.093 I/System.out:    id=1
2021-06-16 16:35:04.093 I/System.out:    barText=BAR1
2021-06-16 16:35:04.093 I/System.out: }
2021-06-16 16:35:04.093 I/System.out: 1 {
2021-06-16 16:35:04.093 I/System.out:    fooId=2
2021-06-16 16:35:04.093 I/System.out:    barId=3
2021-06-16 16:35:04.093 I/System.out:    fooText=FOO2
2021-06-16 16:35:04.093 I/System.out:    id=3
2021-06-16 16:35:04.093 I/System.out:    barText=BAR2
2021-06-16 16:35:04.094 I/System.out: }
2021-06-16 16:35:04.094 I/System.out: <<<<<