【问题标题】:Could someone help me understand Room persistence library better?有人可以帮助我更好地理解 Room 持久性库吗?
【发布时间】: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 还是很陌生

【问题讨论】:

    标签: android kotlin mvvm android-room dao


    【解决方案1】:

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

    1. 您可以为所有实体使用一个数据库。
    2. 每个实体都应该有单独的存储库。
    3. DAO(Data Access Object)类主要用于数据库操作(INSERT、UPDATE、DELETE)。
    4. 您可以通过@ForeignKey 注释到相应字段来创建一个表与另一个表的关系。
    5. 您不应在 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 ComponentInfo{a.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)
    ....
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-02-03
      • 1970-01-01
      • 2021-09-04
      • 1970-01-01
      • 1970-01-01
      • 2016-07-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多