Как обращаться с отношением комнаты, встроенными списками и оперативными данными?

avatar
MikkelT
8 августа 2021 в 22:34
42
1
0

Я делаю приложение для тренировок. В этом приложении вы можете создать несколько программ, содержащих несколько тренировок, содержащих несколько упражнений.

Я управляю всем этим в базе данных Room

Programme.kt

@Entity(tableName = "programmes")
data class Programme(
    @PrimaryKey
    val programmeId:String,
    val name:String,
    var nextWorkoutId: String,
    var lastWorkout:Long,
    @Ignore
    val workouts:List<Workout>
)

Workout.kt

@Entity(tableName = "workouts", indices = [Index("workoutId")])
data class Workout(
    @PrimaryKey
    val workoutId: String,
    val name: String,
    @Ignore
    val exercises: List<Exercise>
)

Программа с тренировками

data class ProgrammeWithWorkouts(
    @Embedded val programme: Programme,
    @Relation(
        parentColumn = "programmeId",
        entityColumn = "workoutId",
    )
    val workouts:List<WorkoutsWithExercises>
)

@Entity(primaryKeys = ["programmeId","workoutId"],
indices = [Index("programmeId","workoutId")])
data class ProgrammeWorkoutCrossRef(
    val programmeId:String,
    val workoutId:String
)

И затем я хочу прослушать изменения через оперативные данные:

@Transaction
@Query("SELECT * FROM programmes")
    fun getProgrammesWithWorkoutsLiveData(): LiveData<List<ProgrammesWithWorkouts>>

К сожалению, это выдает ошибку:

constructor WorkoutWithRoutines in class ProgrammeWithWorkouts cannot be applied to given types;
            _item = new ProgrammeWithWorkouts();
                    ^
  required: Programme,List<WorkoutsWithExercises>
  found: no arguments
  reason: actual and formal argument lists differ in length

Возможно ли это?

Источник

Ответы (1)

avatar
MikeT
9 августа 2021 в 03:26
1

Обычный способ обработки отношений "многие-многие" — использование промежуточной таблицы, отображающей отношения. Такая таблица будет иметь два основных столбца, один из которых является уникальным идентификатором одной из строк в одной из таблиц, а другой — уникальным идентификатором одной из строк в другой таблице.

Поскольку у вас есть два уровня много-много, у вас будут основные таблицы Программа, Тренировка и Упражнение. Программа - Тренировка много-много, Тренировка к Упражнению тоже много-много. Таким образом, у вас будет 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