Cupboard vs Room

At I/O, Google announced their Architecture Components effort for Android. This effort contains of a guide and a couple of libraries that help you implementing the “reference” architecture. One of these libraries is Room. Going forward, Room will be the official Android persistence library.

As you might be aware, a couple of years ago I released Cupboard. Cupboard, like Room, is also a persistence library which I created because the existing ones at the time didn’t fit my needs.

Some people have been asking what the announcement of Room means for Cupboard. Don’t worry, Cupboard has been stable for quite a while and is used in many apps already. I currently see no reason to deprecate it. If you are starting a new project however, you might consider using Room. In the rest of this post, I’ll try to compare Room to Cupboard to help you decide.

Note that Room currently is still an alpha release and as such the API might still be incomplete or change.

Entities

Room and Cupboard are both tools that let you map objects to a SQlite database. Room uses the term “persistence library” because unlike other similar tools it doesn’t manage any database relations between objects. Cupboard doesn’t either. This reduces a lot of complexity and prevents performance issues.

The objects that you store are referred to as entities. Here’s what a typical entity in Room looks like:

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;
}

Here’s the same entity using Cupboard:

class User {
    public Long _id;

    public String firstName;
    public String lastName;
}

As you can see, Room requires you to annotate your entities with @Entity. It also requires you to specify the primary key for the entity. The primary key is what identifies the entity in the database.

The Cupboard entity looks similar, but lacks annotations. Cupboard does not allow you to set a primary key. By convention the _id field is always the auto incremented primary key. Room gives you a little bit more ceremony, but also more flexibility. Room also allows for specifying composite primary keys and whether the primary key should be auto generated.

Manipulating data

In essence, Cupboard is just a mapper from Cursor to Java objects and from Java objects to ContentValues. Cupboard was designed to make it possible to get your data from a SQLiteDatabase, Cursor, or ContentProvider. It somewhat hides SQL from you by using the put and get primitives, but you can also query of course. A query result is basically a wrapper around a Cursor and you can convert it to a List of objects or iterate it. Cupboard doesn’t really care where your data comes from, its only goal is to map a query result (Cursor) row to an object. This allows you to use Cupboard with any API that returns a Cursor, like the built in content providers like MediaStore or ContactsContract When you put or get an entity, it doesn’t do a lot of sanity checks, it just converts the object you give it to ContentValues.

Room has much stronger ties to SQL. Its backed by an annotation processor which allows it to do compile time checks on the SQL you write, very cool! Room always operates on a database, an instance of RoomDatabase to be exact. Room requires you to define DAO interfaces that describe the data operations that you’d like to perform. If you ever used Retrofit, this is very similar to how you define a service interface for REST calls. Here’s an example from the Room documentation:

@Dao
public interface MyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertUsers(User... users);

    @Insert
    public void insertBothUsers(User user1, User user2);

    @Insert
    public void insertUsersAndFriends(User user, List<User> friends);
}

Every data access object (DAO) needs to be annotated with @DAO, and each method needs to be annotated with the operation you like to perform: @Insert, @Delete @Update, or @Query. As the @Insert example shows, Room understands various forms for passing arguments, which is very powerful.

Here’s a query example:

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
}

The SQL statement in the @Query annotation will be compile-time checked by Room, so if you make a typo the compiler will fail to compile or warn you! The compiler will also warn you for other cases, such as when Room does not know how to convert a query to the requested entity. Room uses SQLite bind parameter, like :minAge to map to parameter names. The query you specify can be more complex than show here. It’s totally OK to return only the columns you need or to use a join query for example.

Room currently (alpha1) has no options for returning “lazy” or iterating results, but it can return results as a Cursor. The Room documentation has an explicit note discouraging the use of Cursors though. This means that if you have a large result set, and you follow the recommendations, there only option is to have Room return a List of things, which might not work if the result set is large.

The good news is that Google is aware of this limitation and support for lazy evaluation is being worked on.

Database management

Unlike Room, Cupboard doesn’t manage your database. For all database calls you make to Cupboard, you pass in the database yourself, which you manage somewhere. A common pattern is to use cupboard-tools which provides a ContentProvider that will make sure the database is initialised and upgraded correctly. Cupboard provides two methods to make sure the database schema is in sync with your entities: createTables() will create all required tables and indices and upgradeTables() will add or update existing tables and indices. You don’t have to use these methods. It’s totally fine to create or modify or wrap an existing schema, as long as Cupboard can map it.

Room takes a more managed approach and the database becomes the entry point for getting an implementation of your @Dao interfaces. This uses annotations again:

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract MyDao myDao();
}

The first step is to create a class that extends RoomDatabase. This class needs to be annotated with @Database. You pass the database version and the entities that this database should support in the annotation. This allows Room to generate the correct schema, and check that the entities you use in your DAOs are used in the schema. Room will create the schema for you and add a table to ensure the schema matches your code. If you update your code, but forget to update the database version, Room will detect this and let you know.

Currently there doesn’t seem to be a way to customise the schema (adding a FTS table or triggers, etc) by hooking into onCreate() and onUpdate() of the database, or to query tables that are not managed by Room.

To actually use the database, you use Room.databaseBuilder():

AppDatabase db = Room.databaseBuilder(getApplicationContext(),
        AppDatabase.class, "database-name").build();

This database instance has to be managed somewhere. The architecture guide suggest using the Repository pattern, which effectively is a singleton that uses the DAO to perform the operations. Since Room doesn’t support ContentValues at this time, using a ContentProvider for managing the database isn’t really viable, as all calls to ContentProvider calls get ContentValues as input in stead of your entity classes.

Because the database is managed by Room, the generated code can potentially do optimisations that Cupboard can’t, like pooling the SQLite statement objects.

Data migration

When you update your database version, you need to provide a Migration to upgrade the schema from one version to another. If you don’t provide a migration, Room will wipe out your database. This means that you have to be 100% sure that you include the correct migrations for all database versions that are alive in the field.

Here’s another example from the Room docs:

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
                + "`name` TEXT, PRIMARY KEY(`id`))");
    }
};

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE Book "
                + " ADD COLUMN pub_year INTEGER");
    }
};

Note how there are two migrations provided: from version 1 to version 2 and from version 2 to version 3. Room can upgrade a database directly from version 1 to 3 using these migrations. However, if you’d set you database version to 4 by mistake or on purpose, every upgrade would now rebuild the database and delete all data!

In this sense Room is more strict when it comes to migrations compared to Cupboard. Cupboard will simply never throw away your data, at the (low IMHO) risk that your data becomes inconsistent because you forgot to migrate data, while Room will remove all data if it can’t ensure the schema matches your entities, unless you have the correct Migrations defined.

Summary

To me, Room and Cupboard are similar in the sense that they both don’t try to be full featured ORMs but provide a simple and powerful interface for accessing your data.

To sum up:

  • Room has more powerful entity mapping, allowing for foreign keys and specifying (composite) primary keys
  • Room uses DAO code generation and compile type checking and thus is more type safe, as long as you do all data access through Room.
  • Cupboard allows you to work with more data sources, like ContentProviders or raw Cursors, whereas Room only works with a database.
  • Cupboard is more flexible when it comes to wrapping an existing schema, Room currently requires tables to be managed by Room.
  • Room has more strict schema checking and migrations; not providing (all) migrations will potentially destroy data.

Of course, Room is in alpha now, so features may still change or be added. To keep the length of this post reasonable, I haven’t listed all features of Room, so be sure to checkout the documentation.

Thanks to Rebecca Franks and Yigit Boyar for reviewing this post.



Questions, remarks or other feedback? Discuss this post on Google+