我不应该拥有一个包含所有相互关联的表的数据库吗?
是的。
我计划用多对多或一对多关系连接这两个表,但这不是我现在遇到的问题。
您可能想要多对多,即锻炼可以有很多锻炼,而一个锻炼可以有很多作为父母的锻炼。
因此,您可以为关系使用第三个表,其中包含锻炼 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
)
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
当第二次尝试插入名为“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)
....