For now, I have a working ListView that successfully loads every “parent” object in the ListView, retrieving the parent attributes and showing them in every element. Due to I am trying to follow Android’s Model-View-ViewModel architecture patterns, I am using RoomDb, a Repository class and a ViewModel class (viewmodel class holds logic for both parent and children; I wonder if this is an antipattern or not; but that might be a subject for another question).
Anyway, due to I have to make the queries retrieving LiveData objects that hold lists of POJOs, this limits the data I can show on the ListView to only parent’s fields, because of the POJO only contains parent table’s fields; and making a query for every single parentID to retrieve all the child objects in some kind of loop doesn’t sound good for some reason. So, while searching about this on the internet, I found (but I don’t remember where) this way of making some kind of “Relational Entity” that holds both parent and children:
import androidx.room.Embedded; import androidx.room.Relation; import java.io.Serializable; import java.util.List; public class ParentWithChildren implements Serializable { @Embedded public Parent parent; @Relation( parentColumn = "id", entityColumn = "parent_id" ) public List<Child> children; }
Then, from my ParentDao, I added this:
// XXX The old DAO searcher that already works for retrieving // just the parent POJOs @Query("SELECT * FROM parent WHERE parent_field1 = :search OR parent_field2 = :search_no_accents") LiveData<List<Parent>> searchParents(String search, String search_no_accents); // XXX The new method I'd like to create @Transaction @Query("SELECT * FROM parent WHERE parent_field1 = :search OR parent_field2 = :search_no_accents") LiveData<List<ParentWithChildren>> searchParentsWithChildren(String search, String search_no_accents);
And, in my Repository class:
public LiveData<List<ParentWithChildren>> searchParentsWithChildren(String search, String search_no_accents) { return parent_dao.searchParentsWithChildren(search, search_no_accents); }
…and here is my ParentViewModel’s method to retrieve the list from the Repository:
public LiveData<List<ParentWithChildren>> searchParentsWithChildren(String search) { // I remove accents from user search with and ad-hoc method String search_no_accents = removeAccents(search); return repository.searchParentsWithChildren(search, search_no_accents); }
In my MainActivity, I have a SearchView that handles the user’s search:
SearchView search_view = findViewById(R.id.searchView); search_view.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { if (query.equals("")) { loadFullListFragment(); } else { search_results = parent_viewmodel.searchParents(query); //search_with_children = parent_viewmodel.searchParentsWithChildren(query); observeSearchResults(search_results); //observeSearchResults(search_with_children); } return true; } });
My observeSearchResults
method, also in MainActivity, loads the appropiate fragment depending on the size of the result’s list:
private void observeSearchResults(LiveData<List<Parent>> parents_results) { //private void observeSearchResults(LiveData<List<ParentWithChildren>> parents_with_children) { parents_results.observe(this, new Observer<List<Parent>>() { //parents_with_children.observe(this, new Observer<List<ParentWithChildren>>() { @Override //public void onChanged(List<ParentWithChildren> parents) { public void onChanged(List<Parent> parents) { if ( parents.size() > 1 ) { // ## List view of multiple results listFragment = ParentsListFragment.newInstance( (ArrayList)parents ); transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragmentContainer, listFragment); transaction.addToBackStack(null); transaction.commit(); } else if ( parents.size() > 0 ) { // TODO XXX This is kinda hacky loadParentProfileFragment(parents.get(0)); } else { transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragmentContainer, insertFragment); transaction.addToBackStack(null); transaction.commit(); } } }); }
Well, you see those commented-out lines of code? It’s the new changes I want to introduce to start using the new ParentWithChildren entity. They are commented-out because the ones directly above or below them are the ones that are already working to show parent’s objects in ListView. So, when I try to implement the changes, I comment-out the old ones and “dis-comment-out” the ones with ParentWithChildren
changes. I have to change so many classes to implement one single change… Hmmm… it smells like Shotgun Surgery here… 🙁
…well, long story short, I try to replace in the Fragment that loads the ListView the previous List<Parent>
with List<ParentWithChildren>
, so every list element will be able to retrieve also the parent related child objects and show them. But, the last time I tried to implement ParentWithChildren
entity this way, it crashed with this Exception (which absolutely blows my mind):
java.lang.NoClassDefFoundError: Failed resolution of: com/mydomain/myapp/daos/ParentDao_Impl$5;
The Traceback of this exception started at some line in this auto-generated ParentDao_Implement
, and the next line of the Traceback pointed at my new Repository.searchParentsWithChildren
method. I absolutely don’t know if either I am doing it wrong, or how to deal with this strange failure.
My ArrayAdapter and what I’d like it to become
This snippet shows my currently working ArrayAdapter, and commented-out lines of code that show more or less what changes I want to introduce:
public class ParentsArrayAdapter extends ArrayAdapter<Parent> { //public class ParentsArrayAdapter extends ArrayAdapter<ParentWithChildren> { //public ParentsArrayAdapter(Context context, List<ParentWithChildren> parents) { public ParentsArrayAdapter(Context context, List<Parent> parents) { super(context, 0, parents); } @Override public View getView(int position, View convertView, ViewGroup vgroup) { // Get the data item for this position Parent parent = getItem(position); //ParentWithChildren parent = getItem(position); // XXX TODO Load child attributes list // Check if an existing view is being reused, otherwise inflate the view if (convertView == null) { convertView = LayoutInflater.from(getContext()) .inflate(R.layout.listview_parents, vgroup, false); } // Lookup view for data population TextView parentField1 = convertView.findViewById(R.id.list_parentField1); // Populate the data into the template view using the data object parentField1.setText(parent.getField1()); //parentField1.setText(parent.parent.getField1()); TextView parentField2 = convertView.findViewById(R.id.list_parentField2); parentField2.setText(parent.getField2()); //pinyinText.setText(parent.parent.getField2()); // TODO Load first 5-10 childs here (when ParentWithChildren works) TextView childrenCommaListField = convertView.findViewById(R.id.list_childsCommaList); /* ¿¿?? SebasSBM is confused */ // Return the completed view to render on screen return convertView; } }
In a PC app, this would be as simple as doing some kind of sqlSELECT p.*, c.* FROM parent AS p INNER JOIN child as c ON p.id = c.parent_id WHERE bla bla bla
: but with RoomDB I have the sensation everything with relational databases is way much more complicated.
So, how can I make the ListView load children’s fields in every parent list’s element,so it would look like this more or less? Any help is appreciated:
sorry, I am terrible making examples sometimes ------------ parent1 ---> parent_field1 ---> child1, child2, child3 ------------ parent2 ---> parent_field1 ---> child4, child5, child6
Database’s Table Structure (more or less)
Parent table +------+--------+--------+- | ID | field1 | field2 | [...] +------+--------+--------+- | | | | +------+--------+--------+- Child table (relation One2Many by FK) +------+-----------+--------+---------+- | ID | parent_id | field1 | field2 | [...] +------+-----------+--------+---------+- | | | | | +------+-----------+--------+---------+-
Advertisement
Answer
In a PC app, this would be as simple as doing some kind of sql SELECT p., c. FROM parent AS p INNER JOIN child as c ON p.id = c.parent_id WHERE bla bla bla: but with RoomDB I have the sensation everything with relational databases is way much more complicated.
You can use do this with room just have a POJO that has
@Embedded Parent parent; @Embedded Child child;
It becomes a little harder if the are any column names that are the same as you need to disambiguate the columns. You can use @Embedded(prefix = “a suitable prefix”).
Here’s a working example.
The Parent class/entity :-
@Entity class Parent { public static final String PREFIX = "parent_"; @PrimaryKey Long id=null; String name; Parent(){} @Ignore Parent(String name) { this.name = name; } }
The Child class/entity :-
@Entity class Child { public static final String PREFIX = "child_"; @PrimaryKey Long id = null; long parent; String name; Child(){} @Ignore Child(long parent,String name) { this.parent = parent; this.name = name; } }
The POJO for the cartesian product of joining parent with child :-
class ParentChildCartesian { @Embedded(prefix = Parent.PREFIX) Parent parent; @Embedded(prefix = Child.PREFIX) Child child; }
The Dao’s (in an abstract class rather than interface, so abstract …. for dao methods):-
@Dao abstract class AllDao { @Insert(onConflict = OnConflictStrategy.IGNORE) abstract long insert(Parent parent); @Insert(onConflict = OnConflictStrategy.IGNORE) abstract long insert(Child child); @Query("SELECT parent.id AS " + Parent.PREFIX + "id, " + "parent.name AS " + Parent.PREFIX + "name, " + "child.id AS " + Child.PREFIX + "id," + "child.parent AS " + Child.PREFIX + "parent," + "child.name AS " + Child.PREFIX + "name " + " FROM parent JOIN child ON child.parent = parent.id" ) abstract List<ParentChildCartesian> getParentChildCartesianList(); }
Obviously a suitable @Database class, in this it is TheDatabase (no need to show).
Finally, demonstrating the above being used:-
db = TheDatabase.getInstance(this); dao = db.getAllDao(); long p1 = dao.insert(new Parent("Parent1")); long p2 = dao.insert(new Parent("Parent2")); dao.insert(new Child(p1,"Child1")); dao.insert(new Child(p1,"Child2")); dao.insert(new Child(p1,"Child3")); dao.insert(new Child(p2,"Child4")); for(ParentChildCartesian pc: dao.getParentChildCartesianList()) { Log.d( "DBINFO", "Parent name is " + pc.parent.name + " id is" + pc.parent.id + ". Child name is " + pc.child.name + " id is " + pc.child.id + " parent id is " + pc.child.parent); }
The result in the log :-
D/DBINFO: Parent name is Parent1 id is1. Child name is Child1 id is 1 parent id is 1 D/DBINFO: Parent name is Parent1 id is1. Child name is Child2 id is 2 parent id is 1 D/DBINFO: Parent name is Parent1 id is1. Child name is Child3 id is 3 parent id is 1 D/DBINFO: Parent name is Parent2 id is2. Child name is Child4 id is 4 parent id is 2
However, I think you want all the children’s name with with parent in a single row. This can utilise the PJO above but a different query that GROUP’s the data allow the use of the group_concat aggregate function e.g. :-
@Query("SELECT parent.id AS " + Parent.PREFIX + "id, " + "parent.name AS " + Parent.PREFIX + "name, " + "child.id AS " + Child.PREFIX + "id," + /* note will be an arbitrary id */ "child.parent AS " + Child.PREFIX + "parent," + "group_concat(child.name) AS " + Child.PREFIX + "name " + "FROM parent " + "JOIN child ON child.parent = parent.id " + "GROUP BY parent.id" ) abstract List<ParentChildCartesian> getParentwithCSV();
When used with :-
for (ParentChildCartesian pc: dao.getParentwithCSV()) { Log.d( "DBINFO", "Parent is " + pc.parent.name+ " children are " + pc.child.name ); }
The Log includes :-
D/DBINFO: Parent is Parent1 children are Child1,Child2,Child3 D/DBINFO: Parent is Parent2 children are Child4
You don’t need to use @Embedded, this conveniently includes the member variables of the object.
So consider the POJO :-
class WithoutEmbedded { long parentId; String parentName; String childrenNameCSV; String childrenIdCSV; }
You have to match the member variable names with column names so you could have, for example :-
@Query("SELECT parent.id AS parentId, " + "parent.name AS parentName, " + "group_concat(child.name) AS childrenNameCSV, " + "group_concat(child.id) AS childrenIdCSV " + "FROM parent " + "JOIN child ON child.parent = parent.id " + "GROUP BY parent.id" ) abstract List<WithoutEmbedded> getParentsWithchildrenCSVandChildrenIdCSV();
Using :-
for(WithoutEmbedded pc: dao.getParentsWithchildrenCSVandChildrenIdCSV()) { Log.d( "DBINFO", "Parent is " + pc.parentName + " children are " + pc.childrenNameCSV + " children ids are " + pc.childrenIdCSV ); }
The result in the log would be :-
D/DBINFO: Parent is Parent1 children are Child1,Child2,Child3 children ids are 1,2,3 D/DBINFO: Parent is Parent2 children are Child4 children ids are 4
In short @Relation does the equivalent of a join by using an underlying queries for ALL related children and hence the recommendation for @Transaction.
Whilst @Embedded expects the result of the query to have columns that match the member variable names of the embedded classes and therefore JOINS can be used (including filtering). As it’s all done in a single query there is no need for an @Transaction.
@Embedded is basically a convenience so can be omitted by including member variables manually. (again no need for @Transaction).
Additional
Re comments. It is now a little clearer what you want. In short your original issue was the clue. You can use @Embedded and @Realtion for what you want (if I understand correctly).
e.g. from a working example :-
Mary and Fred are parents (rows repeated due to a few test runs).
The adapter should be along the lines of :-
class MyAdapter extends ArrayAdapter<ParentWithChildren> ....
where ParentWithChildren is the @Embedded …. @Realation POJO e.g. :-
class ParentWithChildren { @Embedded Parent parent; @Relation( entity = Child.class, parentColumn = Parent.COL_PARENT_ID, entityColumn = Child.COL_PARENT_MAP ) List<Child> children; }
Child being (in the case of the example) :-
@Entity(tableName = Child.TABLE_NAME, foreignKeys = { @ForeignKey( entity = Parent.class, parentColumns = {Parent.COL_PARENT_ID}, childColumns = {Child.COL_PARENT_MAP}, onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE ) } ) class Child { public static final String TABLE_NAME = "child"; public static final String COL_CHILD_ID = "childid"; public static final String COL_PARENT_MAP = "parentmap"; public static final String COL_CHILD_NAME = "childname"; @PrimaryKey @ColumnInfo(name = COL_CHILD_ID) Long childId = null; @ColumnInfo(name = COL_PARENT_MAP) Long parentMap; @ColumnInfo(name = COL_CHILD_NAME, index = true) String childName; Child(){} @Ignore Child(String name, long parentMap) { this.childName = name; this.parentMap = parentMap; } }
and Parent :-
@Entity(tableName = Parent.TABLE_NAME) class Parent { public static final String TABLE_NAME = "parent"; public static final String COL_PARENT_ID = "parentid"; public static final String COL_PARENT_NAME = "parentname"; @PrimaryKey @ColumnInfo(name = COL_PARENT_ID) Long parentId=null; @ColumnInfo(name = COL_PARENT_NAME, index = true) String parentName; Parent(){} @Ignore Parent(String name) { this.parentName = name; } }
the @Dao class AllDao :-
@Dao abstract class AllDao { @Insert abstract long insert(Parent parent); @Insert abstract long insert(Child child); @Transaction @Query("SELECT * FROM parent") abstract List<ParentWithChildren> getAllParentsWithChildren(); }
The @Database TheDatabase being :-
@Database(entities = {Parent.class,Child.class},version = TheDatabase.DATABASE_VERSION) abstract class TheDatabase extends RoomDatabase { abstract AllDao getAllDao(); public static final String DATABASE_NAME = "my.db"; public static final int DATABASE_VERSION = 1; private static volatile TheDatabase instance = null; public static TheDatabase getInstance(Context context) { if (instance == null) { instance = Room.databaseBuilder(context,TheDatabase.class,DATABASE_NAME) .allowMainThreadQueries() .build(); } return instance; } }
The layout for the adapter parentwithchildre.xml for up to 5 children :-
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/parent_name" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" > </TextView> <TextView android:id="@+id/child1" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" > </TextView> <TextView android:id="@+id/child2" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" > </TextView> <TextView android:id="@+id/child3" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" > </TextView> <TextView android:id="@+id/child4" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" > </TextView> <TextView android:id="@+id/child5" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" > </TextView> </LinearLayout>
The activity MainActity being :-
public class MainActivity extends AppCompatActivity { TheDatabase db; AllDao dao; ListView lv1; MyAdapter adapter; List<ParentWithChildren> baseList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); lv1 = this.findViewById(R.id.lv1); db = TheDatabase.getInstance(this); dao = db.getAllDao(); long p1 = dao.insert(new Parent("Mary")); long p2 = dao.insert(new Parent("Fred")); dao.insert(new Child("Tom",p1)); dao.insert(new Child("Alice",p1)); dao.insert(new Child("Jane",p1)); dao.insert(new Child("Bob",p1)); dao.insert(new Child("Susan",p2)); dao.insert(new Child("Alan",p2)); setOrRefreshAdapter(); setOrRefreshAdapter(); } private void setOrRefreshAdapter() { baseList = dao.getAllParentsWithChildren(); if (adapter == null) { adapter = new MyAdapter(this,R.layout.parentwithchildren, baseList); lv1.setAdapter(adapter); } else { adapter.clear(); adapter.addAll(baseList); adapter.notifyDataSetChanged(); } } }
and last but most MyAdapter :-
class MyAdapter extends ArrayAdapter<ParentWithChildren> { public MyAdapter(@NonNull Context context, int resource, @NonNull List<ParentWithChildren> objects) { super(context, resource, objects); } @NonNull @Override public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { ParentWithChildren pwc = getItem(position); if (convertView == null) { convertView = LayoutInflater.from(getContext()).inflate( R.layout.parentwithchildren,parent,false ); TextView parent_textview = convertView.findViewById(R.id.parent_name); parent_textview.setText(pwc.parent.parentName); TextView child1_textview = convertView.findViewById(R.id.child1); TextView child2_textview = convertView.findViewById(R.id.child2); TextView child3_textview = convertView.findViewById(R.id.child3); TextView child4_textview = convertView.findViewById(R.id.child4); TextView child5_textview = convertView.findViewById(R.id.child5); if(pwc.children.size() >= 1) { child1_textview.setText(pwc.children.get(0).childName); } if (pwc.children.size() >= 2) { child2_textview.setText(pwc.children.get(1).childName); } if (pwc.children.size() >=3) { child3_textview.setText(pwc.children.get(2).childName); } if (pwc.children.size() >= 4) { child4_textview.setText(pwc.children.get(3).childName); } if (pwc.children.size() >= 5) { child5_textview.setText(pwc.children.get(4).childName); } } return convertView; //return super.getView(position, convertView, parent); } }
So with a List being passed in the getView you get a ParentWithChild object, below it’s named pwc as in ParentWithChildren pwc = getItem(position);
(i.e the nth ParentWithChildren in the List)
The parent’s name is pwc.parent.parentName
The children’s name are obtained using :-
pwc.children.get(?).childName
where ? is a number 0 to the number of children but you would limit the value to the number of views – 1.