Search code examples
javaandroiddatabaseandroid-room

how to use lists in room db android java


I want to use room

I want to use a class for the insert method, one of the parameters of which is the title, which is ok, and the second one is a list of edit texts that ruin this work, when I call the insert method, the program crashes, what should I do, I don't know how to use a type converter. And this or what?

i use type converter and it is not working maybe the problem is my mine


Solution

  • how to use lists in room db android java

    There are two primary ways, either

    1. store the list via a type converter, which converts the object as a single representation of the object to a handled type. or,
      1. SQLite and therefore room can handle INTEGER types (int, long, Integer, Long and others, even boolean), REAL types (double, float, Double, Float and others), TEXT such as String or BLOB byte arrays, byte streams.
    2. via a related row in another table

    As SQLite is a relational database, then from a database approach, the latter would typically be used. However, from an object orientated approach the former is often used. The former is more likely to conform to normalisation. The latter tends to initially be simpler BUT can introduce complexities.

    TypeConverters

    If storing a representation of an unhandled type (object) then 2 TypeConverter annotated methods, in an interface or abstract class are required.

    To

    1. convert the object into a single representation of the object, when storing the data in the database, and
    2. convert the JSON string into the object when retrieving the data from the database.

    To try to represent an object as a numeric would be quite complex if at all even possible. so INTEGER and REAL can be ruled out.

    A serialised representation could be stored as a BLOB, but typically this is not used.

    Typically, as libraries exist, a JSON String representation is utilised.

    • This encapsulates the various components/fields/values/attributes/properties within various paired characters e.g. [ and ]. This adds extra data to be stored which is often repeated and thus conflicts with normalisation. Additionally functionality such as searching for such values can be quite complex.

    Example

    • Note this example incorporates BOTH methodologies, so there are some complexities (however understanding them could well be an advantage)

    As an example consider an object that, consists of a Title and two lists,

    1. a list of Strings (List) and the other,
    2. a list of TheObjects List

    So a class and @Entity annotated class (as it will be a table in the database) TheTable :-

    @Entity
    class TheTable {
        @PrimaryKey
        @NonNull
        String title;
        List<String> stringList;
        ListOfObjects objectList;
    
        TheTable(){}
        @Ignore
        TheTable(String title, List<String> stringList, ListOfObjects objectList) {
            this.title = title;
            this.stringList = stringList;
            this.objectList = objectList;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public List<String> getStringList() {
            return stringList;
        }
    
        public void setStringList(List<String> stringList) {
            this.stringList = stringList;
        }
    
        public ListOfObjects getObjectList() {
            return objectList;
        }
    
        public void setObjectList(ListOfObjects objectList) {
            this.objectList = objectList;
        }
    }
    
    • @Ignore supresses Room's warning about multiple constructors in this case

    To be able to store the 2 Lists, as explained, the MUST be converted via Type Converters. In this case the @TypeConverter annotated methods are in the TypeConverters class:-

    class TypeConverters {
        private static final String separator = "~~";
        @TypeConverter
        public String convertFromListString(List<String> stringList) {
            StringBuilder rv = new StringBuilder();
            boolean afterFirst = false;
            for (String s: stringList) {
                if (afterFirst) rv.append(separator);
                afterFirst = true;
                rv.append(s);
            }
            return rv.toString();
        }
        @TypeConverter
        public List<String> convertToString(String string) {
            ArrayList<String> rv = new ArrayList<>();
            boolean afterFirst = false;
            for(String s: string.split(separator)) {
                rv.add(s);
            }
            return rv;
        }
    
        @TypeConverter
        public String convertFromTheObjectListToJSONString(ListOfObjects objectList) {
            return new Gson().toJson(objectList);
        }
        @TypeConverter
        public ListOfObjects convertFromJSONStringToObjectList(String JSONString) {
            return new Gson().fromJson(JSONString,ListOfObjects.class);
        }
    }
    
    • The first pair is an example of using/building/splitting a list of Strings using a separator (in this case ~~ (commas could be problematic if a string contained commas)).
    • The second pair using the com.google.code.gson JSON/GSOB library.

    As this answer comes from a bias towards the database aspect and thus that JSON is seen as wasteful. The due to the limited use the List of TheObject is within a ListOfObjects class as per:-

    class ListOfObjects {
       List<TheObject> objectList;
    
       ListOfObjects(){}
       @Ignore
       ListOfObjects(List<TheObject> objectList) {
          this.objectList = objectList;
       }
    
       public List<TheObject> getObjectList() {
          return objectList;
       }
    
       public void setObjectList(List<TheObject> objectList) {
          this.objectList = objectList;
       }
    }
    

    As the answer shows both approaches and then that the TheObject class has to suit both being a table and being a stored object then it is a little complex, as per:-

    @Entity(primaryKeys = {"objectName","objectNumber"})
    class TheObject {
       @NonNull
       String objectName;
       long objectNumber;
       String parentInTheTable = "";
       TheObject(){}
       @Ignore
       TheObject(String objectName,long objectNumber){
          this.objectName = objectName;
          this.objectNumber = objectNumber;
       }
       @Ignore
       TheObject(String objectName, long objectNumber, String parentInTheTable) {
          this.objectName = objectName;
          this.objectNumber = objectNumber;
          this.parentInTheTable = parentInTheTable;
       }
    
       public long getObjectNumber() {
          return objectNumber;
       }
    
       public String getObjectName() {
          return objectName;
       }
    
       public void setObjectNumber(long objectNumber) {
          this.objectNumber = objectNumber;
       }
    
       public void setObjectName(String objectName) {
          this.objectName = objectName;
       }
    }
    
    • As can be seen it is annotated with @Entity so it can be a table in the database.
    • To further complicate matters, the PrimaryKey is a composite (comprised of multiple columns), not that it need be.
    • Room will complain about the potential of a primary key value being null hence the @NonNull annotation.
    • The @ColumnInfo annotation creates an index on the parentInTheTable column as that would very likely improve efficiency. Note the the value is irrelevant/not required at all for the non-relational method and hence a default value of an empty string.
    • constructors to suit both methodologies.

    To utilise either method then access to the database will be required and this is through a single @Dao annotated interface TheDAOs:-

    @Dao
    interface TheDAOs {
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        long insert(TheTable theTable);
        @Query("SELECT * FROM thetable")
        List<TheTable> getAllTheTableRows();
    
        /* For DB Relationship */
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        long insert(TheObject theObject);
        @Transaction /* coded to supress the Room warning */
        @Query("SELECT * FROM thetable")
        List<TheTableWithALLRelatedTheObjectsPOJO> getAllTheTablerowsWithTheRelatedTheObjects();
    }
    
    • The first pair (insert and query) is utilised (or could be) by both methodologies
    • the second pair are for the relational database methodology

    As can be seen another class TheTableWithALLRelatedTheObjectsPOJO has appeared, to suit the relational database methodology. This is:-

    class TheTableWithALLRelatedTheObjectsPOJO {
       @Embedded
       TheTable theTable;
       @Relation(entity = TheObject.class,
               parentColumn = "title",
               entityColumn = "parentInTheTable")
       List<TheObject> objectList;
    }
    
    • it should be noted that Type Converters are not required as an TheObject object is comprised of handled types (String-->TEXT and long-->INTEGER).

    All that remains is to bring this all together in an @Database annotated abstract class AND for the non-relational database methodology is to introduce(inform Room where they are) the Type Converters.

    • the @TypeConverters(listOfclasses) is used. This can be coded at various places, the place defining the scope. Placed at the @Database annotation offers the fullest scope.

    So the TheDatabase abstract class is:-

    @TypeConverters({a.a.so78349925javaroomlists.TypeConverters.class})
    @Database(entities = {TheTable.class,TheObject.class},exportSchema = false,version = 1)
    abstract class TheDatabase extends RoomDatabase {
        abstract TheDAOs getTheDAOs();
    
        private static volatile TheDatabase instance;
        public static TheDatabase getInstance(Context context) {
            if (instance==null) {
                instance = Room.databaseBuilder(context,TheDatabase.class,"the_database.db")
                        .allowMainThreadQueries()
                        .build();
            }
            return instance;
        }
    }
    
    • Note, for brevity, the .allowMainThreadQueries has been used. This would not be used for a production App but simplifies the code.

    To put all the above actual use then some Activity code (a little long winded to go step by step through adding an retrieving data):-

    public class MainActivity extends AppCompatActivity {
    
        TheDatabase db;
        TheDAOs dao;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            db = TheDatabase.getInstance(this); /* NOTE this does not create the database */
            dao = db.getTheDAOs(); /* Database will be created WHEN an attempt is made to access the database i.e. here */
            ArrayList<String> test = new ArrayList<>();
            test.add("S1");
            test.add("s2");
            test.add("S3");
            ArrayList<TheObject> testObjectList = new ArrayList<>();
            testObjectList.add(new TheObject("MyFirstObject",100));
            testObjectList.add(new TheObject("MySecondObject",200));
            dao.insert(new TheTable("TITLE1",test,new ListOfObjects(testObjectList)));
    
            /* Add the same list>TheObject> data via the related TheObject table */
            for (TheObject to : testObjectList) {
                dao.insert(new TheObject(to.objectName,to.objectNumber,"TITLE1"));
            }
    
            /* Some more Data (another set)*/
            test.clear();
            test.add("SS100");
            test.add("SS200");
            testObjectList.clear();
            testObjectList.add(new TheObject("MyThirdObject",300));
            testObjectList.add(new TheObject("MyFourthObject",400));
            testObjectList.add(new TheObject("MyFifthObject",500));
            dao.insert(new TheTable("Title2",test,new ListOfObjects(testObjectList)));
            for (TheObject to: testObjectList) {
                dao.insert(new TheObject(to.objectName,to.getObjectNumber(),"Title2"));
            }
    
            /* Extract the data non-relational methodology */
            for (TheTable t: dao.getAllTheTableRows()) {
                StringBuilder sb = new StringBuilder();
                for (String s: t.stringList) {
                    sb.append("\n\tStringList is ").append(s);
                }
                StringBuilder sbo = new StringBuilder();
                for (TheObject o: t.objectList.objectList) {
                    sbo.append("\n\tTheObject is ObjectName is ").append(o.objectName).append(" ObjectNumber is ").append(o.objectNumber);
                }
                Log.d("DBINFO","Title is " + t.title + ".\nThere are " + t.stringList.size() + " Strings. They are:-" + sb +
                        " : \n Additionally There are " + t.objectList.objectList.size() + " TheObjects. They are:- " + sbo);
            }
            /* Extract the data relational methodology */
            for (TheTableWithALLRelatedTheObjectsPOJO ttwlrto: dao.getAllTheTablerowsWithTheRelatedTheObjects()) {
                StringBuilder sb = new StringBuilder();
                for (String s: ttwlrto.theTable.stringList) {
                    sb.append("\n\tStringList is ").append(s);
                }
                StringBuilder sbo = new StringBuilder();
                for (TheObject o: ttwlrto.objectList) {
                    sbo.append("\n\tTheObject is ObjectName is ").append(o.objectName).append(" ObjectNumber is ").append(o.objectNumber);
                }
                Log.d("DBINFO","Title is " + ttwlrto.theTable.title + ".\nThere are " + ttwlrto.theTable.stringList.size() + " Strings. They are:-" + sb +
                        " : \n Additionally There are " + ttwlrto.objectList.size() + " TheObjects. They are:- " + sbo);
            }
        }
    }
    

    Results

    When run the log includes:-

    2024-04-19 11:17:42.628 D/DBINFO: Title is TITLE1.
        There are 3 Strings. They are:-
            StringList is S1
            StringList is s2
            StringList is S3 : 
         Additionally There are 2 TheObjects. They are:- 
            TheObject is ObjectName is MyFirstObject ObjectNumber is 100
            TheObject is ObjectName is MySecondObject ObjectNumber is 200
    2024-04-19 11:17:42.629 D/DBINFO: Title is Title2.
        There are 2 Strings. They are:-
            StringList is SS100
            StringList is SS200 : 
         Additionally There are 3 TheObjects. They are:- 
            TheObject is ObjectName is MyThirdObject ObjectNumber is 300
            TheObject is ObjectName is MyFourthObject ObjectNumber is 400
            TheObject is ObjectName is MyFifthObject ObjectNumber is 500
            
            
            
            
    2024-04-19 11:17:42.638 D/DBINFO: Title is TITLE1.
        There are 3 Strings. They are:-
            StringList is S1
            StringList is s2
            StringList is S3 : 
         Additionally There are 2 TheObjects. They are:- 
            TheObject is ObjectName is MyFirstObject ObjectNumber is 100
            TheObject is ObjectName is MySecondObject ObjectNumber is 200
    2024-04-19 11:17:42.638 D/DBINFO: Title is Title2.
        There are 2 Strings. They are:-
            StringList is SS100
            StringList is SS200 : 
         Additionally There are 3 TheObjects. They are:- 
            TheObject is ObjectName is MyThirdObject ObjectNumber is 300
            TheObject is ObjectName is MyFourthObject ObjectNumber is 400
            TheObject is ObjectName is MyFifthObject ObjectNumber is 500
    

    i.e. the results from either methodology are the same.

    However, using App inspection, then the TheTable is:-

    enter image description here

    • Noting that for the first non-relational methodology the entire table is required.

    For the second relational methodology the highlighted data in TheTable would not be stored here, instead the data will be in the TheObject table :-

    enter image description here

    • for the first methodology this table would not exist.

    • Note this has only dealt with a 1-n (one to many) type if relationship. n-n (many to many) relationships would typically utilise an associative table.