Android Room Database: How to Handle Arraylist in an Entity

Android Room Database: How to handle Arraylist in an Entity?

Option #1: Have MyListItems be an @Entity, as MainActivityData is. MyListItems would set up a @ForeignKey back to MainActivityData. In this case, though, MainActivityData cannot have private ArrayList<MyListItems> myListItems, as in Room, entities do not refer to other entities. A view model or similar POJO construct could have a MainActivityData and its associated ArrayList<MyListItems>, though.

Option #2: Set up a pair of @TypeConverter methods to convert ArrayList<MyListItems> to and from some basic type (e.g., a String, such as by using JSON as a storage format). Now, MainActivityData can have its ArrayList<MyListItems> directly. However, there will be no separate table for MyListItems, and so you cannot query on MyListItems very well.

Null Arraylist from Room Database

Here is the modified converter class. It might help you.

object class Converters {

val gson = Gson()

@TypeConverter
fun arrayListToJson(list: List<Weather>?): String? {
return if(list == null) null else gson.toJson(list)
}

@TypeConverter
fun jsonToArrayList(jsonData: String?): List<Weather>? {
return if (jsonData == null) null else gson.fromJson(jsonData, object : TypeToken<List<Weather>?>() {}.type)
}
}

Room Database: how to embedded a object model have ArrayList field?

You could place @TypeConverters(ConvertersDAO::class) at:-

The @Database level (all Daos and Entities in that database will be able to use it.), e.g.

@Database(entities = [UserModel::class],version = 1)
@TypeConverters(ConvertersDAO::class)

at the @Entity level (all fields of the Entity will be able to use it) e.g.

@Entity(tableName = "user_table")
@TypeConverters(ConvertersDAO::class)

and you should be able to code it at the @Entity's member/field level e.g.

@Embedded
@TypeConverters(ConvertersDAO::class)
var location: LocationModel = LocationModel()
  • all the above compile successfully when tested.

see https://developer.android.com/reference/androidx/room/TypeConverters

Saving Multiple Arraylist in Room Database - Best way of doing it?

What is the best way of storing the Arraylists in Room while knowing
that I want to there is not only one Arraylist coming?

Arguably no way is the best way.

ArrayList's don't just appear, the data and element structure (if multiple values exist per element) comes from somewhere, that is they are just containers but not persistent containers of grouped data. As the end result appears to be a persistent structured sets of data then it would likely be simpler to primarily utilise a database.

I am trying to build a Deckbuilder for a Cardgame. The different decks
are based on one Arraylists that I want to safe locally.

It would sound like this is the base of cards i.e. those available for the game. So it sounds like you want a table in the database for the cards.

That means you can create your deck and than I want you to be able to
safe it, and then create a new deck,

It sounds like you want a table in the database for the decks and that a deck can have a list of cards. So how many cards? 10, 20, 3000? Well if you utilise the relationship capabilities of a Realtional Database Manager (which SQLite and therefore Room (as the latter is an abstract layer over SQLite)) is. So it is likely that an what is know as a mapping (reference, relationship and other names for the same thing) table.

This primarily stores relationships that consist of a column that can identify one part of the relationship and another column that can identify the other part. Apply this to your case a DECK would have a relationship to a number of cards, a CARD may be able to appear in a number of DECKs. This is a many-many relationship which a mapping table caters for. So you likely want a mapping table.

For the basis of explaining further it will be assumed that we are talking about playing cards (Ace of Spades, Queen of Hearts and so on).

So we want three tables a Card table, a Deck table and a table that maps cards to decks (and therefore vice-versa) a Card_Deck_Map table.

The Card table to keep it simple will have a single column for the cardname.
The Deck tables will have a single column for the deckname
To be efficient an identifier which is an alias of the special column rowid will be used for the mapping. So the above tables will each have an extra column that will be called _id (naming the column as _id this can be beneficial for Android).

It will be assumed that you don't want a card name nor a deck name to be duplicated and as such a UNIQUE constraint will be placed applied that will not allow the names to be duplicated.

To cut things short here's how this could look like in SQL (which is ultimately how most data storage, manipulation and extraction is done) :-

-- Delete the tables if they exist (just in case)
DROP TABLE IF EXISTS card_deck_map;
DROP TABLE IF EXISTS card;
DROP TABLE IF EXISTS deck;

-- Create the tables
CREATE TABLE IF NOT EXISTS card (_id INTEGER PRIMARY KEY, cardname UNIQUE);
CREATE TABLE IF NOT EXISTS deck (_id INTEGER PRIMARY KEY, deckname UNIQUE);
CREATE TABLE IF NOT EXISTS card_deck_map (
card_reference INTEGER REFERENCES card(_id),
deck_reference INTEGER REFERENCES deck(_id),
PRIMARY KEY(card_reference,deck_reference)
);

-- Add 3 cards to the card table
INSERT INTO card (cardname) VALUES ('CARD001'),('CARD002'),('CARD003');
-- Add 3 decks to the deck table
INSERT INTO deck (deckname) VALUES ('DECK001'),('DECK002');
-- Create some mapping entries (aka put some cards into each deck)
INSERT INTO card_deck_map VALUES
(1,2), -- _id value for CARD001 should be 1, _id value for DECK002 should be 2
(3,2), -- CARD003 is in DECK002
(2,1), -- CARD002 is in DECK001
(1,1) -- CARD001 is also in DECK002
;
-- Have a look at what we have (ignore the id values they mean little to the user)
SELECT deckname, cardname
FROM deck
JOIN card_deck_map ON deck._id = deck_reference
JOIN card ON card_deck_map.card_reference = card._id
ORDER BY deckname, cardname
;

The output from the above would be :-

Sample Image

So now the database design appears to suit, then it can now be converted for use by ROOM.

First the 3 entities Defining data using Room entities

Card.java

:-

@Entity (indices = {@Index(value = {"cardname"}, unique = true)})
public class Card {
@PrimaryKey(autoGenerate = true)
public long _id;

@ColumnInfo(name = "cardname")
public String cardname;

}

Deck.java

:-

@Entity(indices = {@Index(value = "deckname", unique = true)})
public class Deck {

@PrimaryKey(autoGenerate = true)
public long _id;

@ColumnInfo(name = "deckname")
public String deckname;
}

Card_Deck_Map.java

:-

@Entity(
primaryKeys = {"card_reference","deck_reference"},
foreignKeys = {
@ForeignKey(entity = Card.class,parentColumns = "_id",childColumns = "card_reference"),
@ForeignKey(entity = Deck.class, parentColumns = "_id",childColumns = "deck_reference")}
)
public class Card_Deck_Map {

@ColumnInfo (name="card_reference")
public long card_reference;

@ColumnInfo(name="deck_reference")
public long deck_reference;
}

Now you want the Data Access Objects definitions Accessing data using Room DAOs

DeckBuildeDao

:-

@Dao
public interface DeckBuilderDao {

@Insert(onConflict = OnConflictStrategy.IGNORE)
public long[] insertCards(Card... cards);

@Insert(onConflict = OnConflictStrategy.IGNORE)
public long insertCard(Card card);

@Update
public int updateCardBaseEntries(Card... cards);

@Update
public int updateCardBaseEntry(Card card);

@Delete
public int deleteCardBaseEntried(Card... cards);

@Delete
public int deleteCardBaseEntry(Card card);

@Query("SELECT * FROM card")
public Card[] getAllCards();

@Query("SELECT * FROM card WHERE _id = :id")
public Card getACard(long id);

@Insert(onConflict = OnConflictStrategy.IGNORE)
public long[] insertDecks(Deck... decks);

@Insert(onConflict = OnConflictStrategy.IGNORE)
public long insertDeck(Deck deck);

@Update
public int updateDeckEntries(Deck... decks);

@Update
public int updateDeckEntry(Deck deck);

@Delete
public int deleteDeckEntries(Deck... decks);

@Delete
public int deleteDeckEntry(Deck deck);

@Query("SELECT * FROM deck")
public int getAllDecks();

@Query("SELECT * FROM deck WHERE _id = :id")
public Deck getADeck(long id);

@Insert(onConflict = OnConflictStrategy.IGNORE)
public long[] addCardDeckEntries(Card_Deck_Map... cardDeckMaps);

@Insert(onConflict = OnConflictStrategy.IGNORE)
public long addCardDeckEntry(Card_Deck_Map cardDeckMap);

@Query("SELECT Deck._id,Card.cardname, Deck.deckname " +
"FROM deck " +
"JOIN card_deck_map ON deck._id = card_deck_map.deck_reference " +
"JOIN card ON card_deck_map.card_reference = card._id " +
"ORDER BY deckname, cardname")
public Cursor getAllDecksWithCards();

}

A class for the Database that ties the entities and DAO's together

DeckBuilderDatabase.java

:-

@Database(entities = {Card.class, Deck.class, Card_Deck_Map.class}, version = 1)
public abstract class DeckBuilderDatabase extends RoomDatabase {
public abstract DeckBuilderDao deckBuilderDao();
}

Now an activity that uses the database.

In this working example;

  1. the database will be populated with 2 Decks (Deck001 and Deck002) a card base as per a pack of playing cards less the Jokers.

    1. Cards will be named like Ace of Spades, 2 of Hearts.
  2. The decks will be loaded with some cards (the mapping)

    1. Deck002 with all 52 cards.
    2. Deck001 with 3 cards.
  3. The Decks and Cards will be extracted from the database and used to populate a ListView.

MainActivity.java

public class MainActivity extends AppCompatActivity {

public static final String[] SUITS = new String[]{"Spades","Hearts","Clubs","Diamons"};
public static final int CARDS_IN_A_SUIT = 13;

DeckBuilderDatabase mDBDB;
SimpleCursorAdapter mSCA;
ListView mDecks_and_Cards_List;
Cursor mCursor;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDecks_and_Cards_List = this.findViewById(R.id.decksandcards);
mDBDB = Room.databaseBuilder(this,DeckBuilderDatabase.class,"deckbuilder.db").build();
populateDB();
}

/**
* Populate the DB with some data, extract the data in the DB and setup the ListView
*/
private void populateDB() {
new Thread(new Runnable() {
@Override
public void run() {

Card_Deck_Map currentcdm = new Card_Deck_Map();

Deck[] decks_to_add = new Deck[]{new Deck(), new Deck()};
decks_to_add[0].deckname = "DECK001";
decks_to_add[1].deckname= "DECK002";
mDBDB.deckBuilderDao().insertDecks(decks_to_add);

// Build Card base pack of 52 (no Jokers)
Card[] cardstoadd = new Card[CARDS_IN_A_SUIT * SUITS.length];
int counter = 0;
for (int suit = 0; suit < SUITS.length; suit++) {
for (int cardval = 0; cardval < CARDS_IN_A_SUIT; cardval++) {
Card thiscard = new Card();
String thiscardname = generateCardValueDescription(cardval+1,suit);
thiscard.cardname = thiscardname;
cardstoadd[counter++] = thiscard;
}
}
mDBDB.deckBuilderDao().insertCards(cardstoadd);

// Populate the decks with cards Deck002 has full pack of 52 Deck001 has 3 cards
Card_Deck_Map[] mappings = new Card_Deck_Map[55];
for (int cardid = 1; cardid < 53; cardid++) {
Card_Deck_Map cdm = new Card_Deck_Map();
cdm.deck_reference = 2;
cdm.card_reference = cardid;
mappings[cardid-1] = cdm;
}
Card_Deck_Map cdm53 = new Card_Deck_Map();
cdm53.card_reference = 19;
cdm53.deck_reference = 1;
mappings[52] = cdm53;
Card_Deck_Map cdm54 = new Card_Deck_Map();
cdm54.card_reference = 10;
cdm54.deck_reference = 1;
mappings[53] = cdm54;
Card_Deck_Map cdm55 = new Card_Deck_Map();
cdm55.card_reference = 23;
cdm55.deck_reference = 1;
mappings[54] = cdm55;
mDBDB.deckBuilderDao().addCardDeckEntries(mappings);

// Get the Decks and cards in the decks
mCursor = mDBDB.deckBuilderDao().getAllDecksWithCards();
setupOrRefeshListView();
}
}).start();
}

/**
* Handles the ListView (also write data to the log for debugging)
*/
private void setupOrRefeshListView() {
int rowcount = mCursor.getCount();
Log.d("ROWS","Number of rows in the Cursor is " + String.valueOf(rowcount));
while (mCursor.moveToNext()) {
Log.d(
"ROWS",
"Row " +
String.valueOf(mCursor.getPosition()) +
" Has a deck called " +
mCursor.getString(mCursor.getColumnIndex("deckname")) +
" and a card called " +
mCursor.getString(mCursor.getColumnIndex("cardname"))
);
}
if (mSCA == null) {
mSCA = new SimpleCursorAdapter(
this,
android.R.layout.simple_list_item_2,
mCursor,
new String[]{
"deckname",
"cardname"},
new int[]{
android.R.id.text1,
android.R.id.text2},
0
);
mDecks_and_Cards_List.setAdapter(mSCA);
} else {
mSCA.swapCursor(mCursor);
}
}

/**
* Converts numeric cardvalue (1-13) and suit to a decriptive name
* @param cardvalue
* @param suit
* @return
*/
private String generateCardValueDescription(int cardvalue, int suit) {
String rv;
switch (cardvalue) {
case 1:
rv = "Ace of " + SUITS[suit];
break;
case 11:
rv = "Jack of " + SUITS[suit];
break;
case 12:
rv = "Queen of " + SUITS[suit];
break;
case 13:
rv = "King of " + SUITS[suit];
break;
default:
rv = String.valueOf(cardvalue) + " of " + SUITS[suit];
}
return rv;
}
}

The Resultant mini-app :-

Sample Image

Room: Use class that extends ArrayList as Entity

I'd like to use MyDemoClass as a playlist. I'd add Songs to this playlist and save it inside the database

MyDemoClass would be a plain ol' Java object (POJO) and would have fields that correspond to simple properties of a playlist, such as a String that would serve as the name that users can provide and see in rosters of playlists. MyDemoClass would not be a subclass of ArrayList.

Song would be another POJO and have fields that correspond to simple properties of a song, such as a String for its title.

Since a playlist can have multiple songs, and a song can appear in multiple playlists, you would need to create an entity that represents that join, with foreign key relationships back to the playlist and song entities.

The use of foreign key relationships is lightly covered in the Room documentation. It should be covered in any book that spends significant time on Room. For example, here is a preview edition of my chapter on M:N relations in Room (from this book).

How to improve my Room database architecture?

Issue 1

You appear to have an issue that will likely cause some frustration if not addressed.

That is a User, has it's primary key as the reference to the parent group (Gru). As such a Group could only have a single User (Student) as the primary key, for the User must be unique.

Likewise for Eval's.

So you could consider the following:-

@Entity(tableName = "groupe_table")
data class Gru (
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "idGroup") var idGroup: Int=0,
@ColumnInfo(name = "nameGroupG") var nameGroupG : String
): Parcelable

@Entity(tableName = "user_table", foreignKeys = arrayOf(
ForeignKey(entity = Gru::class,
parentColumns = arrayOf("idGroup"),
//childColumns = arrayOf("id"), //<<<<< REMOVED
childColumns = ["gru_id_reference"], //<<<<< REPLACED WITH

onDelete = ForeignKey.CASCADE)
))

@Parcelize
data class User(@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var id: Int=0,
@ColumnInfo(name = "nameGroup") var nameGroup: String,
@ColumnInfo(name = "firstName") var firstName: String,
@ColumnInfo(name = "lastName") var lastName: String,
@ColumnInfo(name = "nbTeam") var nbTeam: String,
@ColumnInfo(index = true) //<<<<< ADDED (may be more efficient)
var gru_id_reference: Int //<<<<< ADDED
):Parcelable

@Entity(tableName = "eval_table", foreignKeys = arrayOf(
ForeignKey(entity = User::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("user_id_reference"), //<<<<< CHANGED
onDelete = ForeignKey.CASCADE)
))

data class Eval(@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "idEval") var idEval: Int=0,
@ColumnInfo(name = "note_classement") var note_classement: String,
@ColumnInfo(name = "note_attaque") var note_attaque: String,
@ColumnInfo(name = "note_passe") var note_passe: String,
@ColumnInfo(name = "note_afl2") var note_afl2: String,
@ColumnInfo(name = "note_afl3") var note_afl3: String,
@ColumnInfo(name = "note_sur_vingt") var note_sur_vingt: String,
@ColumnInfo(index = true) var user_id_reference: Int //<<<<< ADDED
)
  • See the comments
  • note that there is no need to use @ColumnInfo to name a column the same name as the field/member (hence the added code doesn't, but instead uses the annotation to introduce an index on the additional column use to reference the parent).

Without going into all the other code, the following code:-

db = TheDatabase.getInstance(this)
dao = db.getAllDao()

val g1id = dao.insert(Gru(nameGroupG = "Group001"))
val g2id = dao.insert(Gru(nameGroupG = "Group002"))
val g3id = dao.insert(Gru(nameGroupG = "group003"))

val u1id = dao.insert(User(nameGroup = "Why have this here?", firstName = "Fred", lastName = "Bloggs", nbTeam = "TeamA", gru_id_reference = g1id.toInt()))
val u2id = dao.insert(User(nameGroup = "?????", firstName = "Jane", lastName = "Doe", nbTeam = "TeamX", gru_id_reference = g1id.toInt()))
val u3id = dao.insert(User(nameGroup = "?????", firstName = "Mary", lastName = "Smith", nbTeam = "TeamB", gru_id_reference = g2id.toInt()))
val u4id = dao.insert(User(nameGroup = "?????", firstName = "Tom", lastName = "Cobbely", nbTeam = "TeamC", gru_id_reference = g3id.toInt()))

var baseEval = Eval(note_classement = "CMENT_", note_attaque = "ATTQ_", note_afl2 = "AFL2_", note_afl3 = "AFL3_", note_passe = "PASSE_", note_sur_vingt = "SV_",user_id_reference = -99)

for (i in 1..10) {
dao.insert(
Eval(
note_classement = baseEval.note_classement + i,
note_attaque = baseEval.note_classement + i,
note_afl2 = baseEval.note_afl2 + i,
note_afl3 = baseEval.note_afl3 + i,
note_passe = baseEval.note_passe + i,
note_sur_vingt = baseEval.note_sur_vingt + i,
user_id_reference = Random.nextInt(4) + 1
)
)
}

results in a database (i.e. tests the changed code) as per :-

  1. The 3 Groups (Gru's/Classes) :-

    • Sample Image
  2. The 4 users :-

    • Sample Image

    • Note how Fred and Jane are both in Group001

  3. And the 10 Eval's spread across the 4 Users

    • Sample Image
  4. A query that joins the data according to the relationships looks like:-

Sample Image

- here you can see that there are 3 evaluations for Group001, 2 of them for Fred and 1 for Jane etc

- (note Eval's reference a random User)
  • The above data was obtained by running the code above and then using App Inspection (available in Android Studio).

Issue 2

You may well encounter subsequent issues due to both the user_table and the eval_table having a column named id. From an SQL point of view this can be overcome by qualifying the column with it's table name (see SQL used above where the tablename . column is used to disambiguate the ambiquity). However, as far as the resultant output there would still be 2 id columns. This ambiguity can be overcome using AS to rename the output column but you may then encounter issues. I would suggest ensuring that all column names are unique (so perhaps have column names userId and evalId instead of just id).



Related Topics



Leave a reply



Submit