Logo Spiria

Persistance de données sous Android

9 octobre 2015.
Qu’est-ce qu’un ORM ? Un ORM, pour Object Relational Mapping, est un outils à déstination de la programmation orientée objet permettant de créer une représentation en base de données d’un objet. Prenons l’exemple de l’objet suivant:

Qu’est-ce qu'un ORM ?

Un ORM, pour Object Relational Mapping, est un outils à déstination de la programmation orientée objet permettant de créer une représentation en base de données d'un objet. Prenons l'exemple de l'objet suivant:

public class User {
    private String name = null;
    private int age = -1;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Avec un ORM cet objet deviendra une table "User" contenant les champs "name" et "age". Ces champs seront typés selon le moteur de base de données utilisé (par exemple, le champ "name" sera certainement un "varchar" si le projet est "mappé" sur une base de données Oracle.) L'intérêt est donc d'avoir une approche dite "objet" de sa base de données. La persistance des instances de ces objets se fait à l'aide d'un composant dédié de l'ORM appelé un DAO pour Data Access Object. Le rôle de ce DAO sera de sauvegarder (persister) les instances des objets "mappés". En reprenant l'exemple d'avant, si j'utilise mon objet comme tel...

User johnDoe = new User("John", 20);

... je pourrais alors le persister en appelant son DAO:

UserDAO.saveOrUpdate(johnDoe);

À l'exécution du code, une nouvelle entrée sera ajoutée à la table "User" de la base de données et contiendra les informations de l'instance. Son champ "name" vaudra alors "John" et son champ "age" "20".

Comment utiliser un ORM avec mon projet Android?

Il existe pour Android un ORM particulièrement simple à utiliser du nom d'ORM Lite disponible à l'adresse suivante: http://ormlite.com/.
Il faudra télécharger les dernières versions des deux fichiers suivants:

  • ormlite-android-x.xx.jar
  • ormlite-core-x.xx.jar


Il suffit ensuite de créer un dossier "libs" à la racine de votre projet Android et d'y déposer les deux archives. Selon votre IDE et la version d'Android que vous utilisez, vous aurez peut-être à les référencer manuellement.
Par exemple, sous Eclipse, en allant via un clique droit sur votre projet dans:
-Properties
--Java Build Path
---Libraries
----Add Jar
-----Allez dans le dossier ou vous avez déposé vos Jars puis ajoutez les.

Quels sont les éléments dont j’ai besoin dans mon code?

L'architecture la plus fréquement rencontrée avec ORM Lite est la suivante:

  • Dans un package "Data", ajoutez un sous package "Entities" pour obtenir la structure "com.myORMLiteProject.Data.Entities".
  • Dans le package "com.myORMLiteProject.Data" créez deux nouvelles classes vides avec les noms "DatabaseHelper.java" et "DatabaseManager.java".
  • Le fichier "DatabaseHelper.java" servira à mettre à jour la base de données, assurer sa création et fournira les DAOs pour les objets et tout autre chose ayant trait à la base de données elle-même.
  • Quant au fichier "DatabaseManager.java", il servira à encapsuler les DAOs afin de faciliter l'utilisation de ces derniers. Il contiendra par exemple une méthode statique "saveUser(User)" qui, dans sa mécanique interne, fera appel au DAO de l'entité "User" pour en effectuer la persistance. Ce fichier n'a d'intêret que pour les projets de petite taille n'ayant pas plus de quelques DAOs, ce qui devrait suffir le plus souvent pour des projets mobiles. Pour les projets de tailles plus conséquentes, il sera fortement conseillé de créer un objet "DaoFoo" pour chaque objet qui sera instancié et appelé séparement au besoin.

Par exemple :

public class UserDAO {
    private Dao userDao = DatabaseHelper.getDao(User.class);

    public static boolean saveUser(User user) {
        try {
            userDao.saveOrUpdate(user);

            return true;
        } catch (SQLException e) {
            // Log exception here.
            return false;
        }
    }
}

Les entités, quant à elles, sont de simple "PoJo" utilisant des annotations pour définir le fait qu'elles sont justement des entités et quels sont leurs champs. Elles doivent obligatoirement avoir un champ "id" qui sera toujours déclaré de la même façon, ainsi qu'un constructeur publique vide. Dans notre cas, l'entité "User.java" pourrait ressembler à ça:

@DatabaseTable
public class User {
    // ******************************
    // Fields
    // ******************************
    @DatabaseField(generatedId = true)
    private int id;
    @DatabaseField
    private String name;
    @DatabaseField
    private int age;
    
    // ******************************
    // Constructor(s)
    // ******************************
    /**
     * Default constructor.
     * 
     * @param name The {@link #User}'s name.
     * @param age The {@link #User}'s age.
     */
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    /**
     * Empty constructor.

     * Used by ORMLite for mapping purpose.
     */
    public User() {
    
    }
    
    // ******************************
    // Getters & setters
    // ******************************
    /**
     * Returns the {@link #User}'s identifier.
     * 
     * @return The {@link #User}'s identifier.
     */
    public int getId() {
        return id;
    }
    
    /**
     * Returns the {@link #User}'s name.
     * 
     * @return The {@link #User}'s name.
     */
    public String getName() {
        return name;
    }
    
    /**
     * Sets the {@link #User}'s name.
     * 
     * @param name The {@link #User}'s name.
     */
    public void setName(String name) {
        this.name = name;
    }
    
    /**
     * Returns {@link #User}'s age.
     * 
     * @return {@link #User}'s age.
     */
    public int getAge() {
        return age;
    }
    
    /** 
     * Sets {@link #User}'s age.
     * 
     * @param age {@link #User}'s age.
     */
    public void setAge(int age) {
        this.age = age;
    }
}

Tel que vu précédemment, voici un exemple de ce à quoi pourrait ressembler le fichier "DatabaseHelper.java":

public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
    // ******************************
    // Constants
    // ******************************
    private static final String DATABASE_NAME = "MyOrmliteProject.sqlite";
    private static final int DATABASE_VERSION = 1;
    
    // ******************************
    // Fields
    // ******************************
    private Dao userDao = null;
    
    // ******************************
    // Constructor(s)
    // ******************************
    /**
     * Default constructor.
     * 
     * @param context The application's {@link #Context}.
     */
    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    
    // ******************************
    // Methods
    // ******************************
    @Override
    public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
        try {
            TableUtils.createTable(connectionSource, User.class);
        } catch (java.sql.SQLException e) {
            Log.e("Can't create database!", e);
        }
    }
    
    @Override
    public void onUpgrade(SQLiteDatabase db, ConnectionSource connectionSource, int oldVersion, int newVersion) {
        try {
            List allSql = new ArrayList();
            
            switch (oldVersion) {
                case 1:
                    // allSql.add("alter table AdData add column `new_col` VARCHAR");
                    // allSql.add("alter table AdData add column `new_col2` VARCHAR");
                    break;
                case 2:
                    break;
                case 3:
                    break;
            }
            
            for (String sql : allSql) {
                db.execSQL(sql);
            }
        } catch (SQLException e) {
            Log.e("Can't update database!", e);

            throw new RuntimeException(e);
        }
    }
    
    /**
     * Returns the {@link #User}'s data access object.
     * 
     * @return The {@link #User}'s data access object.
     */
    public Dao getUserDao() {
        if (null == userDao) {
            try {
                userDao = getDao(User.class);
            } catch (java.sql.SQLException e) {
                Log.e("Can't retrieve DAO!", e);
            }
        }
        
        return userDao;
    }
}

Et un exemple de la classe utilitaire "DatabaseManager.java" qui encapsule l'utilisation des DAOs. Il faut souligner de nouveau que dans le cas ou le projet contient plus de 2 ou 3 entités, il sera largement préférable d'utiliser un objet DAO dédié à chacune d'entre elles. Par exemple, ici nous avons un "UserDAO.java" (qui contiendrait dans cet exemple la même chose que "DatabaseManager.java"), et si nous avions une autre entité gérant des parpaings, nous aurions un "ParpaingDAO" qui ne contiendrait que des accès relatifs à son DAO et aucun relatif à l'objet "User":

public class DatabaseManager {    
    // ******************************
    // Fields
    // ******************************
    private static DatabaseHelper databaseHelper = null;
    
    // ******************************
    // Private methods
    // ******************************
    private static DatabaseHelper getDatabaseHelper(Context context) {
        if(databaseHelper == null) {
            databaseHelper = new DatabaseHelper(context);
        }
        
        return databaseHelper;
    }
    
    // ******************************
    // Public methods
    // ******************************
    /**
     * Saves or updates the given {@link #User} in the database.
     * 
     * @param context The application's {@link #Context}.
     * @param user The {@link #User} to save or update.
     * @return true if the given {@link #User} was correctly saved or updated, false otherwise.
     */
    public static boolean saveOrUpdateUser(Context context, User user) {
        try {
            getDatabaseHelper(context).getUserDao().createOrUpdate(user);

            return true;
        } catch (SQLException e) {
            Log.e("Can't save or update the given User!", e);

            return false;
        }
    }
    
    /**
     * Deletes the given {@link #User} from the database.
     * 
     * @param context The application's {@link #Context}.
     * @param user The {@link #User} to delete.
     * @return true if the given {@link #User} was correctly removed from the database.
     */
    public static boolean deleteUser(Context context, User user) {
        Dao userDao = getDatabaseHelper(context).getUserDao();
        
        try {
            userDao.delete(user);

            return true;
        } catch (SQLException e) {
            Log.e("Can't delete the given User!", e);

            return false;
        }
    }
    
    /**
     * Returns the first {@link #User} from the database, null if not found.
     * 
     * @param context The application's {@link #Context}.
     * @return The first {@link #User} present in database, null if not found.
     */
    public static User getUser(Context context) {
        Dao userDao = getDatabaseHelper(context).getUserDao();
        List result = null;
        
        try {
            result = userDao.queryForAll();
        } catch (SQLException e) {
            Log.e("Can't retrieve the first User!", e);
        }

        if(result != null && result.size() > 0) {
            user = result.get(0);

            return user;
        }
        
        return null;
    }
}

Exemple d'application Android.

Vous trouverez ici https://github.com/Romain41/RunRun une mise en oeuvre de ce qui a été vu. Cet exemple d'application va enregistrer des courses à pied faites par un utilisateur unique et les restituer sous forme de liste. Il s'agit d'un projet Eclipse qu'il suffira d'importer dans votre IDE en tant que projet Android existant.