Skip to content
Advertisement

What should we do for nested objects in Room? [closed]

If there is a structure like my JSON structure below, how should we create Entity Classes? There are no examples of this. While @embeded was used for inner arrays in the articles written long ago, now a structure like converter is used. Which one should we use? What do these do? How can I create a struct of my type? Please help in Java

All required structures are available here: https://github.com/theoyuncu8/roomdb

JSON Data

{
"MyData": [
 {
   "food_id": "1",
   "food_name": "Food 1",
   "food_image": "imageurl",
   "food_kcal": "32",
   "food_url": "url",
   "food_description": "desc",
   "carb_percent": "72",
   "protein_percent": "23",
   "fat_percent": "4",
   "units": [
     {
       "unit": "Unit A",
       "amount": "735.00",
       "calory": "75.757",
       "calcium": "8.580",
       "carbohydrt": "63.363",
       "cholestrl": "63.0",
       "fiber_td": "56.12",
       "iron": "13.0474",
       "lipid_tot": "13.01",
       "potassium": "11.852",
       "protein": "717.1925",
       "sodium": "112.02",
       "vit_a_iu": "110.7692",
       "vit_c": "110.744"
     },
     {
       "unit": "Unit C",
       "amount": "32.00",
       "calory": "23.757",
       "calcium": "53.580",
       "carbohydrt": "39.363",
       "cholestrl": "39.0",
       "fiber_td": "93.12",
       "iron": "93.0474",
       "lipid_tot": "93.01",
       "potassium": "9.852",
       "protein": "72.1925",
       "sodium": "10.0882",
       "vit_a_iu": "80.7692",
       "vit_c": "80.744"
     }
   ]
 },
 {
   "food_id": "2",
   "food_name": "Food 2",
   "food_image": "imageurl",
   "food_kcal": "50",
   "food_url": "url",
   "food_description": "desc",
   "carb_percent": "25",
   "protein_percent": "14",
   "fat_percent": "8",
   "units": [
     {
       "unit": "Unit A",
       "amount": "25.00",
       "calory": "25.757",
       "calcium": "55.580",
       "carbohydrt": "53.363",
       "cholestrl": "53.0",
       "fiber_td": "53.12",
       "iron": "53.0474",
       "lipid_tot": "53.01",
       "potassium": "17.852",
       "protein": "757.1925",
       "sodium": "122.02",
       "vit_a_iu": "10.7692",
       "vit_c": "10.744"
     },
     {
       "unit": "Unit C",
       "amount": "2.00",
       "calory": "2.757",
       "calcium": "5.580",
       "carbohydrt": "3.363",
       "cholestrl": "3.0",
       "fiber_td": "3.12",
       "iron": "3.0474",
       "lipid_tot": "3.01",
       "potassium": "77.852",
       "protein": "77.1925",
       "sodium": "12.02",
       "vit_a_iu": "0.7692",
       "vit_c": "0.744"
     },
     {
       "unit": "Unit G",
       "amount": "1.00",
       "calory": "2.1",
       "calcium": "0.580",
       "carbohydrt": "0.363",
       "cholestrl": "0.0",
       "fiber_td": "0.12",
       "iron": "0.0474",
       "lipid_tot": "0.01",
       "potassium": "5.852",
       "protein": "0.1925",
       "sodium": "1.02",
       "vit_a_iu": "0.7692",
       "vit_c": "0.744"
     }
   ]
 }
]
}

Entity Class

Foods Class

public class Foods {
    @SerializedName("food_id")
    @Expose
    private String foodId;
    @SerializedName("food_name")
    @Expose
    private String foodName;
    @SerializedName("food_image")
    @Expose
    private String foodImage;
    @SerializedName("food_kcal")
    @Expose
    private String foodKcal;
    @SerializedName("food_url")
    @Expose
    private String foodUrl;
    @SerializedName("food_description")
    @Expose
    private String foodDescription;
    @SerializedName("carb_percent")
    @Expose
    private String carbPercent;
    @SerializedName("protein_percent")
    @Expose
    private String proteinPercent;
    @SerializedName("fat_percent")
    @Expose
    private String fatPercent;

// here

    @SerializedName("units")
    @Expose
    private List<FoodUnitsData> units = null;

    // getter setter

}

FoodUnitsData Class

public class FoodUnitsData {
   @SerializedName("unit")
   @Expose
   private String unit;
   @SerializedName("amount")
   @Expose
   private String amount;
   @SerializedName("calory")
   @Expose
   private String calory;
   @SerializedName("calcium")
   @Expose
   private String calcium;
   @SerializedName("carbohydrt")
   @Expose
   private String carbohydrt;
   @SerializedName("cholestrl")
   @Expose
   private String cholestrl;
   @SerializedName("fiber_td")
   @Expose
   private String fiberTd;
   @SerializedName("iron")
   @Expose
   private String iron;
   @SerializedName("lipid_tot")
   @Expose
   private String lipidTot;
   @SerializedName("potassium")
   @Expose
   private String potassium;
   @SerializedName("protein")
   @Expose
   private String protein;
   @SerializedName("sodium")
   @Expose
   private String sodium;
   @SerializedName("vit_a_iu")
   @Expose
   private String vitAIu;
   @SerializedName("vit_c")
   @Expose
   private String vitC;


   // getter setter
}

Answer

What do these do?

TypeConverters are used to convert a type that room cannot handle to a type that it can (String, primitives, integer types such as Integer, Long, decimal types such as Double, Float).

@Embedded basically says include the member variables of the @Embedded class as columns. e.g. @Embedded FoodUnitsData foodUnitsData;.

Test/Verify the Schema from the Room perspective

With the above class and with the entities defined in the class annotated with @Database (FoodDatabase) it would be a good idea to compile/build the project and fix anything that room complains about (none in this case).

So have FoodDataabse to be :-

@Database(entities = {Foods.class, FoodUnitsDataEntity.class /*<<<<<<<<<< ADDED*/}, version = 1)
public abstract class FoodDatabase extends RoomDatabase {
    public abstract DaoAccess daoAccess(); //* do not inlcude this line until the DaoAccess class has been created
}
  • Note see comment re DaoAccess (i.e. comment out the line)

and then CTRL + F9 and check the build log

Fourth DaoAccess

Obviously FoodUnitsDataEntity rows need to be added, update and deleted. It would also be very convenient if a Foods object could drive adding the FoodUnitsDataEntity rows all in one. This requires a method with a body therefore DaoAccess is changed from an interface to an abstract class to facilitate such a method.

Which one should we use?

You main issue is with the List of FoodUnitsData

Although you could convert the List and use a TypeConverter I would suggest not.

  • you would probably convert to a JSON string (so you extract from JSON into objects to then store the embedded objects as JSON). You BLOAT the data and also make using that data difficult.

  • Say for example you wanted to do a search for foods that have 1000 calories or more this would require a pretty complex query or you would load ALL the database and then loop through the foods and then the units.

I would say that @Embedded is the method to use. Along with using @Ignore (the opposite i.e. exclude the member variable from being a column). i.e. you would @Ignore the List in the Foods class.

  • With @Embedded you can then easily use individual values in queries.

  • You could then do something like SELECT * FROM the_table_used_for_the_foodunitsdata WHERE calory > 1000 and you would get a List of FoodUnitsData returned. SQLite will do this pretty efficiently.

Working Example

So putting the above into a working example:-

First the Foods class and adding the @Ignore annotation :-

@Entity(tableName = "food_data") // ADDED to make it usable as a Room table
public class Foods {
    @SerializedName("food_id")
    @Expose
    @PrimaryKey // ADDED as MUST have a primary key
    @NonNull // ADDED Room does not accept NULLABLE PRIMARY KEY
    private String foodId;
    @SerializedName("food_name")
    @Expose
    private String foodName;
    @SerializedName("food_image")
    @Expose
    private String foodImage;
    @SerializedName("food_kcal")
    @Expose
    private String foodKcal;
    @SerializedName("food_url")
    @Expose
    private String foodUrl;
    @SerializedName("food_description")
    @Expose
    private String foodDescription;
    @SerializedName("carb_percent")
    @Expose
    private String carbPercent;
    @SerializedName("protein_percent")
    @Expose
    private String proteinPercent;
    @SerializedName("fat_percent")
    @Expose
    private String fatPercent;
    @SerializedName("units")
    @Expose
    @Ignore // ADDED AS going to be a table
    private List<FoodUnitsData> units = null;

    @NonNull // ADDED (not reqd)
    public String getFoodId() {
        return foodId;
    }


    public void setFoodId(@NonNull /* ADDED @NonNull (not reqd)*/ String foodId) {
        this.foodId = foodId;
    }

    public String getFoodName() {
        return foodName;
    }

    public void setFoodName(String foodName) {
        this.foodName = foodName;
    }

    public String getFoodImage() {
        return foodImage;
    }

    public void setFoodImage(String foodImage) {
        this.foodImage = foodImage;
    }

    public String getFoodKcal() {
        return foodKcal;
    }

    public void setFoodKcal(String foodKcal) {
        this.foodKcal = foodKcal;
    }

    public String getFoodUrl() {
        return foodUrl;
    }

    public void setFoodUrl(String foodUrl) {
        this.foodUrl = foodUrl;
    }

    public String getFoodDescription() {
        return foodDescription;
    }

    public void setFoodDescription(String foodDescription) {
        this.foodDescription = foodDescription;
    }

    public String getCarbPercent() {
        return carbPercent;
    }

    public void setCarbPercent(String carbPercent) {
        this.carbPercent = carbPercent;
    }

    public String getProteinPercent() {
        return proteinPercent;
    }

    public void setProteinPercent(String proteinPercent) {
        this.proteinPercent = proteinPercent;
    }

    public String getFatPercent() {
        return fatPercent;
    }

    public void setFatPercent(String fatPercent) {
        this.fatPercent = fatPercent;
    }

    public List<FoodUnitsData> getUnits() {
        return units;
    }

    public void setUnits(List<FoodUnitsData> units) {
        this.units = units;
    }
}
  • The Foods class now has two uses:-
  1. as the class for extracting the JSON (where units will be populated with FoodUnitsData objects accordingly)
  2. as the model for the Room table.
  • See the comments

Second the FoodUnitsDataEntity class.

This is a new class that will be based upon the FoodUnitsData class but include two important values/columns not catered for by the FoodsUnitsData class, namely:-

  • a unique identifier that will be the primary key, and
  • a map/reference for establishing the relationship between a row and it’s parent in the Foods table. As this column will be used quite frequently (i.e. it is essential for making the relationship) it makes sense to have an index on the column (speeds up making the relationship (like an index in a book would speed up finding stuff))
  • as there is a relationship, it is wise to ensure that referential integrity is maintained. That is you don’t want orphaned units. As such a Foreign Key constraint is employed (a rule saying that the child must have a parent).
  • as it will be convenient to build/insert based upon a FoodUnitsData object then a constructor has been added that will create a FoodUnitsDataEnity object from a FoodUnitsData object (plus the all important Foods mapping/referencing/associating value).

So :-

/*
    NEW CLASS that:-
        Has a Unique ID (Long most efficient) as the primary Key
        Has a column to reference/map to the parent FoodUnitsData of the food that owns this
        Embeds the FoodUnitsData class
        Enforces referential integrity be defining a Foreign Key constraint (optional)
            If parent is delete then children are deleted (CASCADE)
            If the parent's foodId column is changed then the foodIdMap is updated in the children (CASCADE)
 */
@Entity(
        tableName = "food_units",
        foreignKeys = {
                @ForeignKey(
                        entity = Foods.class, /* The class (annotated with @ Entity) of the owner/parent */
                        parentColumns = {"foodId"}, /* respective column referenced in the parent (Foods) */
                        childColumns = {"foodIdMap"}, /* Column in the table that references the parent */
                        onDelete = CASCADE, /* optional within Foreign key */
                        onUpdate = CASCADE /* optional with foreign key */
                )
        }
)
class FoodUnitsDataEntity {
    @PrimaryKey
    Long foodUnitId = null;
    @ColumnInfo(index = true)
    String foodIdMap;
    @Embedded
    FoodUnitsData foodUnitsData;

    FoodUnitsDataEntity(){}
    FoodUnitsDataEntity(FoodUnitsData fud, String foodId) {
        this.foodUnitsData = fud;
        this.foodIdMap = foodId;
        this.foodUnitId = null;
    }
}

Third the FoodUnitsData class

This class is ok as it is. However, for the demo/example constructors were added as per :-

public class FoodUnitsData {
    @SerializedName("unit")
    @Expose
    private String unit;
    @SerializedName("amount")
    @Expose
    private String amount;
    @SerializedName("calory")
    @Expose
    private String calory;
    @SerializedName("calcium")
    @Expose
    private String calcium;
    @SerializedName("carbohydrt")
    @Expose
    private String carbohydrt;
    @SerializedName("cholestrl")
    @Expose
    private String cholestrl;
    @SerializedName("fiber_td")
    @Expose
    private String fiberTd;
    @SerializedName("iron")
    @Expose
    private String iron;
    @SerializedName("lipid_tot")
    @Expose
    private String lipidTot;
    @SerializedName("potassium")
    @Expose
    private String potassium;
    @SerializedName("protein")
    @Expose
    private String protein;
    @SerializedName("sodium")
    @Expose
    private String sodium;
    @SerializedName("vit_a_iu")
    @Expose
    private String vitAIu;
    @SerializedName("vit_c")
    @Expose
    private String vitC;

    /* ADDED Constructors */
    FoodUnitsData(){}
    FoodUnitsData(String unit,
                  String amount,
                  String calory,
                  String calcium,
                  String cholestrl,
                  String carbohydrt,
                  String fiberTd,
                  String iron,
                  String lipidTot,
                  String potassium,
                  String protein,
                  String sodium,
                  String vitAIu,
                  String vitC
    ){
        this.unit = unit;
        this.amount = amount;
        this.calory = calory;
        this.calcium = calcium;
        this.cholestrl = cholestrl;
        this.carbohydrt = carbohydrt;
        this.fiberTd = fiberTd;
        this.iron = iron;
        this.lipidTot = lipidTot;
        this.potassium = potassium;
        this.sodium = sodium;
        this.protein = protein;
        this.vitAIu = vitAIu;
        this.vitC = vitC;

    }
    /* Finish of ADDED code */


    public String getUnit() {
        return unit;
    }

    public void setUnit(String unit) {
        this.unit = unit;
    }

    public String getAmount() {
        return amount;
    }

    public void setAmount(String amount) {
        this.amount = amount;
    }

    public String getCalory() {
        return calory;
    }

    public void setCalory(String calory) {
        this.calory = calory;
    }

    public String getCalcium() {
        return calcium;
    }

    public void setCalcium(String calcium) {
        this.calcium = calcium;
    }

    public String getCarbohydrt() {
        return carbohydrt;
    }

    public void setCarbohydrt(String carbohydrt) {
        this.carbohydrt = carbohydrt;
    }

    public String getCholestrl() {
        return cholestrl;
    }

    public void setCholestrl(String cholestrl) {
        this.cholestrl = cholestrl;
    }

    public String getFiberTd() {
        return fiberTd;
    }

    public void setFiberTd(String fiberTd) {
        this.fiberTd = fiberTd;
    }

    public String getIron() {
        return iron;
    }

    public void setIron(String iron) {
        this.iron = iron;
    }

    public String getLipidTot() {
        return lipidTot;
    }

    public void setLipidTot(String lipidTot) {
        this.lipidTot = lipidTot;
    }

    public String getPotassium() {
        return potassium;
    }

    public void setPotassium(String potassium) {
        this.potassium = potassium;
    }

    public String getProtein() {
        return protein;
    }

    public void setProtein(String protein) {
        this.protein = protein;
    }

    public String getSodium() {
        return sodium;
    }

    public void setSodium(String sodium) {
        this.sodium = sodium;
    }

    public String getVitAIu() {
        return vitAIu;
    }

    public void setVitAIu(String vitAIu) {
        this.vitAIu = vitAIu;
    }

    public String getVitC() {
        return vitC;
    }

    public void setVitC(String vitC) {
        this.vitC = vitC;
    }
}

Fourth DaoAccess

Obviously inerts/updates/ deletes for the new FoodUnitsDataEntity should be added. However note that existing ones have been changed to not return void but instead long for inserts and int for updates deletes.

  • inserts return eithr -1 or the rowid (a hidden column that all tables (if using Room) will have that uniquely identifies the inserted row). So if it’s -1 then row not inserted (or < 0).
  • delete and updates return the number of affected (updated/deleted) rows.

It would be beneficial to be able to pass a Food object and insert all the units rows. As this requires a method with a body instead of an interface an abstract class will be used.

So DaoAccess becomes :-

@Dao
public /* CHANGED TO abstract class from interface */ abstract class DaoAccess {
    @Query("SELECT * FROM food_data")
    abstract List<Foods> getAll();

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract long insert(Foods task);
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract long insert(FoodUnitsDataEntity foodUnitsDataEntity); 

    @Delete
    abstract int delete(Foods task);
    @Delete
    abstract int delete(FoodUnitsDataEntity foodUnitsDataEntity);

    @Update
    abstract int update(Foods task);
    @Update
    abstract int update(FoodUnitsDataEntity foodUnitsDataEntity);

    @Query("") /* Trick Room to allow the use of @Transaction*/
    @Transaction
    long insertFoodsWithAllTheFoodUnitsDataEntityChildren(Foods foods) {
        long rv = -1;
        long fudInsertCount = 0;
        if (insert(foods) > 0) {
          for(FoodUnitsData fud: foods.getUnits()) {
              if (insert(new FoodUnitsDataEntity(fud,foods.getFoodId())) > 0) {
                  fudInsertCount++;
              }
          }
          if (fudInsertCount != foods.getUnits().size()) {
              rv = -(foods.getUnits().size() - fudInsertCount);
          } else {
              rv = 0;
          }
        }
        return rv;
    }
}

Fifth FoodDatabase

Just add the FoodUnitsDataEntity as an entity :-

@Database(entities = {Foods.class, FoodUnitsDataEntity.class /*<<<<<<<<<< ADDED*/}, version = 1)
public abstract class FoodDatabase extends RoomDatabase {
    public abstract DaoAccess daoAccess(); 
}

Sixth testing the above in an Activity MainActivity

This activity will :-

  1. Build a Foods object with some embedded FoodUnitsData.
  2. Save it as a JSON string, extract it from the JSON string (logging the JSON string)
  3. get an instance of the database.
  4. get an instance of the DaoAccess.
  5. use the insertFoodsWithAllTheFoodUnitsDataEntityChildren method to insert the Foods and the assoctiated/related children.

as per :-

public class MainActivity extends AppCompatActivity {

    FoodDatabase fooddb;
    DaoAccess foodDao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /* Build data to test */
        Foods foods = new Foods();
        foods.setFoodId("MyFood");
        foods.setCarbPercent("10.345");
        foods.setFoodDescription("The Food");
        foods.setFatPercent("15.234");
        foods.setFoodImage("The Food Image");
        foods.setFoodKcal("120");
        foods.setFoodName("The Food");
        foods.setFoodUrl("URL for the Food");
        foods.setProteinPercent("16.234");
        foods.setUnits(Arrays.asList(
                new FoodUnitsData("100","15","1200","11","12","13","14","15","16","17","18","19","20","21"),
                new FoodUnitsData("1001","151","12001","11","12","13","14","15","16","17","18","19","20","21"),
                new FoodUnitsData("1002","152","12002","11","12","13","14","15","16","17","18","19","20","21")
        ));

        String json = new Gson().toJson(foods);
        Log.d("JSONINFO",json);
        Foods foodsFromJSON = new Gson().fromJson(json,Foods.class);

        fooddb = Room.databaseBuilder(this,FoodDatabase.class,"food.db")
                .allowMainThreadQueries()
                .build();
        foodDao = fooddb.daoAccess();
        foodDao.insertFoodsWithAllTheFoodUnitsDataEntityChildren(foodsFromJSON);
    }
}

Results after running the App

The log includes :-

D/JSONINFO: {"carb_percent":"10.345","fat_percent":"15.234","food_description":"The Food","food_id":"MyFood","food_image":"The Food Image","food_kcal":"120","food_name":"The Food","food_url":"URL for the Food","protein_percent":"16.234","units":[{"amount":"15","calcium":"11","calory":"1200","carbohydrt":"13","cholestrl":"12","fiber_td":"14","iron":"15","lipid_tot":"16","potassium":"17","protein":"18","sodium":"19","unit":"100","vit_a_iu":"20","vit_c":"21"},{"amount":"151","calcium":"11","calory":"12001","carbohydrt":"13","cholestrl":"12","fiber_td":"14","iron":"15","lipid_tot":"16","potassium":"17","protein":"18","sodium":"19","unit":"1001","vit_a_iu":"20","vit_c":"21"},{"amount":"152","calcium":"11","calory":"12002","carbohydrt":"13","cholestrl":"12","fiber_td":"14","iron":"15","lipid_tot":"16","potassium":"17","protein":"18","sodium":"19","unit":"1002","vit_a_iu":"20","vit_c":"21"}]}

Using App Inspection (Database Inspector) :-

enter image description here

and

enter image description here

Advertisement