From a database aspect then I would suggest that the proper way is to utilise a relationship rather than nesting.
Here's a basic working Example, that not only caters for 1-0..1 (CreditCardDetails to Account) but caters for an Account to have 0..n credit cards.
- Assuming that an account is a user as such then there would be a real possibility that an account could have multiple cards.
The example demonstrates all the permutations (accounts without any cards, cards with no accounts, accounts with one or multiple cards).
The ""boiler plate" isn't that much of an overhead.
So there are 3 main classes the 2 Entities (tables) Account and CreditCardDetails and a POJO for getting CreditCardDetailsWithParentAccount** :-
@Entity
public class Account implements Serializable {
@PrimaryKey(/*autoGenerate = true */) /* inefficient as uses SQLite AUTOINCREMENT */
@ColumnInfo(name = "id")
private Long id; /* Long rather than long for more efficient auto generate */
@NonNull
@ColumnInfo(name = "account_type")
private String/*AccountType changed to String for demo */ type ;
Account() {}
@Ignore
Account( String type) {
this.id = null;
this.type = type;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@NonNull
public String getType() {
return type;
}
public void setType(@NonNull String type) {
this.type = type;
}
}
and
@Entity
public class CreditCardDetails implements Serializable {
@PrimaryKey(/*autoGenerate = true*/) /* same as for Account */
private Long id;
@ColumnInfo(name = "account_reference", index = true)
private Long account_id; /* Long rather than Long allows potential for null for no account */
@ColumnInfo(name = "credit_card_number")
private String creditCardNumber;
@ColumnInfo(name = "credit_card_type")
private String /*CreditCardType change to String for demo so no call for TypeConverter */ creditCardType;
CreditCardDetails(){}
@Ignore
CreditCardDetails(String creditCardNumber, Long account_id,String creditCardType) {
this.id = null;
this.creditCardNumber = creditCardNumber;
this.account_id = account_id;
this.creditCardType = creditCardType;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getAccount_id() {
return account_id;
}
public void setAccount_id(Long account_id) {
this.account_id = account_id;
}
public String getCreditCardNumber() {
return creditCardNumber;
}
public void setCreditCardNumber(String creditCardNumber) {
this.creditCardNumber = creditCardNumber;
}
public String getCreditCardType() {
return creditCardType;
}
public void setCreditCardType(String creditCardType) {
this.creditCardType = creditCardType;
}
}
and the POJO
class CreditCardDetailsWithParentAccount {
@Embedded
private CreditCardDetails creditCardDetails;
@Relation(
entity = Account.class,
entityColumn = "id",
parentColumn = "account_reference"
)
private Account account;
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public CreditCardDetails getCreditCardDetails() {
return creditCardDetails;
}
public void setCreditCardDetails(CreditCardDetails creditCardDetails) {
this.creditCardDetails = creditCardDetails;
}
}
For Data Access an @Dao annotated abstract class AllDao (could be an interface, but an abstract class allows methods with bodies so is more flexible BUT the methods have to be abstract) :-
@Dao
abstract class AllDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract long insert(Account account);
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract long insert(CreditCardDetails creditCardDetails);
@Query("SELECT * FROM account")
abstract List<Account> getAllAccounts();
@Query("SELECT * FROM creditCardDetails")
abstract List<CreditCardDetails> getAllCreditCardDetails();
@Transaction
@Query("SELECT * FROM creditCardDetails")
abstract List<CreditCardDetailsWithParentAccount> getAllCreditCardsDetailsWithAccount();
}
Putting it altogether from the Room perspective (for brevity/convenience allowing access from the main thread) an '@Database' annotated abstract class that extends RoomDatabase is the TheDatabase class:-
@Database(entities = {Account.class,CreditCardDetails.class}, exportSchema = false, version = 1)
abstract class TheDatabase extends RoomDatabase {
abstract AllDao getAllDao();
private volatile static TheDatabase instance;
public static TheDatabase getInstance(Context context) {
if (instance==null) {
instance = Room.databaseBuilder(context,TheDatabase.class,"the_database.db")
.allowMainThreadQueries() /* for demo */
.build();
}
return instance;
}
}
Finally to demonstrate MainActivity which inserts some accounts and credit card details and then extracts all the accounts, all the credit card details and then All the credit card details with the related account via the POJO:-
public class MainActivity extends AppCompatActivity {
TheDatabase db;
AllDao dao;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
db = TheDatabase.getInstance(this);
dao = db.getAllDao();
long a1id = dao.insert(new Account("Account001"));
long a2id = dao.insert(new Account("Account002"));
long a3id = dao.insert(new Account("Account003"));
dao.insert(new CreditCardDetails("1234-1111-1111",a1id,"VISA"));
dao.insert(new CreditCardDetails("1234-2222-2222",a2id,"MC"));
dao.insert(new CreditCardDetails("1234-3333-3333",a2id,"AMEX"));
dao.insert(new CreditCardDetails("1234-4444-4444",null,"NOWT"));
for (Account a: dao.getAllAccounts()) {
Log.d("DI_ACCOUNT","Account is " + a.getId() + " Type is " + a.getType());
}
for (CreditCardDetails cd: dao.getAllCreditCardDetails()) {
Log.d("DI_CREDITCARD","Credit Card Number is " + cd.getCreditCardNumber() + " Type is " + cd.getCreditCardType() + "Account id is " + cd.getAccount_id());
}
for(CreditCardDetailsWithParentAccount cdwa: dao.getAllCreditCardsDetailsWithAccount()) {
if (cdwa.getAccount() != null) {
Log.d("DI_CD_WITH_A", "Credit Card Number is " + cdwa.getCreditCardDetails().getCreditCardNumber() + ".... Account Type is " + cdwa.getAccount().getType());
} else {
Log.d("DI_CD_WITH_NOA","Credit Card Number is " + cdwa.getCreditCardDetails().getCreditCardNumber() + ".... Account Type is NO PARENT ACCOUNT");
}
}
}
}
When the above is run (for the first time (it's only intended to be run once)) then the output to the log is:-
2022-08-18 19:28:42.033 D/DI_ACCOUNT: Account is 1 Type is Account001
2022-08-18 19:28:42.033 D/DI_ACCOUNT: Account is 2 Type is Account002
2022-08-18 19:28:42.033 D/DI_ACCOUNT: Account is 3 Type is Account003
2022-08-18 19:28:42.034 D/DI_CREDITCARD: Credit Card Number is 1234-1111-1111 Type is VISAAccount id is 1
2022-08-18 19:28:42.034 D/DI_CREDITCARD: Credit Card Number is 1234-2222-2222 Type is MCAccount id is 2
2022-08-18 19:28:42.034 D/DI_CREDITCARD: Credit Card Number is 1234-3333-3333 Type is AMEXAccount id is 2
2022-08-18 19:28:42.034 D/DI_CREDITCARD: Credit Card Number is 1234-4444-4444 Type is NOWTAccount id is null
2022-08-18 19:28:42.041 D/DI_CD_WITH_A: Credit Card Number is 1234-1111-1111.... Account Type is Account001
2022-08-18 19:28:42.041 D/DI_CD_WITH_A: Credit Card Number is 1234-2222-2222.... Account Type is Account002
2022-08-18 19:28:42.041 D/DI_CD_WITH_A: Credit Card Number is 1234-3333-3333.... Account Type is Account002
2022-08-18 19:28:42.041 D/DI_CD_WITH_NOA: Credit Card Number is 1234-4444-4444.... Account Type is NO PARENT ACCOUNT
Nested Example
For the nested (embedded) example the following have been added or changed.
AccountV2 (added)
public class AccountV2 implements Serializable {
//private Long id; Nested within CreditCardDetailsV2 so not needed */
private String/*AccountType changed to String for demo */ type ;
AccountV2() {}
@Ignore
AccountV2( String type) {
this.type = type;
}
@NonNull
public String getType() {
return type;
}
public void setType(@NonNull String type) {
this.type = type;
}
}
- NOT annotated with
@Entity as is no longer a table
CreditDetailsV2
@Entity
public class CreditCardDetailsV2 implements Serializable {
@PrimaryKey(/*autoGenerate = true*/) /* same as for Account */
private Long id;
@Embedded
private AccountV2 account;
@ColumnInfo(name = "credit_card_number")
private String creditCardNumber;
@ColumnInfo(name = "credit_card_type")
private String /*CreditCardType change to String for demo so no call for TypeConverter */ creditCardType;
CreditCardDetailsV2(){}
@Ignore
CreditCardDetailsV2(String creditCardNumber, AccountV2 account,String creditCardType) {
this.id = null;
this.creditCardNumber = creditCardNumber;
this.account = account;
this.creditCardType = creditCardType;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public AccountV2 getAccount() {
return account;
}
public void setAccount(AccountV2 account) {
this.account = account;
}
public String getCreditCardNumber() {
return creditCardNumber;
}
public void setCreditCardNumber(String creditCardNumber) {
this.creditCardNumber = creditCardNumber;
}
public String getCreditCardType() {
return creditCardType;
}
public void setCreditCardType(String creditCardType) {
this.creditCardType = creditCardType;
}
}
- Note the POJO is not required as the AccountV2 is embedded (nested) within
AllDaoV2 (added just to keep the code separate could be combined with our even replace AllDao)
@Dao
interface AllDaoV2 {
@Insert(onConflict = OnConflictStrategy.IGNORE)
long insert(CreditCardDetailsV2 creditCardDetailsV2);
@Query("SELECT * FROM creditCardDetailsV2")
List<CreditCardDetailsV2> getAllCreditCardDetailsV2();
}
- interface rather than abstract class to show both
TheDatabase (changed to allow co-existence of the methodologies)
@Database(entities = {Account.class,CreditCardDetails.class, CreditCardDetailsV2.class /*<<<<< ADDED for V2*/}, exportSchema = false, version = 1)
abstract class TheDatabase extends RoomDatabase {
abstract AllDao getAllDao();
abstract AllDaoV2 getAllDaoV2(); /* ADDED for V2 */
private volatile static TheDatabase instance;
public static TheDatabase getInstance(Context context) {
if (instance==null) {
instance = Room.databaseBuilder(context,TheDatabase.class,"the_database.db")
.allowMainThreadQueries() /* for demo */
.build();
}
return instance;
}
}
Finally MainActivity (changed to demo both methodologies using very much the equivalent data for each method):-
public class MainActivity extends AppCompatActivity {
TheDatabase db;
AllDao dao;
AllDaoV2 daoV2; /* NEW for V2 */
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
db = TheDatabase.getInstance(this);
dao = db.getAllDao();
daoV2 = db.getAllDaoV2(); /* NEW for V2 */
long a1id = dao.insert(new Account("Account001"));
long a2id = dao.insert(new Account("Account002"));
long a3id = dao.insert(new Account("Account003"));
dao.insert(new CreditCardDetails("1234-1111-1111",a1id,"VISA"));
dao.insert(new CreditCardDetails("1234-2222-2222",a2id,"MC"));
dao.insert(new CreditCardDetails("1234-3333-3333",a2id,"AMEX"));
dao.insert(new CreditCardDetails("1234-4444-4444",null,"NOWT"));
for (Account a: dao.getAllAccounts()) {
Log.d("DI_ACCOUNT","Account is " + a.getId() + " Type is " + a.getType());
}
for (CreditCardDetails cd: dao.getAllCreditCardDetails()) {
Log.d("DI_CREDITCARD","Credit Card Number is " + cd.getCreditCardNumber() + " Type is " + cd.getCreditCardType() + "Account id is " + cd.getAccount_id());
}
for(CreditCardDetailsWithParentAccount cdwa: dao.getAllCreditCardsDetailsWithAccount()) {
if (cdwa.getAccount() != null) {
Log.d("DI_CD_WITH_A", "Credit Card Number is " + cdwa.getCreditCardDetails().getCreditCardNumber() + ".... Account Type is " + cdwa.getAccount().getType());
} else {
Log.d("DI_CD_WITH_NOA","Credit Card Number is " + cdwa.getCreditCardDetails().getCreditCardNumber() + ".... Account Type is NO PARENT ACCOUNT");
}
}
/* NEW for V2 Equivalent of the initial data*/
daoV2.insert(new CreditCardDetailsV2("1234-1111-1111",new AccountV2("Account0001"),"VISA"));
daoV2.insert(new CreditCardDetailsV2("1234-2222-2222",new AccountV2("Account0002"),"MC"));
daoV2.insert(new CreditCardDetailsV2("1234-3333-3333", new AccountV2("Account0002"),"AMEX"));
daoV2.insert(new CreditCardDetailsV2("1234-4444-4444", null,"NOWT"));
for (CreditCardDetailsV2 cdv2: daoV2.getAllCreditCardDetailsV2()) {
if (cdv2.getAccount() != null) {
Log.d("DI_CD_V2", "Credit Card Number is " + cdv2.getCreditCardNumber() + ".... Account is " + cdv2.getAccount().getType());
} else {
Log.d("DI_CD_V2_NOA", "Credit Card Number is " + cdv2.getCreditCardNumber() + ".... NO NESTED ACCOUNT");
}
}
}
}
When run (after uninstalling) The log additionally contains (i.e. the output relevant to V2) :-
2022-08-18 20:11:54.464 D/DI_CD_V2: Credit Card Number is 1234-1111-1111.... Account is Account0001
2022-08-18 20:11:54.464 D/DI_CD_V2: Credit Card Number is 1234-2222-2222.... Account is Account0002
2022-08-18 20:11:54.464 D/DI_CD_V2: Credit Card Number is 1234-3333-3333.... Account is Account0002
2022-08-18 20:11:54.464 D/DI_CD_V2_NOA: Credit Card Number is 1234-4444-4444.... NO NESTED ACCOUNT
From a database perspective, the data is now not normalised that is the Account0002, as it has 2 CreditCards, is repeated and thus the same data is stored twice, probably wasting space, whilst in the first example the key (reference/map/relation) only is repeated, which will at most consume 8 bytes (well 16 or so as the key/reference was indexed.
Note that Embedding is basically just copying the fields/members from the Embedded class into the class so each row has additional columns.
As both versions/methods were combined the difference can easily be seen by using App Inspection which reveals :-



Note the encircled field/member anotherField can be ignored (it was added to test something else out)
The code I have provided does not use autogenerate = true, the code does briefly comment about using AUTOINCREMENT, which is inefficient. This link explains why AUTOINCREMENT is inefficient https://sqlite.org/autoinc.html