Обычный способ обработки отношений "многие-многие" — использование промежуточной таблицы, отображающей отношения. Такая таблица будет иметь два основных столбца, один из которых является уникальным идентификатором одной из строк в одной из таблиц, а другой — уникальным идентификатором одной из строк в другой таблице.
Поскольку у вас есть два уровня много-много, у вас будут основные таблицы Программа, Тренировка и Упражнение. Программа - Тренировка много-много, Тренировка к Упражнению тоже много-много. Таким образом, у вас будет 2 таблицы сопоставления.
Когда дело доходит до комнаты, вы используете @Embedded
для одной стороны отношений и @Relation
для другой в рамках POJO, который представляет "завершение".
-
Я вижу, что @Ignore
закодировано так, как будто это волшебным образом что-то делает, это на самом деле не требуется и может быть проблемой. Скорее я бы предложил рассматривать Entity исключительно как интерфейс между необъектно-ориентированной базой данных и конечным/используемым объектом, и, следовательно, POJO является «полным» объектом.
-
@Ignore
может использоваться для дополнительных данных, которые не сохраняются, а являются производными, возможно, при извлечении через производный столбец или, возможно, с помощью функций класса.
Возможно, рассмотрим следующий пример (я использую длинные идентификаторы, они эффективнее и быстрее):-
Итак, сначала три основные таблицы/сущности:-
- Обратите внимание, что это было адаптировано из другого ответа, поэтому они могут немного отличаться от вашего, но принцип применим.
Упражнение :-
@Entity(
indices = [Index(value = ["exerciseName"],unique = true)] // enforce unique exercise name.
)
data class Exercise(
@PrimaryKey
val exerciseId: Long? = null,
val exerciseName: String
)
- в другом ответе было включено использование
UNIQUE
для имени упражнения, чтобы показать его эффект с помощью OnConflictStrategy
; оставлено для удобства.
Тренировка
@Entity
data class Workout(
@PrimaryKey
val workoutId: Long? = null,
val workoutName: String
)
Программа
@Entity(tableName = "programmes")
data class Programme(
@PrimaryKey
val programmeId:Long? = null,
val name:String,
var nextWorkoutId: String,
var lastWorkout:Long
)
- Как уже объяснялось, фактически бесполезные @Ignore были опущены
Далее 2 таблицы сопоставления
Карта тренировок
@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
)
- см. комментарии
- Обратите внимание, что внешние ключи не требуются и не определяют отношения, вместо этого они поддерживают отношения, помогая обеспечить ссылочную целостность (вкратце, без сирот).
ProgrammeWorkoutMap
@Entity(
primaryKeys = ["programmeIdMap","workoutIdMap"],
indices = [Index("workoutIdMap")],
foreignKeys = [
ForeignKey(
entity = Programme::class,
parentColumns = ["programmeId"],
childColumns = ["programmeIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = Workout::class,
parentColumns = ["workoutId"],
childColumns = ["workoutIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class ProgrammeWorkoutMap (
val programmeIdMap: Long,
val workoutIdMap: Long
)
Для сопоставления WorkoutExercise, извлечения, POJO WorkoutWithExercises с одной тренировкой @Embedded
и упражнений 0-N с @Relation
- Обратите внимание, как я прорабатываю иерархию снизу вверх (упражнение) и стараюсь быть описательным с именами классов (надеюсь, чтобы их было легче понять)
:-
@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
)
Чтобы перейти на следующий уровень, программа с тренировкой, вы на самом деле получаете программу с тренировкой с упражнениями, поэтому
data class ProgrammeWithWorkoutsWithExercises (
@Embedded
val programme: Programme,
@Relation(
entity = Workout::class,
parentColumn = "programmeId",
entityColumn = "workoutId",
associateBy = Junction(
value = ProgrammeWorkoutMap::class,
parentColumn = "programmeIdMap",
entityColumn = "workoutIdMap"
)
)
val workouts: List<WorkoutWithExercises>
)
-
ПРИМЕЧАНИЕ хотя объекты WorkoutWithExercises извлекаются, такой таблицы нет, поэтому объект Workout NOT WorkoutWithExercises
Дао для приведенного выше (и последующей рабочей демонстрации): -
@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
abstract fun insert(programme: Programme): Long
@Insert
abstract fun insert(workoutExerciseMap: WorkoutExerciseMap): Long
@Insert
abstract fun insert(programmeWorkoutMap: ProgrammeWorkoutMap): Long
@Query("SELECT * FROM workout") /* Not Used */
abstract fun getAllWorkouts(): List<Workout>
@Query("SELECT * FROM workout WHERE workout.workoutId=:workoutId") /* Not used in Demo */
abstract fun getWorkoutById(workoutId: Long): Workout
@Query("SELECT * FROM Exercise")
abstract fun getAllExercises(): List<Exercise>
@Query("SELECT * FROM exercise WHERE exercise.exerciseId=:exerciseId") /* Not Used in Demo */
abstract fun getExerciseById(exerciseId: Long): Exercise
@Query("SELECT * FROM workout") /* Not used in Demo*/
@Transaction
abstract fun getAllWorkoutsWithExercises(): List<WorkoutWithExercises>
/* Only extract used in Demo */
@Query("SELECT * FROM programmes")
@Transaction
abstract fun getAllPorgrammesWithWorkoutsWithExercises(): List<ProgrammeWithWorkoutsWithExercises>
}
База @Database в TheDatabase включает 5 объектов согласно :-
@Database(entities = [Programme::class,Workout::class,ProgrammeWorkoutMap::class,Exercise::class,WorkoutExerciseMap::class],version = 1)
Демонстрация выполняется в основном потоке, а код в действии:-
db = TheDatabase.getDatabaseInstance(this)
dao = db.getAllDao()
var ex1 = dao.insert(Exercise(exerciseName = "Exercise1"))
var ex2 = dao.insert(Exercise(exerciseName = "Exercise2"))
var ex3 = dao.insert(Exercise(exerciseName = "Exercise3"))
var ex4 = dao.insert(Exercise(exerciseName = "Exercise4"))
var ex5 = dao.insert(Exercise(exerciseName = "Exercise5"))
var wo1 = dao.insert(Workout(workoutName = "Workout1"))
var wo2 = dao.insert(Workout(workoutName = "Workout2"))
var ex6 = dao.insert(Exercise(exerciseName = "Exercise6"))
var ex7 = dao.insert(Exercise(exerciseName = "Exercise7"))
var wo3 = dao.insert(Workout(workoutName = "Workout3"))
var wo4 = dao.insert(Workout(workoutName = " Workout4"))
var wo5 = dao.insert(Workout(workoutName = "Workout5"))
// Add 4 exercises to Workout1
dao.insert(WorkoutExerciseMap(wo1,ex7))
dao.insert(WorkoutExerciseMap(wo1,ex5))
dao.insert(WorkoutExerciseMap(wo1,ex3))
dao.insert(WorkoutExerciseMap(wo1,ex1))
// Add 3 Exercises to Workout2
dao.insert(WorkoutExerciseMap(wo2,ex2))
dao.insert(WorkoutExerciseMap(wo2,ex4))
dao.insert(WorkoutExerciseMap(wo2,ex6))
// Add 2 Exercises to Workout3
dao.insert(WorkoutExerciseMap(wo3,ex3))
dao.insert(WorkoutExerciseMap(wo3,ex4))
// Add 1 Exercise to Workout 4
dao.insert(WorkoutExerciseMap(wo4,ex5))
// Don't add anything to Workout 5
// Add some Programmes
var p1 = dao.insert(Programme(name = "Prog1", nextWorkoutId = "????",lastWorkout = 100L))
var p2 = dao.insert(Programme(name = "Prog2",nextWorkoutId = "????", lastWorkout = 200L))
var p3 = dao.insert(Programme(name = "Prog3", nextWorkoutId = "????", lastWorkout = 300L))
//Map Workouts to Programmes (none for p3)
dao.insert(ProgrammeWorkoutMap(p1,wo3))
dao.insert(ProgrammeWorkoutMap(p1,wo5))
dao.insert(ProgrammeWorkoutMap(p1,wo2))
dao.insert(ProgrammeWorkoutMap(p2,wo1))
dao.insert(ProgrammeWorkoutMap(p2,wo4))
for(pww: ProgrammeWithWorkoutsWithExercises in dao.getAllPorgrammesWithWorkoutsWithExercises()) {
Log.d(TAG,"Programme is ${pww.programme.name}")
for(wwe: WorkoutWithExercises in pww.workouts) {
Log.d(TAG,"\tWorkout is ${wwe.workout.workoutName} (in Programme ${pww.programme.name}) ")
for (e: Exercise in wwe.exercises) {
Log.d(TAG,"\t\tExercise is ${e.exerciseName} (in Workout ${wwe.workout.workoutName} that is in Programme ${pww.programme.name})")
}
}
}
Демонстрация при первом запуске (она не предназначена для многократного запуска, достаточно просто показать, что принцип работает) результат в следующем выводе в журнал:-
D/WOEINFO: Programme is Prog1
D/WOEINFO: Workout is Workout2 (in Programme Prog1)
D/WOEINFO: Exercise is Exercise2 (in Workout Workout2 that is in Programme Prog1)
D/WOEINFO: Exercise is Exercise4 (in Workout Workout2 that is in Programme Prog1)
D/WOEINFO: Exercise is Exercise6 (in Workout Workout2 that is in Programme Prog1)
D/WOEINFO: Workout is Workout3 (in Programme Prog1)
D/WOEINFO: Exercise is Exercise3 (in Workout Workout3 that is in Programme Prog1)
D/WOEINFO: Exercise is Exercise4 (in Workout Workout3 that is in Programme Prog1)
D/WOEINFO: Workout is Workout5 (in Programme Prog1)
D/WOEINFO: Programme is Prog2
D/WOEINFO: Workout is Workout1 (in Programme Prog2)
D/WOEINFO: Exercise is Exercise1 (in Workout Workout1 that is in Programme Prog2)
D/WOEINFO: Exercise is Exercise3 (in Workout Workout1 that is in Programme Prog2)
D/WOEINFO: Exercise is Exercise5 (in Workout Workout1 that is in Programme Prog2)
D/WOEINFO: Exercise is Exercise7 (in Workout Workout1 that is in Programme Prog2)
D/WOEINFO: Workout is Workout4 (in Programme Prog2)
D/WOEINFO: Exercise is Exercise5 (in Workout Workout4 that is in Programme Prog2)
D/WOEINFO: Programme is Prog3