javaandroidandroid-studioandroid-roomrelation

Room database relational


I am currently building an Inteval App for workout management. Basically, user can create preset with stages, and a stage can be timer-based or rep-based. Stage is like "REST", "WORKOUT, "COOLDOWN". I am currently stuck at getting all preset and theirs stages, if the stage is rep-based then get the reps too. The relation of entities are:

  1. Preset - Stage: 1 - n
  2. Stage - Rep: 1 - n

I asked ChatGPT about this and it suggested me to create relational entites. But I get the error like this:

The class must be either @Entity or @DatabaseView. public class StageWithReps

Here is my entities:

Preset.java

package com.example.intervalapp.entity;

import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey;

import java.time.LocalDateTime;


@Entity(tableName = "preset") public class Preset {   @PrimaryKey(autoGenerate = true)   @ColumnInfo(name = "preset_id")   private int id;

  @ColumnInfo(name = "preset_name")   private String name;

  @ColumnInfo(name = "created_date")   private LocalDateTime createdDate;

  @ColumnInfo(name = "modified_date")   private LocalDateTime modifiedDate;

  public Preset(String name, LocalDateTime createdDate, LocalDateTime modifiedDate) {
    this.name = name;
    this.createdDate = createdDate;
    this.modifiedDate = modifiedDate;   }

  public int getId() {
    return id;   }

  public void setId(int id) {
    this.id = id;   }

  public String getName() {
    return name;   }

  public void setName(String name) {
    this.name = name;   }

  public LocalDateTime getCreatedDate() {
    return createdDate;   }

  public void setCreatedDate(LocalDateTime createdDate) {
    this.createdDate = createdDate;   }

  public LocalDateTime getModifiedDate() {
    return modifiedDate;   }

  public void setModifiedDate(LocalDateTime modifiedDate) {
    this.modifiedDate = modifiedDate;   }

  @NonNull   @Override   public String toString() {
    return "Preset{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", createdDate=" + createdDate +
            ", modifiedDate=" + modifiedDate +
            '}';   } }

Stage.java

package com.example.intervalapp.entity;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
import androidx.room.PrimaryKey;

import com.example.intervalapp.enums.StageType;

import java.time.Duration;

@Entity(
        tableName = "stage",
        foreignKeys = @ForeignKey(
                entity = Preset.class,
                parentColumns = "preset_id",
                childColumns = "preset_id",
                onDelete = ForeignKey.CASCADE
        ),
        indices = {@Index("preset_id")}
)
public class Stage {
  @PrimaryKey(autoGenerate = true)
  @ColumnInfo(name = "stage_id")
  private int id;

  @ColumnInfo(name = "preset_id")
  private int presetId;

  @ColumnInfo(name = "set_number")
  private int setNumber;

  @ColumnInfo(name = "stage_label")
  private String label;

  @ColumnInfo(name = "stage_type")
  private String stageType; // E.g., "TIMER" or "REP"

  @Nullable
  @ColumnInfo(name = "duration")
  private Duration duration; // Nullable for REP-based stages

  @Nullable
  @ColumnInfo(name = "num_reps")
  private Integer numReps; // Nullable for TIMER-based stages

  public Stage(int presetId, String label, int setNumber, String stageType, @Nullable Duration duration, @Nullable Integer numReps) {
    this.presetId = presetId;
    this.label = label;
    this.setNumber = setNumber;
    this.stageType = stageType;

    // Assign the duration only for TIMER stages
    if (StageType.TIME.toString().equals(stageType)) {
      this.duration = duration;
      this.numReps = null; // Ensure no reps value for TIMER stages
    } else if (StageType.REPS.toString().equals(stageType)) {
      this.duration = null; // Ensure no duration for REP stages
      this.numReps = numReps;
    }
  }

  public int getId() {
    return id;
  }

  public void setId(int id) {
    this.id = id;
  }

  public int getPresetId() {
    return presetId;
  }

  public void setPresetId(int presetId) {
    this.presetId = presetId;
  }

  public int getSetNumber() {
    return setNumber;
  }

  public void setSetNumber(int setNumber) {
    this.setNumber = setNumber;
  }

  public String getLabel() {
    return label;
  }

  public void setLabel(String label) {
    this.label = label;
  }

  public String getStageType() {
    return stageType;
  }

  public void setStageType(String stageType) {
    this.stageType = stageType;
  }

  @Nullable
  public Duration getDuration() {
    return duration;
  }

  public void setDuration(@Nullable Duration duration) {
    this.duration = duration;
  }

  @Nullable
  public Integer getNumReps() {
    return numReps;
  }

  public void setNumReps(@Nullable Integer numReps) {
    this.numReps = numReps;
  }

  @NonNull
  @Override
  public String toString() {
    return "Stage{" +
            "id=" + id +
            ", presetId=" + presetId +
            ", label='" + label + '\'' +
            ", setNumber=" + setNumber +
            ", stageType='" + stageType + '\'' +
            ", duration=" + duration +
            ", numReps=" + numReps +
            '}';
  }
}

Rep.java

package com.example.intervalapp.entity;

import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
import androidx.room.PrimaryKey;

@Entity(
        tableName = "rep",
        foreignKeys = @ForeignKey(
                entity = Stage.class,
                parentColumns = "stage_id",
                childColumns = "stage_id",
                onDelete = ForeignKey.CASCADE
        ),
        indices = {@Index("stage_id")}
)
public class Rep {
  @PrimaryKey(autoGenerate = true)
  @ColumnInfo(name = "rep_id")
  private int id;

  @ColumnInfo(name = "stage_id")
  private int stageId;

  @ColumnInfo(name = "rep_duration")
  private float repDuration;

  public Rep(int stageId, float repDuration) {
    this.stageId = stageId;
    this.repDuration = repDuration;
  }

  public int getId() {
    return id;
  }

  public void setId(int id) {
    this.id = id;
  }

  public int getStageId() {
    return stageId;
  }

  public void setStageId(int stageId) {
    this.stageId = stageId;
  }

  public float getRepDuration() {
    return repDuration;
  }

  public void setRepDuration(int repDuration) {
    this.repDuration = repDuration;
  }

  @NonNull
  @Override
  public String toString() {
    return "Rep{" +
            "id=" + id +
            ", stageId=" + stageId +
            ", repDuration=" + repDuration +
            '}';
  }
}

PresetWithStages.java

package com.example.intervalapp.relation;

import androidx.room.Embedded;
import androidx.room.Relation;
import com.example.intervalapp.entity.Preset;
import java.util.List;

public class PresetWithStages {
  @Embedded
  private Preset preset;

  @Relation(
          parentColumn = "preset_id",
          entityColumn = "preset_id"
  )
  private List<StageWithReps> stages;

  // Getters and Setters
  public Preset getPreset() {
    return preset;
  }

  public void setPreset(Preset preset) {
    this.preset = preset;
  }

  public List<StageWithReps> getStages() {
    return stages;
  }

  public void setStages(List<StageWithReps> stages) {
    this.stages = stages;
  }
}

StageWithReps.java

package com.example.intervalapp.relation;

import androidx.room.Embedded;
import androidx.room.Relation;
import com.example.intervalapp.entity.Stage;
import com.example.intervalapp.entity.Rep;
import java.util.List;

public class StageWithReps {
  @Embedded
  private Stage stage;

  @Relation(
          parentColumn = "stage_id",
          entityColumn = "stage_id"
  )
  private List<Rep> reps;

  // Getters and Setters
  public Stage getStage() {
    return stage;
  }

  public void setStage(Stage stage) {
    this.stage = stage;
  }

  public List<Rep> getReps() {
    return reps;
  }

  public void setReps(List<Rep> reps) {
    this.reps = reps;
  }
}

PresetDAO.java

package com.example.intervalapp.dao;

import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Transaction;
import androidx.room.Update;

import com.example.intervalapp.entity.Preset;
import com.example.intervalapp.relation.PresetWithStages;

import java.time.LocalDateTime;
import java.util.List;

@Dao
public interface PresetDAO {

  @Insert
  long createPreset(Preset preset);

  @Transaction
  @Query("SELECT * FROM preset")
  LiveData<List<PresetWithStages>> getAllPresets(); // Fetch hierarchical data
}

Can I get some help 🥲. This is my first time uploading a post on stackoverflow, so if I miss something, please tell me. Thank you everyone!


Solution

  • Your issue is with:-

    @Relation(
              parentColumn = "preset_id",
              entityColumn = "preset_id"
      )
      private List<StageWithReps> stages;
    

    The @Relation implies that StageWithReps is expected to be a table (and thus @Entity annotated) from which the children (Stages) will be retrieved. If the children are an expansion then that underlying @Relation will then be processed to build the children.

    To get the StageWithReps you need to get the parent(@Embedded) Stage from the Stage table by specifying the Stage class via the entity parameter of the @Relation annotation.

    As such, you likely want:-

    @Relation(
           entity = Stage.class, /*<<<<<<<<<< ADDED */
           parentColumn = "preset_id",
           entityColumn = "preset_id"
    )
    private List<StageWithReps> stages;
    

    Demo


    Using the above and some modifications for simplicty:-

    1. .allowMainThreadQueries for brevity of ruuning on the main thread
    2. Instead of time based types longs so there is no need for type converters.

    Then consider the following activity code:-

    public class MainActivity extends AppCompatActivity {
    
        private TheDatabase db;
        private PresetDAO dao;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            /* Get the database and the DAO */
            db = TheDatabase.getInstance(this);
            dao = db.getPresetDAO();
            
            /* Insert some Presets */
            long p1 = dao.createPreset(new Preset("P001"));
            long p2 = dao.createPreset(new Preset("P002"));
            long p3 = dao.createPreset(new Preset("P003"));
    
            /* Insert some Stages related to the Presets (not p3 is not referenced)*/
            long s1 = dao.creatStage(new Stage(((int) p1),"S001",100,"TYPEA",1000L,5));
            long s2 = dao.creatStage(new Stage(((int) p1),"S002",200,"TYPEB",2000L,25));
            long s3 = dao.creatStage(new Stage(((int) p2),"S003",300,"TYPEC",3000L,35));
    
            /* Insert some Reps related to Stages */
            dao.createRep(new Rep((int)s1,10.1234F));
            dao.createRep(new Rep((int)s1,11.1234F));
            dao.createRep(new Rep((int)s1,12.1234F));
            dao.createRep(new Rep((int)s2,13.1234F));
            dao.createRep(new Rep((int)s2,14.1234F));
            dao.createRep(new Rep((int)s3,15.1234F));
    
    
            /* Extract all of the PresetWithStages */
            
            /* first construct empty StringBuilders to cater for a single Log per extracted Preset*/
            StringBuilder stages_in_preset = new StringBuilder();
            StringBuilder reps_in_stages = new StringBuilder();
    
            /* use the extract for the core loop*/
            for (PresetWithStages pws: dao.getAllPresets()) {
                /* new Preset so effectively empty the StringBuilder for the stages of the current Preset */
                stages_in_preset = new StringBuilder();
                /* Loop through the stages of the current preset */
                for (StageWithReps swr: pws.getStages()) {
                    /* new Stage so empty the StringBuilder for the reps of the current Stage */
                    reps_in_stages = new StringBuilder();
                    for (Rep r: swr.getReps()) {
                        /* Add line feed and 2 tabs and then the info for the current Rep */
                        reps_in_stages.append("\n\t\t");
                        reps_in_stages.append("Rep ID is " + r.getId());
                        reps_in_stages.append(" DURATION is " + r.getRepDuration());
                        reps_in_stages.append(" REFERENCES STAGE WITH ID " + r.getStageId());
                    }
                    /* add the current Stage info and the info for all the reps */
                    stages_in_preset.append("\n\tStage is " + swr.getStage().getLabel());
                    stages_in_preset.append(" Number of Reps is " + swr.getReps().size() + ". They are:-" + reps_in_stages);
                }
                /* For the current Preset Log the Preset and all the Stage info (indented) and for each Stage the Rep info (double indented) */
                Log.d("DBINFO","PRESET is " + pws.getPreset().getName() + " it has " + pws.getStages().size() + " Stages. They are:-" + stages_in_preset);
            }
        }
    }
    

    This outputs (to the log):-

    D/DBINFO: PRESET is P001 it has 2 Stages. They are:-
            Stage is S001 Number of Reps is 3. They are:-
                Rep ID is 1 DURATION is 10.1234 REFERENCES STAGE WITH ID 1
                Rep ID is 2 DURATION is 11.1234 REFERENCES STAGE WITH ID 1
                Rep ID is 3 DURATION is 12.1234 REFERENCES STAGE WITH ID 1
            Stage is S002 Number of Reps is 2. They are:-
                Rep ID is 4 DURATION is 13.1234 REFERENCES STAGE WITH ID 2
                Rep ID is 5 DURATION is 14.1234 REFERENCES STAGE WITH ID 2
    D/DBINFO: PRESET is P002 it has 1 Stages. They are:-
            Stage is S003 Number of Reps is 1. They are:-
                Rep ID is 6 DURATION is 15.1234 REFERENCES STAGE WITH ID 3
    D/DBINFO: PRESET is P003 it has 0 Stages. They are:-