Issue
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
}
Solution
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:-
- as the class for extracting the JSON (where units will be populated with FoodUnitsData objects accordingly)
- 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 :-
- Build a Foods object with some embedded FoodUnitsData.
- Save it as a JSON string, extract it from the JSON string (logging the JSON string)
- get an instance of the database.
- get an instance of the DaoAccess.
- 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) :-
and
Answered By - MikeT
Answer Checked By - Timothy Miller (JavaFixing Admin)