Previous Tutorial : Inversion of Control & Dependency Injection
In this tutorial I am going to show You, how to make use of lazy loading and object navigation using references. This can make Your application development a lot easier, because many problems in the data world can be solved by navigation instead of writing a query.
This tutorial is based on the tutorial part 3, so if You have not read it, start from there.
Download the source code to this tutorial from Github
Step 1 : The basics
We have modelled the BankAccount in the prior tutorial and fitted it with a DAO and a RowMapper. This aspect of using a row mapper ist pretty vital for lazy loading. Baracus does not rely on annotation based code generation like hibernate nor on reflection based object loading. It follows a very straight forward rowmapping strategy leaving a lot of control in the hands of the developer. This includes choosing to have lazy loading or eager loading in the place where the row data is mapped.
Imagine, we create a Customer entity. One customer may have multiple BankAccounts but a BankAccount may only have one designated customer - a classic 1:N or N:1 situation we meet in every well designed database environment nowadays.
The proceeding is following the four steps mentioned in the prior tutorial (entity, dao+rowmapper, migr8 mode, bean wiring)
ONE - The entity
So at first, we create a bare CustomerEntity having only some properties : Name and FirstName (hey, this is a demo only ;-)) and it will look somewhat like this :
package org.baracus.model; import org.baracus.orm.ModelBase; import org.baracus.orm.Field; import org.baracus.orm.FieldList; /** * Created with IntelliJ IDEA. * User: marcus */ public class Customer extends ModelBase { public static final String TABLE_CUSTOMER = "customer"; private static int columnIndex= ModelBase.fieldList.size(); private String lastName; private String firstName; public static final FieldList fieldList = new FieldList(Customer.class.getSimpleName()); public static final Field lastNameCol = new Field("last_name", columnIndex++); public static final Field firstNameCol = new Field("first_name", columnIndex++); static { fieldList.add(ModelBase.fieldList); fieldList.add(lastNameCol); fieldList.add(firstNameCol); } public Customer() { super(TABLE_CUSTOMER); } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public static FieldList getFieldList() { return fieldList; } public static Field getLastNameCol() { return lastNameCol; } public static Field getFirstNameCol() { return firstNameCol; } }
We are playing the same game again like we did on the BankAccount. Define fields, define tablename, define column metadata. Next we are going to need is a dao
TWO - The DAO
package org.baracus.dao; import android.content.ContentValues; import android.database.Cursor; import org.baracus.model.BankAccount; import org.baracus.model.Customer; import org.baracus.orm.Field; import org.baracus.orm.FieldList; import static org.baracus.model.Customer.*; import static org.baracus.orm.ModelBase.idCol; /** * Created with IntelliJ IDEA. * User: marcus * To change this template use File | Settings | File Templates. */ public class CustomerDao extends BaseDao<Customer> { /** * Lock the DAO of */ public CustomerDao() { super(Customer.class); } private final RowMapper<Customer> rowMapper = new RowMapper<Customer>() { @Override public Customer from(Cursor c) { Customer result = new Customer(); result.setId(c.getLong(idCol.fieldIndex)); result.setLastName(c.getString(lastNameCol.fieldIndex)); result.setFirstName(c.getString(firstNameCol.fieldIndex)); result.setTransient(false); return result; } @Override public String getAffectedTable() { return Customer.TABLE_CUSTOMER; } @Override public FieldList getFieldList() { return Customer.fieldList; } @Override public Field getNameField() { return Customer.lastNameCol; } @Override public ContentValues getContentValues(Customer customer) { ContentValues result = new ContentValues(); if (customer.getId() != null) { result.put(idCol.fieldName, customer.getId()); } if (customer.getLastName() != null) { result.put(lastNameCol.fieldName, customer.getLastName()); } if (customer.getFirstName() != null) { result.put(firstNameCol.fieldName, customer.getFirstName()); } return result; } }; @Override public RowMapper<Customer> getRowMapper() { return rowMapper; } }
You can mostly copy the BankAccountDao and modify it properly using the Customer entity.
THREE - The migr8 model
In order to let android create the table, we need to make use of migr8. Because this application is a "already running" one, we create a new MigrationStep implementation; this will show You to make use of the extensible migration system allowing to migrate any application version to the latest one:
package org.baracus.migr8; import android.database.sqlite.SQLiteDatabase; import org.baracus.model.BankAccount; import org.baracus.model.Customer; import org.baracus.util.Logger; /** * Created with IntelliJ IDEA. * User: marcus * To change this template use File | Settings | File Templates. */ public class ModelVersion101 implements MigrationStep { private static final Logger logger = new Logger(ModelVersion101.class); @Override public void applyVersion(SQLiteDatabase db) { String stmt = "CREATE TABLE " + Customer.TABLE_CUSTOMER + "( "+ Customer.idCol.fieldName+" INTEGER PRIMARY KEY" + ", "+ Customer.lastNameCol.fieldName+ " TEXT" + ", "+ Customer.firstNameCol.fieldName+ " TEXT"+ ")"; logger.info(stmt); db.execSQL(stmt); } @Override public int getModelVersionNumber() { return 101; } }
As You can see, we make use of the column metadata to create the CREATE TABLE-statement.
FOUR - Wire the beans
Now we wire the beans properly, this means a) registering the OpenHelper implementation and b) registering the DAO in the app initializer.
package org.baracus.application; import android.content.Context; import org.baracus.dao.BaracusOpenHelper; import org.baracus.migr8.ModelVersion100; import org.baracus.migr8.ModelVersion101; /** * Created with IntelliJ IDEA. * User: marcus */ public class OpenHelper extends BaracusOpenHelper { private static final String DATABASE_NAME="tutorial-app.db"; private static final int TARGET_VERSION=101; static { addMigrationStep(new ModelVersion100()); addMigrationStep(new ModelVersion101()); } /** * Open Helper for the android database * * @param mContext - the android context */ public OpenHelper(Context mContext) { super(mContext, DATABASE_NAME, TARGET_VERSION); } }
As you can see, we increase the TARGET_VERSION and we add our MigrationStep. Finally, we register the DAO in the ApplicationContext :
package org.baracus.application; import org.baracus.context.BaracusApplicationContext; import org.baracus.dao.BankAccountDao; import org.baracus.dao.BaracusOpenHelper; import org.baracus.dao.CustomerDao; import org.baracus.service.BankAccountService; import org.baracus.service.CustomerService; /** * Created with IntelliJ IDEA. * User: marcus */ public class ApplicationContext extends BaracusApplicationContext { static { registerBeanClass(OpenHelper.class); registerBeanClass(BankAccountDao.class); registerBeanClass(CustomerDao.class); registerBeanClass(CustomerService.class); registerBeanClass(BankAccountService.class); } }
Step2 : Many-To-One relation
So far, we made the basics mapping. Now imagine, a BankAccount entity is bound to a customer. This will bring us to directly to the classical ManyToOne relation, a standard situation in any database carrying one or more table relations. This requires us to wire the BankAcount to the Customer entity and is realized using the Reference<T> interface. Because there is no dynamic typing and no code generation done with Baracus, this is the way to enable us having object relations.
So basically, at first we create a field in the BankAccount using the Reference<Customer> type and create the designated database column :
package org.baracus.model; // .. public class BankAccount extends ModelBase { // ... private Reference<Customer> customerReference; public static final Field customerIdCol = new Field("customerId", columnIndex++); static { // ... fieldList.add(customerIdCol); } // ... public Reference<Customer> getCustomerReference() { return customerReference; } public void setCustomerReference(Reference<Customer> customerReference) { this.customerReference = customerReference; } public Customer getCustomer() { return customerReference.getObject(); } public void setCustomer(Customer customer) { this.customerReference = new ObjectReference(customer); } }
As You can see, I added a reference field, a column for the Customer-Id foreign key and three getter/setter functions (two for the reference and two commodity getter/setter. As You can see, I am using the ObjectReference class for setting the customer, if the complete object is present (well it isn't in case of lazy loading, but when creating objects we normally have the objects present).
Next, we have to extend the database migration, the existing table needs to be altered. Therefore we need to create another MigrationStep carrying the version 102 :
package org.baracus.migr8; // ... public class ModelVersion102 implements MigrationStep { private static final Logger logger = new Logger(ModelVersion102.class); @Override public void applyVersion(SQLiteDatabase db) { String stmt = "ALTER TABLE " + BankAccount.TABLE_BANK_ACCOUNT + " ADD COLUMN "+BankAccount.customerIdCol.fieldName + " INTEGER"; logger.info(stmt); db.execSQL(stmt); } @Override public int getModelVersionNumber() { return 102; } }
This migration step needs to be registered to the OpenHelper implementation, whose TARGET_VERSION is increased to 102 :
package org.baracus.application; import android.content.Context; import org.baracus.dao.BaracusOpenHelper; import org.baracus.migr8.ModelVersion100; import org.baracus.migr8.ModelVersion101; import org.baracus.migr8.ModelVersion102; /** * Created with IntelliJ IDEA. * User: marcus */ public class OpenHelper extends BaracusOpenHelper { private static final String DATABASE_NAME="tutorial-app.db"; private static final int TARGET_VERSION=102; static { addMigrationStep(new ModelVersion100()); addMigrationStep(new ModelVersion101()); addMigrationStep(new ModelVersion102()); } /** * Open Helper for the android database * * @param mContext - the android context */ public OpenHelper(Context mContext) { super(mContext, DATABASE_NAME, TARGET_VERSION); } }
Finally, we have to modify the BankAccoutn RowMapper. The developer is responsible how to resolve these references, normally You want lazy loading, so this is our extension :
package org.baracus.dao; // ... public class BankAccountDao extends BaseDao<BankAccount> { @Bean CustomerDao customerDao; // .. private final RowMapper<BankAccount> rowMapper = new RowMapper<BankAccount>() { @Override public BankAccount from(Cursor c) { // ... Long customerId = c.getLong(customerIdCol.fieldIndex); result.setCustomerReference(new LazyReference<Customer>(new ReferenceLoader<Customer>(customerDao, customerId))); result.setTransient(false); return result; } // ... @Override public ContentValues getContentValues(BankAccount account) { // ... if (account.getCustomerReference() != null && account.getCustomerReference().getObjectRefId() != null) { result.put(customerIdCol.fieldName, account.getCustomerReference().getObjectRefId()); } return result; } }; // ... }Because all ID-columns in the Baracus persistence world, You can simplify the lazy loading by passing a ReferenceLoader to the lazy reference. That's all, the resolution of the object now can be done lazily.
I am fully conscious about the fact, that this solution is technologically far behind a JPA implementation like hibernate, but - and I believe this as a full time JEE programmer using JPA all the day - leaving more control to the programmer in a small scale world like android is much better and efficient than making use of reflection and mapping code generation. If you need more transparency, You might inherit the reference class encapsulating all the specialized code for relation resolution. Maybe I am going to make another tutorial about that, later.
Step 3 : One-To-Many relation
Now lets proceed adding the (more trivial case) of the OneToMany relationship (Customer -- *BankAccount)
Therefore we add the Collection<BankAccount> plus getter/setter to the class :
public class Customer extends ModelBase { // ... private String lastName; private String firstName; private Collection<BankAccount> accounts; // ... public Collection<BankAccount> getAccounts() { return accounts; } public void setAccounts(Collection<BankAccount> accounts) { this.accounts = accounts; } }
In order to make the stuff beeing filled with life, we have to write a DAO function retrieving the BankAccount list for each customer :
package org.baracus.dao;
// ... public List<BankAccount> getByCustomerId(Long id) { Cursor c = null; List<BankAccount> result = new LinkedList<BankAccount>(); try { c = this.getDb().query(true, rowMapper.getAffectedTable(), rowMapper.getFieldList().getFieldNames(), BankAccount.customerIdCol.fieldName + "=" + id.toString(), null, null, null, null, null); result = iterateCursor(c); } finally { if (c != null && !c.isClosed()) { c.close(); } } return result; }
Here the FieldList paired with the rowmapper does the job of making the matching row columns to fields more easy. Having a little comfort like this was the initial intention of the OR technique in this framework.
Finally, we extend the Customer's RowMapper implementation in order to get the relation resolved :
package org.baracus.dao; // ... import static org.baracus.model.Customer.*; import static org.baracus.orm.ModelBase.idCol; // ... public class CustomerDao extends BaseDao<Customer> { @Bean BankAccountDao bankAccountDao; // ... private final RowMapper<Customer> rowMapper = new RowMapper<Customer>() { @Override public Customer from(Cursor c) { Customer result = new Customer(); final Long id = c.getLong(idCol.fieldIndex); result.setId(id); result.setLastName(c.getString(lastNameCol.fieldIndex)); result.setFirstName(c.getString(firstNameCol.fieldIndex)); result.setTransient(false); result.setAccounts(new LazyCollection<BankAccount>(new LazyCollection.LazyLoader<BankAccount>() { @Override public List<BankAccount> loadReference() { return bankAccountDao.getByCustomerId(id); } })); return result; } // ... }; // ... }
Done, now we can lazily load the BankAccount entities per customer. This method also leaves all control to the coder. If You want to use a cache implementation instead of the dao, You easily can implement a LazyLoader doing what you want. Because the Container Types in Java are used, there is no Reference-alike solution needed.
Now that we have created a bidirectional relationship, let's try to use it!
Finish - tryout the relationship
In order to demonstrate the lazy stuff inside of our model, we are going to add some code to the MainActivity.package org.baracus; // ... public class HelloAndroidActivity extends Activity { static final Logger logger = new Logger(HelloAndroidActivity.class); static { Logger.setTag("TUTORIAL_APP"); } @Bean CustomerService customerService; @Bean BankAccountService bankAccountService; @Bean ConfigurationService configurationService; @Bean CustomerDao customerDao; @Bean BankAccountDao bankAccountDao; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (!configurationService.isApplicationInitializationDone()) { initData(); configurationService.setApplicationInitializationDone(true); } } private void initData() { Customer johnDoe = new Customer(); johnDoe.setFirstName("John"); johnDoe.setLastName("Doe"); customerDao.save(johnDoe); BankAccount b1 = new BankAccount(); b1.setCustomer(johnDoe); b1.setIban("1234DOE777"); b1.setBankName("Foo Bank Inc."); BankAccount b2 = new BankAccount(); b2.setCustomer(johnDoe); b2.setIban("DOEDEEDOE"); b2.setBankName("Bar Investments Inc."); bankAccountDao.save(b1); bankAccountDao.save(b2); } // ... public void onButtonTestClicked(View v) { // Demonstrate Customer 1:N Lazy Loading List<Customer> allCustomers = customerDao.loadAll(); for (Customer customer : allCustomers) { List<BankAccount> customerAccounts = customer.getAccounts(); for (BankAccount account : customerAccounts) { logger.info("Customer $1 $2 --- account --> $3/$4", customer.getFirstName(),customer.getLastName(), account.getBankName(), account.getIban()); } } // Demontrate BankAccount N:1 Lazy Loading List<BankAccount> allBankAccounts = bankAccountDao.loadAll(); for (BankAccount account : allBankAccounts){ logger.info("Bank Account $1 / $2 --- customer ---> $1 $2", account.getBankName(), account.getIban(), account.getCustomer().getFirstName(), account.getCustomer().getLastName()); } } }
As You can see, the data shall become initialized once. Notice, if You should remove the prior installation of this software version before proceeding! Otherwise You'll run into a NullPointerException! Also, be sure, that all Your beans (Dao, Services etc) are registered in the application context!
The persistence layer of the Baracus framework needs manually written persistence code; a JPA enviroment doesn't. In a normal situation You will not have hundreds of database tables and not an ultra-complex object model in a mobile app. That's one of the reasons, why nobody attempted to bring glassfish onto Your smartphone. This closeness to the database using the basic featureset of the framework enables You to act close to the database saving a lot of performance. Baracus is not database aware, it embraces the SQLite built in every android device and lets You built very performant persistence layers with a sustainable pain on mapping the object ONCE. The result can be a set of smart persistence objects making Your developer life a little easier.
Follow Up : Baracus approach for automatic form validation