有人可以帮助我更好地理解 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 持久性库吗?的主要内容,如果未能解决你的问题,请参考以下文章
有人可以帮助我更好地理解正则表达式中的零或一,并可能在同一个正则表达式语句中嵌套另一个
Android Jetpack: Room | 中文教学视频