有人可以帮助我更好地理解 Room 持久性库吗?

Posted

技术标签:

【中文标题】有人可以帮助我更好地理解 Room 持久性库吗?【英文标题】:Could someone help me understand Room persistence library better? 【发布时间】:2021-10-11 10:04:33 【问题描述】:

我已经开始为 android 制作应用程序,并决定了解更多关于数据库的信息。我认为对于一个简单的应用程序,我对 SQL 的理解已经足够好了。我遇到的麻烦是房间。我已经阅读了很多并观看了教程,但我仍然不能完全理解实现它的正确方法。这是我收集的。我认为我应该拥有实体、DAO、数据库和存储库。

我的应用背后的想法是这样的。我会进行锻炼、锻炼等。

锻炼应该有锻炼。

现在我想象它是 2 个表:锻炼表、锻炼表

每个锻炼和锻炼都应该有一个唯一的 id/c 主键

锻炼实体将具有 2 个属性:锻炼 ID、锻炼名称

Exercise 实体有 2 个属性:exerciseId、exerciseName

我计划用多对多或一对多关系连接这两个表,但这不是我现在遇到的问题。

现在,在为这两个实体创建数据类之后,我为每个实体创建了一个 Dao 类。 所以,WorkoutDao 和Exercise Dao(因为我读到每个实体有一个dao 是一种很好的做法)。现在我不太明白的是,很多人在网上说我应该每个实体有一个数据库,这对我来说似乎很奇怪。我不应该拥有一个包含所有相互关联的表的数据库。如果每个表都有一个 DAO,我该如何连接这些表。另外,我不知道是每个实体/表有一个存储库还是每个 database.entity 有一个存储库。

还有一个关于 ViewModel 与 Room 结合的问题。我收集到每个 Activity 应该有一个 ViewModel,基本上在 Fragment 之间共享。假设我有一个具有 recyclerView 和锻炼列表的活动,当我单击列表中的任何锻炼时,我会转到另一个具有回收器视图的活动,该视图显示单击的锻炼所具有的每个锻炼。两个视图模型会实例化同一个数据库吗?因为这似乎不是最好的解决方案。因为锻炼视图模型应该只有一个锻炼列表,而锻炼视图模型应该有一个锻炼列表(嗯,点击锻炼的锻炼)。我将如何解决这个问题?

无论如何,对不起,如果我写的内容令人困惑,如果没有理解,我会尝试更好地解释它,我对 kotlin 和 android studio 还是很陌生

【问题讨论】:

【参考方案1】:

这里有几个要点可以消除您的困惑:

    您可以为所有实体使用一个数据库。 每个实体都应该有单独的存储库。 DAO(Data Access Object)类主要用于数据库操作(INSERT、UPDATE、DELETE)。 您可以通过@ForeignKey 注释到相应字段来创建一个表与另一个表的关系。 您不应在 Activity/viewModel 中初始化数据库。您应该创建一个单例类,它将在 Application 类中启动数据库。这就是您仅在应用启动时才初始化数据库的方式。

【讨论】:

感谢您的回答。我想我明白了,但我仍然感到困惑的一件事是我是否应该每个数据库有一个 DAO 或每个实体一个 DAO。如果是后者,我如何查询关系并获取数据。例如,如果我有一个 WorkoutWithExercises 类,我是在 WorkoutDao 还是 ExerciseDao 中创建一个查询,因为我正在关注的教程只使用一个 DAO,我有点迷失了 或者我只是为那个关系创建另一个 Dao? 一切都是为了整理你的东西。您可以在任何 DAO 中查询任何实体。如果您在 WorkOutDAO 文件中编写与锻炼相关的查询,它将增加您的代码可读性。除此之外,在单个 DAO 中编写所有查询并不是犯罪【参考方案2】:

我不应该拥有一个包含所有相互关联的表的数据库吗?

是的。

我计划用多对多或一对多关系连接这两个表,但这不是我现在遇到的问题。

您可能想要多对多,即锻炼可以有很多锻炼,而一个锻炼可以有很多作为父母的锻炼。

因此,您可以为关系使用第三个表,其中包含锻炼 ID 的列和练习 ID 的列。这样的表有很多名称,例如关联表、引用表....

如果每个表都有一个 DAO,我该如何连接这些表。 可以定义和访问多个 Dao。

参见下面的演示(@Database 类TheDatabase),简而言之,您只需让@Database 类知道它们并允许检索 dao,Room 会为您完成所有底层工作。



演示

这是一个基于您的描述的基本工作示例,这还包括每个实体的 dao 以及所有 dao 的组合:-

锻炼实体(表格):-

@Entity
data class Workout(
    @PrimaryKey
    val workoutId: Long? = null,
    val workoutName: String
)

WorkoutDao :-

@Dao
abstract class WorkoutDao 

    @Insert
    abstract fun insert(workout: Workout): Long
    @Query("SELECT * FROM workout")
    abstract fun getAllWorkouts(): List<Workout>
    @Query("SELECT * FROM exercise WHERE exerciseId=:exerciseId")
    abstract fun getWorkoutById(exerciseId: Long): Exercise

请注意,它不是一个接口,而是一个抽象类(这可能是有益的)

练习实体(表格)

@Entity(
    indices = [Index(value = ["exerciseName"],unique = true)] // enforce unique exercise name.
)
data class Exercise(
    @PrimaryKey
    val exerciseId: Long? = null,
    val exerciseName: String
)
请注意,这里我们为锻炼名称添加了唯一索引,因此锻炼名称必须是唯一的(也可以应用于锻炼)。

ExerciseDao

@Dao
abstract class ExerciseDao 
    @Insert
    abstract fun insert(exercise: Exercise): Long
    @Query("SELECT * FROM exercise")
    abstract fun getAllExercises(): List<Exercise>
    @Query("SELECT * FROM exercise WHERE exerciseId=:exerciseId")
    abstract fun getExerciseById(exerciseId: Long): Exercise

注意请参阅 AllDao 中的等效项(如下),因为插入函数不适合处理重复的练习名称(AllDao 可以,活动代码演示了区别)

由于您可能想要映射表 WorkoutExerciseMap 实体(表)的多对多关系:-

@Entity(
    primaryKeys = ["workoutIdMap","exerciseIdMap"], // a combination of  workout/exercise is primary key
    indices = [Index("exerciseIdMap")],  // Room issues warning if not indexed
    foreignKeys = [
        // Foreign keys, each defines a constraint (rule) saying value to be store MUST exist in the parent table
        // i.e. the value to be stored in the workoutIdMap MUST be the id of an existing Workout
        ForeignKey(
            entity = Workout::class, // the entity/table that the FK points to
            parentColumns = ["workoutId"], // the column in the parent table
            childColumns = ["workoutIdMap"], // column in this table where
            onDelete = ForeignKey.CASCADE, // if a Workout is deleted then delete the children
            onUpdate = ForeignKey.CASCADE // if a workoutId is changed then change the children
        ),
        ForeignKey(entity = Exercise::class,parentColumns = ["exerciseId"],childColumns = ["exerciseIdMap"])
    ]
)
data class WorkoutExerciseMap(
    val workoutIdMap: Long,
    val exerciseIdMap: Long
)
查看希望对您有所帮助的 cmets

WorkoutExerciseMapDao :-

@Dao
abstract class WorkoutExerciseMapDao 
    @Insert
    abstract fun insert(workoutExerciseMapDao: WorkoutExerciseMap): Long
    @Query("SELECT * FROM workoutexercisemap")
    abstract fun getAllWorkoutExerciseMaps(): List<WorkoutExerciseMap>
    @Transaction
    @Query("SELECT * FROM workout")
    abstract fun getAllWorkoutsWithExercises(): List<WorkoutWithExercises>


getAllWorkoutsWithExercises 应该在这里吗? (修辞) 在插入后返回 rowid 是否有用? (修辞) 检索映射是否有用? (修辞)

为了能够通过相关练习获得锻炼需要 POJO(不是实体),所以 WorkoutWithExercises :-

data class WorkoutWithExercises(
    @Embedded
    val workout: Workout,
    @Relation(
        entity = Exercise::class,
        parentColumn = "workoutId",
        entityColumn = "exerciseId",
        associateBy = Junction(WorkoutExerciseMap::class,parentColumn = "workoutIdMap",entityColumn = "exerciseIdMap")
    )
    val exercises: List<Exercise>
)
@Embedded 包含一次锻炼 @Relation 将通过映射(关联)表 WorkoutExerciseMap 检索所有相关的练习 如果您想在所有相关锻炼中进行锻炼,您可以使用类似的 POJO,但关系颠倒。

关于将单个 Dao 放在 AllDao 是否是坏/好,顾名思义,ALL 的 dao 在一个类中:-

@Dao
abstract class AllDao 

    /*
     As exercise has a unique index on exercisename skip if same exercise name is used
     otherwise duplicating name will result in an exception
     */
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract fun insert(exercise: Exercise): Long
    @Insert
    abstract fun insert(workout: Workout): Long
    @Insert
    /*
        not much use (if any) of returning Long as value will be the
        rowid (hidden column).
    */
    abstract fun insert(workoutExerciseMap: WorkoutExerciseMap): Long

    @Query("SELECT * FROM workout")
    abstract fun getAllWorkouts(): List<Workout>
    @Query("SELECT * FROM workout WHERE workout.workoutId=:workoutId")
    abstract fun getWorkoutById(workoutId: Long): Workout
    @Query("SELECT * FROM Exercise")
    abstract fun getAllExercises(): List<Exercise>
    @Query("SELECT * FROM exercise WHERE exercise.exerciseId=:exerciseId")
    abstract fun getExerciseById(exerciseId: Long): Exercise
    @Query("SELECT * FROM workout")
    @Transaction
    abstract fun getAllWorkoutsWithExercises(): List<WorkoutWithExercises>


all-together v 分开有优点/缺点,当然有可能两者甚至全部和分开共存(尽管这会增加维护的复杂性)

绑定所有组件的是@Database 类TheDatabase:-

@Database(entities = [Workout::class,Exercise::class,WorkoutExerciseMap::class],version = 1)
abstract class TheDatabase: RoomDatabase() 

    abstract fun getWorkoutDao(): WorkoutDao
    abstract fun getExerciseDao(): ExerciseDao
    abstract fun getWorkoutExerciseMapDao(): WorkoutExerciseMapDao

    /* Versus all in one Dao ???? */
    abstract fun getAllDao(): AllDao

    companion object 

        @Volatile
        private var instance: TheDatabase? = null

        fun getDatabaseInstance(context: Context): TheDatabase 
            if (instance == null) 
                instance = Room.databaseBuilder(context,TheDatabase::class.java,"workoutexercise.db")
                    .allowMainThreadQueries()
                    .build()
            
            return instance as TheDatabase
        
    

请注意,为了方便/简洁,运行/测试以上将在主线程上完成。

以上内容的实际使用通过 MainActivity 活动进行演示:-

class MainActivity : AppCompatActivity() 

    lateinit var db: TheDatabase
    lateinit var workoutDao: WorkoutDao
    lateinit var exerciseDao: ExerciseDao
    lateinit var workoutExerciseMapDao: WorkoutExerciseMapDao
    lateinit var allDao: AllDao

    private final var TAG = "WOEINFO"

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = TheDatabase.getDatabaseInstance(this)
        workoutDao = db.getWorkoutDao()
        exerciseDao = db.getExerciseDao()
        workoutExerciseMapDao = db.getWorkoutExerciseMapDao()


        var ex1 = exerciseDao.insert(Exercise(exerciseName = "Exercise1"))
        var ex2 = exerciseDao.insert(Exercise(exerciseName = "Exercise2"))
        var ex3 = exerciseDao.insert(Exercise(exerciseName = "Exercise3"))
        var ex4 = exerciseDao.insert(Exercise(exerciseName =  "Exercise4"))
        var ex5 = exerciseDao.insert(Exercise(exerciseName = "Exercise5"))

        var wo1 = workoutDao.insert(Workout(workoutName =  "Workout1"))
        var wo2 = workoutDao.insert(Workout(workoutName = "Workout2"))

        allDao = db.getAllDao()
        var ex6 = allDao.insert(Exercise(exerciseName = "Exercise6"))
        var ex7 = allDao.insert(Exercise(exerciseName = "Exercise7"))
        var wo3 = allDao.insert(Workout(workoutName = "Workout3"))
        var wo4 = allDao.insert(Workout(workoutName =  " Workout4"))
        var wo5 = allDao.insert(Workout(workoutName = "Workout5"))

        // Add 4 exercises to Workout1
        workoutExerciseMapDao.insert(WorkoutExerciseMap(wo1,ex7))
        allDao.insert(WorkoutExerciseMap(wo1,ex5))
        workoutExerciseMapDao.insert(WorkoutExerciseMap(wo1,ex3))
        workoutExerciseMapDao.insert(WorkoutExerciseMap(wo1,ex1))

        // Add 3 Exercises to Workout2
        allDao.insert(WorkoutExerciseMap(wo2,ex2))
        allDao.insert(WorkoutExerciseMap(wo2,ex4))
        allDao.insert(WorkoutExerciseMap(wo2,ex6))

        // Add 2 Exercises to Workout3
        workoutExerciseMapDao.insert(WorkoutExerciseMap(wo3,ex3))
        workoutExerciseMapDao.insert(WorkoutExerciseMap(wo3,ex4))

        // Add 1 Exercise to Workout 4
        allDao.insert(WorkoutExerciseMap(wo4,ex5))

        // Don't add anything to Workout 5

        for(wwe: WorkoutWithExercises in allDao.getAllWorkoutsWithExercises()) 
            Log.d(TAG,"Workout is $wwe.workout.workoutName")
            for(ex: Exercise in wwe.exercises) 
                Log.d(TAG,"\tExercise is $ex.exerciseName")
            
        

        /* Show effect of onConflictStrategy.IGNORE */
        allDao.insert(Exercise(exerciseName = "Exercise1"))
        for(ex: Exercise in allDao.getAllExercises()) 
            Log.d(TAG,"Exercise is $ex.exerciseName")
        
        /* effect without onConflictStrategy.IGNORE i.e. exception
            i.e. exerciseDao does not have onConflictStrategy.IGNORE coded
         */
        exerciseDao.insert(Exercise(exerciseName = "Exercise1"))
        for(ex: Exercise in exerciseDao.getAllExercises()) 
            Log.d(TAG,"Exercise is $ex.exerciseName")
        
    

请注意,后面的代码故意导致异常以演示唯一的练习名称(索引)。

结果

当上述运行时,日志包括:-

2021-08-07 10:17:07.368 D/WOEINFO: Workout is Workout1
2021-08-07 10:17:07.369 D/WOEINFO:  Exercise is Exercise1
2021-08-07 10:17:07.369 D/WOEINFO:  Exercise is Exercise3
2021-08-07 10:17:07.369 D/WOEINFO:  Exercise is Exercise5
2021-08-07 10:17:07.369 D/WOEINFO:  Exercise is Exercise7
2021-08-07 10:17:07.369 D/WOEINFO: Workout is Workout2
2021-08-07 10:17:07.369 D/WOEINFO:  Exercise is Exercise2
2021-08-07 10:17:07.369 D/WOEINFO:  Exercise is Exercise4
2021-08-07 10:17:07.369 D/WOEINFO:  Exercise is Exercise6
2021-08-07 10:17:07.369 D/WOEINFO: Workout is Workout3
2021-08-07 10:17:07.369 D/WOEINFO:  Exercise is Exercise3
2021-08-07 10:17:07.369 D/WOEINFO:  Exercise is Exercise4
2021-08-07 10:17:07.369 D/WOEINFO: Workout is  Workout4
2021-08-07 10:17:07.369 D/WOEINFO:  Exercise is Exercise5
2021-08-07 10:17:07.369 D/WOEINFO: Workout is Workout5
即预期的相关数据

在第一次尝试插入名为“Exercise1”的重复练习之后:-

2021-08-07 10:17:07.376 D/WOEINFO: Exercise is Exercise1
2021-08-07 10:17:07.376 D/WOEINFO: Exercise is Exercise2
2021-08-07 10:17:07.376 D/WOEINFO: Exercise is Exercise3
2021-08-07 10:17:07.376 D/WOEINFO: Exercise is Exercise4
2021-08-07 10:17:07.376 D/WOEINFO: Exercise is Exercise5
2021-08-07 10:17:07.376 D/WOEINFO: Exercise is Exercise6
2021-08-07 10:17:07.377 D/WOEINFO: Exercise is Exercise7
即练习1没有重复

当第二次尝试插入名为“Exercise1”的练习时,但使用来自没有 onConflictStrategy.IGNORE 的 ExperimentDao 的插入,则:-

2021-08-07 10:17:07.380 32394-32394/a.a.so68682797exerciseappexample E/AndroidRuntime: FATAL EXCEPTION: main
    Process: a.a.so68682797exerciseappexample, PID: 32394
    java.lang.RuntimeException: Unable to start activity ComponentInfoa.a.so68682797exerciseappexample/a.a.so68682797exerciseappexample.MainActivity: android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: Exercise.exerciseName (code 2067 SQLITE_CONSTRAINT_UNIQUE)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2913)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3048)
....

【讨论】:

以上是关于有人可以帮助我更好地理解 Room 持久性库吗?的主要内容,如果未能解决你的问题,请参考以下文章

有人可以帮助我更好地理解正则表达式中的零或一,并可能在同一个正则表达式语句中嵌套另一个

需要帮助更好地理解 UIStackView

Android Jetpack: Room | 中文教学视频

帮助我更好地理解 Struts2、验证和有状态操作

我没有完全得到“存在”的部分。谁能帮助我更好地理解它? [关闭]

css Stackla Lightbox - 更好地制作Room.css