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

2

2

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
}

Pilar answered 22/1, 2022 at 18:32 Comment(1)
Have you read the documentation?: developer.android.com/training/data-storage/room/…Concessionaire
R
3

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

Rightness answered 22/1, 2022 at 20:32 Comment(14)
by the way, I'm so sorry.. your old answer was deleted because I have double questionsPilar
Working on the answer. No problem re the deletion have the working example in Android Studio.Rightness
If you want, we can share all the answers under this question, such as for sqlite, for room.. In this way, it reaches more people. here: #70625429 deleted question: #70790607Pilar
I'm testing this solution, #70790607Pilar
@test721 What I would suggest is leaving the SQLite question and answer as it is other than you Ticking it to indicate that it was useful.Rightness
I did.. there is one more problem.. these queries are very slow for (int i = 0; i < foodDao.getAll().size(); i++) { Log.d("TAG_JSON_FOOD_DB", "---: "+foodDao.getAll().get(i).getFoodName()); }Pilar
Because you are doing x individual queries when it could be a single query. So first you get ALL the foods to get the number of Foods to then go an get all the Foods for each Foods. List<Foods> theFoods = foodDao.getAll; then loop through theFoods is all you need. However queries etc should be new questions as you should at least be able to store and retrieve data etc now.Rightness
github.com/theoyuncu8/roomexample the whole example is here, why did you use long statement instead of void in insert operation? there are many structures that are still complexPilar
also getAll only returns the values ​​of the Foods class, when I get the values ​​of FoodUnits it gives an null pointer errorPilar
@test721 long explained in Four DaoAccessRightness
@test721 null because at present it's one-way as the answer was just to explain the storing of the nested objects. Typically one would have a POJO (e.g. FoodWithUnits) that embeds the Foods (e.g. @Embedded Foods food) and uses @Relation for List<FoodUnitsDataEntity>. However, you could included a method in the Foods class to populate units. Really another question.Rightness
It is really difficult to create this structure with sql. There is a library called Realm, everything is very easy, but updating and deleting processes are slower. Also, I can't trust it because it's a 3rd party library, even if it has MongoDB behind it. @Rightness Please review this; github.com/AlexeyZatsepin/Android-ORM-benchmarkPilar
developer.android.com/training/data-storage/room/…Pilar
Can you help me for this topic; #70838703Pilar
I
0

Separate them to 2 entities, than create a relation class. This relation class uses FoodListModel as embedded property that has relation to UnitList as List.

Irmgardirmina answered 22/1, 2022 at 18:49 Comment(8)
How do I connect the two after doing it that way?Pilar
You can use the relation class at the Dao class instead using FoodClassModel class.Irmgardirmina
Can you give a sample project? I already did that, see: github.com/theoyuncu8/roomdbPilar
@test721 if you look at your previous questions. you will notice that a working example has been provided. In short @Ignore private List<FoodUnitsData> units = null; in Foods. Along with FoodUnitsData being @Embedded into FoodUnitsDataEntity with 2 additional member variables one for the id of the FoodUnitsDataEntity, the other for the mapping to the parent Foods.Rightness
@Rightness your example is valid for sql queries, but a structure like converter is used in RoomPilar
@test721 look again, there is a working example using Room (the second/duplicate question) that I added recently.Rightness
please give me link?Pilar
Uhm I think it's been deleted it along with the working example. I will expand this answer to include the working example.Rightness

© 2022 - 2024 — McMap. All rights reserved.