ORMLite pour Android

Johan Poirier Commentaires

Dans le cadre de mon étude des divers frameworks pour le développement java sur Android (voir mon article précedent), j’ai commencé par introduire ORMLite pour Android dans mon application démo (à voir ici sur Github). ORMLite pour Object Relational Mapping Lite est un ORM léger pour Java supportant plusieurs bases de données dont SQLite qui nous intéresse directement pour nos développements Android.

L’utilisation d’ORMLite

Annoter son modèle

En Android natif, les objets de notre modèle sont de simples POJO tels que :

public final class User {

    private int id;
    private String firstName;
    private String lastName;
    private String login;
    private String password;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    ...
}

Nous allons utiliser les annotations d’ORMLite pour enrichir notre POJO :

// We declare a specific DAO, not the one generated by ORMLite
@DatabaseTable(daoClass = UserDaoImpl.class, tableName = User.TABLE_NAME)
public final class User implements Serializable {

    private static final long serialVersionUID = -3366808703621227882L;

    public static final String TABLE_NAME = "users";
    
    @DatabaseField(generatedId = true, columnName = Schema.ID)
    private long id;
    
    @DatabaseField(columnName = Schema.FIRST_NAME)
    private String firstName;
    
    @DatabaseField(columnName = Schema.LAST_NAME)
    private String lastName;
    
    @DatabaseField(columnName = Schema.LOGIN)
    private String login;
    
    @DatabaseField(columnName = Schema.PASSWORD)
    private String password;
    
    public User(long id) {
        this.id = id;
    }
    
    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    
    ...
}

Nous avons donc ajouté @DatabaseTable et @DatabaseField sur notre classe. Les annotations sont explicitement nommées et documentées ici.

Nous pouvons également déclarer des relations entre objets du modèle :

// Foreign field
@DatabaseField(canBeNull = false, foreign = true)
private Account account;

// Foreign collection
@ForeignCollectionField(eager = true, orderColumnName = Booking.Schema.CHECKIN_DATE)
private ForeignCollection<Booking> bookings;

Quelques points négatifs sur les relations :

  • par défaut, les objets étrangers ne sont pas requêtés, seul l’id est renseigné dans un objet vide (par exemple, pour un object User, l’object Account aura seulement son id de renseigné). Il est quand même possible d’activer la mise à jour automatique des objets étrangers (voir le foreignAutoRefresh).
  • pas moyen de préciser l’ordre de tri de la collection quand elle est retournée (certes il est possible de le faire en Java mais le faire en SQL aurait été beaucoup plus simple et rapide). Il est également possible d’utiliser le DAO pour mettre à jour cet objet :
accountDao.refresh(user.getAccount());

La gestion des relations entre objets est une des bases d’un ORM et c’est un vrai plus par rapport à de l’android natif.

Les DAO

ORMLite s’occupe du cycle de vie des DAO :

  • la création via le DaoManager
  • une fois créés, ils sont réutilisés car leur création est une opération côuteuse

Exemple d’appel au DaoManager :

// Long is the class of the ID field of the object User
Dao<User, Long> userDao = DaoManager.createDao(connectionSource, User.class);

Il est également possible (et conseillé) de définir ses propres DAO avec une interface et une implémentation :

// UserDao inteface that extends DAO
public interface UserDao extends Dao<User, Integer> {
    ConnectionSource getConnectionSource();
    User findByLogin(String login);
}

// The UserDaoImpl must also extends BaseDaoImpl
public class UserDaoImpl extends BaseDaoImpl<User, Integer> implements UserDao {

    private ConnectionSource connectionSource;

    public UserDaoImpl(ConnectionSource connectionSource, DatabaseTableConfig<User> tableConfig) throws SQLException {
        super(connectionSource, tableConfig);
        this.connectionSource = connectionSource;
    }

    @Override
    public ConnectionSource getConnectionSource() {
        return connectionSource;
    }

    @Override
    public User findByLogin(String login) {
        try {
            List<User> users = this.queryForEq(Schema.LOGIN, login);
            if(!users.isEmpty()) {
                return users.get(0);
            }
        } catch (SQLException e) {
            Log.e(C.LOG_TAG, "Error querying users for " + login + " login.", e);
        }
        return null;
    }
}

Dans le cas de l’utilisation d’un Dao spécifique, il faut obligatoirement préciser la classe dans l’annotation @DatabaseTable de l’objet User (daoClass). C’est assez moyen car maintenant notre objet du modèle est lié à l’implémentation de notre DAO (!). On va dire que dans le développement mobile, il faut savoir faire des concessions…

Un dernier petit détail : il est possible d’activer un cache au niveau des DAO (voir la doc).

public HotelDao getHotelDao() {
    if (hotelDao == null) {
        try {
            hotelDao = getDao(Hotel.class);
            hotelDao.setObjectCache(true);
        } catch (Exception e) {
            Log.e(C.LOG_TAG, "unable to get hotel DAO", e);
        }
    }
    return (HotelDao) hotelDao;
}

Les transactions

ORMLite fournit un mécanisme de transactions simple (voir la doc) :

final Hotel hotel = new Hotel();
TransactionManager.callInTransaction(connectionSource,
  new Callable<Void>() {
    public Void call() throws Exception {
        // new hotel
        hotelDao.create(hotel);
        
        // set the hotel to the booking
        booking.setHotel(hotel);
        
        // update our booking
        bookingDao.update(booking);
        
        return null;
    }
});

Le QueryBuilder

Le QueryBuilder a pour but de construire des requêtes SQL sans faire du SQL (ou presque). Il permet de chaîner les appels de méthodes afin de rendre plus lisible la requête :

queryBuilder.where().eq(User.Schema.LOGIN, "demo").and().eq(User.Schema.PASSWORD, "1234");

Pas besoin de s’étaler sur cette fonctionnalité qui n’est pas spécifique à Android. La documentation est d’ailleurs très complète à ce sujet.

L’intégration à Android

Création et mise à jour de schémas

ORMLite fournit un OrmLiteSqliteOpenHelper qui étend le SQLiteOpenHelper d’Android et qui permet de créer automatiquement le schéma SQLite et de le mettre à jour. Cette classe surcharge les onCreate et onUpgrade pour les besoins de SQLite. D’autres outils sont disponibles comme TableUtils qui permet de créer, vider et supprimer des tables.

Accès aux DAO dans les activités

ORMLite fournit une classe qui surcharge Activity pour fournir à nos activités un accès direct à la couche de persistence : OrmLiteBaseActivity. Pour faire court, elle fournit un Helper qui nous permet de récupérer le OrmLiteSqliteOpenHelper du chapitre précédent. Ce helper est géré par la classe OpenHelperManager qui permet de gérer le cycle de vie du helper afin qu’une seule instance soit présente au sein de l’application.

// MyOrmLiteSqliteOpenHelper is our implementation of OrmLiteSqliteOpenHelper
public class LoginActivity extends OrmLiteBaseActivity<MyOrmLiteSqliteOpenHelper> {
    
    public void onCreate(Bundle savedInstanceState) {
        int userId = 42;
        User user = ((UserDao) getHelper().getDao(User.class)).queryForId((int) userId);
    }
}

Si vous voulez avoir accès à vos DAO ailleurs que dans vos activités, vous pouvez donc utiliser la classe OpenHelperManager :

OpenHelperManager.getHelper(context, MyOrmLiteSqliteOpenHelper.class);

Par contre, il faudra bien prendre soin de libérer le helper afin de fermer la connection à la bdd (OpenHelperManager.release()).

Performances

Les trucs à savoir

  • Il peut y avoir un temps de chargement très long des DAO au démarrage de l’application. Afin de contrer ça, ORMLite fournit un utilitaire permettant de générer un fichier plat de description de votre schéma qui sera ensuite utilisé au démarrage de l’application pour charger les DAO au lieu d’utiliser la réflexion qui est extrêment coûteuse sur Android. Le fichier généré s’appelle ormlite_config.txt et sera stocké dans le répertoire res/raw/. Il faudra ensuite le référencer dans votre classe OrmLiteSqliteOpenHelper (pour de plus amples informations, voir la doc d’ORMLite).

  • Vous pouvez préciser le nom de votre classe OrmLiteSqliteOpenHelper dans le fichier res/values/strings.xml :

<string name="open_helper_classname">org.pullrequest.android.bookingnative.domain.DatabaseHelper</string>

Ceci permettra d’éviter à ORMLite la recherche de cette classe par réflexion (encore une fois).

Benchmarks

J’ai testé l’insertion de 1000 objets simples avec et sans transaction :

  • sans tx : 1000 objets en 97874 ms, soit 98 ms par objet
  • avec tx : 1000 objets en 842 ms, soit 0,8 ms par objet

On voit que l’utilisation des transactions est bien plus rapide dans le cas d’insertions en masse.

Reste à comparer avec du Android natif :

  • sans tx : 1000 objets en 92468 ms, soit 92 ms par objet
  • avec tx : 1000 objets en 1178 ms, soit 1,2 ms par objet

Ces chiffres donnent juste un ordre d’idée car il faudrait beaucoup plus de tests pour avoir un benchmark plus précis, mais ils me confortent dans l’idée qu’ORMLite ne pénalise pas mon application en terme de performances.

Le ressenti utilisateur

Comme écrit dans le chapitre précédent, je ne ressens aucune différence notable en terme d’utilisation de l’application.

Conclusion

ORMLite est donc un outil assez complet et assez léger (310 Ko) pour nos applications Android. Il nous permet d’écrire un code plus propre, plus lisible et plus léger. Il nous rapproche aussi de l’architecture utilisée dans nos développements Java côté serveur.

Pour aller plus loin, nous pourrons coupler ORMLite avec un framework d’injection comme RoboGuice, que l’on étudiera dans un prochain article. Stay tuned !

Tags :