Premier pas sur SugarORM
Tout ceux qui se seront frottés aux bases de données sous Android savent que c'est une vraie plaie. Pour chaque table il faut créer 3 classes (DAO, Contract et Helper), dès qu'on change quelque chose à la structure il faut flinguer toute la table, bref il y a de quoi retourner en courant chez MySQL.
Les ORM (object-relational mapping) proposent de simplifier tout cela. Le but d'un ORM est de dĂ©finir des correspondances entre les schĂ©mas de la base de donnĂ©es et les classes de l'application. ORMLite, le pionnier a la rĂ©putation d'ĂŞtre un peu lourd. J'ai donc tentĂ© SugarORM (Ă ne pas confondre avec SugarCRM), un ORM signĂ© Satya Narayan, qui propose non seulement de nous dĂ©barrasser de l'obligation de crĂ©er toutes ces classes Ă la con, mais en plus de nous Ă©viter d'avoir Ă Ă©crire la moindre requĂŞte SQLLite (perso j'ai 15 ans de MySQL derrière moi, donc ce n'est pas une petite requĂŞte qui va me faire peur, mais bon hein, si ça peut faire plaisir Ă certains…)
Par contre, pas de miracle :
- si on rajoute un attribut dans une des classes POJO. SugarORM est infichu de rajouter la colonne dans la table correspondante, donc il faut suivre cette procĂ©dure…
- Sugar ORM ne gère pas non plus les relations 1 Ă N. Donc, si vous avez des ArrayLists dans votre classe POJO, vous aurez un petit message d'erreur…
- il n'est pas possible non plus de faire un SELECT DISTINCT avec Sugar ORM
Attention, le Getting Started date de la version 1.3. Pour avoir celui de la tout dernière version, il vaut mieux aller sur la page du dépôt Git. Et ne rêvez pas, c'est aussi peu détaillé !
DĂ©couvrons de ce pas cet ORM…
Importation de la bibliothèque
On va considĂ©rer que vous utilisez Gradle. Au passage, il faudra expliquer au concepteur qu'on n'utilise plus dĂ©sormais plus compile mais implementation… Dans le build.gradle du dossier app/ de l'application, ajouter :
implementation compile 'com.github.satyan:sugar:1.5'
Puis synchroniser, comme le demande Android Studio
Dans le manifest de l'application
Dans app/src/main/AndroidManifest.xml, ajouter entre les balises <application> et </application>
<meta-data android:name="DATABASE" android:value="mabase.db" />
<meta-data android:name="VERSION" android:value="1" />
<meta-data android:name="QUERY_LOG" android:value="true" />
<meta-data android:name="DOMAIN_PACKAGE_NAME" android:value="com.kdjwebdesign.monapp" />
(pensez Ă adapter avec vos propres nom de base et package name d'application)
Et comme attribut dans la balise <application> elle-mĂŞme
android:name="com.orm.SugarApp"
… et penser Ă supprimer android:icon= »@mipmap/ic_launcher » sinon vous aurez cette erreur Ă la compilation
Manifest merger failed : Attribute application@icon value=(@mipmap/ic_launcher) from AndroidManifest.xml:21:9-43
is also present at [com.github.satyan:sugar:1.3] AndroidManifest.xml:13:9-45 value=(@drawable/ic_launcher).
Suggestion: add ‘tools:replace= »android:icon »‘ to <application> element at AndroidManifest.xml:18:5-67:19 to override.
Classes POJO
Petit rappel, une classe POJO (Plain old Java object) est une classe toute simple avec juste un constructeur et des attributs, et qui ici va servir de template pour créer la table correspondante. Je vais reprendre l'exemple du tuto officiel, avec la classe POJO Book. Attention, il y a eu des changements entre la version 1.3 et la version 1.5 (autrefois, on devait hériter de SugarRecord<Book>)
Attention aussi Ă l'annotation @Unique ! Voir plus bas…
public class Book extends SugarRecord {
@Unique
String isbn;
String title;
String edition;
// Default constructor is necessary for SugarRecord
public Book() {
}
public Book(String isbn, String title, String edition) {
this.isbn = isbn;
this.title = title;
this.edition = edition;
}
}
L'annotation @Unique
Rien n'Ă©tant expliquĂ© Ă son sujet, j'ai cru qu'il fallait la mettre dans chaque classe POJO. Grave erreur… du coup, chaque enregistrement ajoutĂ© supprimait le prĂ©cĂ©dent. Des heures de perdues (en plus je croyais que c'Ă©tait un souci liĂ© Ă l'ajout d'une nouvelle colonne dans une des tables. Pfff…)
Erreurs rencontrées
Attention, si vos classes POJO ont déjà un champ id, ça va coincer. Vous allez rencontrer les erreurs
class com.kdjwebdesign.monapp.model.Chat declares multiple JSON fields named id
et
error: id has protected access in SugarRecord
Si vous mettez vos balises <meta-data> avant la balise <application> dans le manifest, l'application va crasher avec ce message d'erreur surréaliste
java.lang.NoSuchMethodException: addFontWeightStyle
Gestion des dates
Les dates sont stockées par Sugar ORM sous la forme d'un timestamp, donc une valeur numérique. Une date vide vaut null (et non pas 0000-00-00 du coup, donc si on cherche les dates vides on mettra WHERE madate IS NULL dans notre requête).
Anecdote amusante… Dans ma classe POJO, j'utilisais un objet java.util.Date… Or, avec la version 1.3 de Sugar CRM, on obtient ce message d'erreur au moment d'appeler la mĂ©thode save() pour insĂ©rer ou mettre Ă jour un enregistrement.
java.lang.NullPointerException: Attempt to invoke virtual method ‘long java.util.Date.getTime()' on a null object reference
On retrouve le souci dans ce ticket de 2015. Dans lequel le dĂ©veloppeur affirme avoir rĂ©solu cela dans la version 1.4. C'est lĂ que j'ai rĂ©alisĂ© que la 1.3 n'Ă©tait pas la dernière version… Mais bordel, mettez Ă jour vos exemples un minimum !
Cas des ArrayLists
Comme je l'ai écrit en début d'article, Sugar ORM ne gère pas les relations 1 à N. Si vous avez des ArrayLists dans votre classe POJO pour gérer les tables liées, cela vous renverra un petit message d'erreur, qui heureusement n'est pas bloquant.
Que faire ? DĂ©jĂ , les exclure Ă l'aide d'une annotation. Ensuite, les peupler Ă la main…
Cas des booléens
Ils sont stockés sous forme de 1 et de 0.
Comment récupérer l'id de l'enregistrement
On a vu qu'il fallait supprimer les attributs id de nos classes POJO. Sauf qu'on va avoir besoin de rĂ©cupĂ©rer l'id de l'enregistrement Ă chaque fois qu'on va vouloir y accĂ©der ! Comme pour la majoritĂ© des frameworks et bibliothèques, l'utilisateur est laissĂ© en plan dès qu'il veut faire quelque chose d'un peu Ă©laborĂ©…
Il n'y a plus qu'à chercher dans des pages de documentation (ah non, zut, c'est vrai, elle est introuvable) ou d'espérer que quelqu'un d'autre a eu le même souci sur Stackoverflow
LĂ encore, je me heurte aux changements de nommage entre les versions de SugarORM : la mĂ©thode save() ne renvoyait pas long mais void dans la version 1.3, il n'y a pas de mĂ©thode getid()… elle s'appelle dĂ©sormais getId() ! Grrrr…
Et puis, les id dans SugarORM sont au format long ! Comme j'utilisais des int pour passer mes id en Extra d'une activitĂ© Ă l'autre, il a fallu repasser partout pour transformer ça en long, sinon bonjour les erreurs de cast…
private long id;
id = intent.getLongExtra("idAppli",0);
Quelques exemples
Récupérer tous les enregistrements d'une table classés
J'aimerais récupérer une liste classée par nom. Pour cela, je vais utiliser la méthode listAll en lui passant le nom de la colonne de tri en 2e paramètre.
listeBooks = Book.listAll(Book.class, "title");
Et si je veux les enregistrements les plus récents en premiers :
listeBooks = Book.listAll(Book.class, "date_release DESC");
Récupérer des enregistrements qui satisfont à une condition
Un enregistrement d'après son id
Book b=Book.findById(Book.class, idBook);
Une liste d'enregistrements d'après un critère (WHERE). On n'oublie pas le list() Ă la fin ! La mĂ©thode orderBy() permet de classer…
List<Book> booksList = Select.from(Book.class).where(Condition.prop("id_editeur").eq(idEditeur)).list();
List<Book> booksList = Select.from(Book.class).where(Condition.prop("id_editeur").eq(idEditeur)).orderBy("ISBN").list()
S'il y a plusieurs conditions, on peut les chaîner.
List<Book> booksList = Select.from(Book.class).where(Condition.prop("id_editeur").eq(idEditeur)).where(Condition.prop("title").eq(titreCherche)).list();
Le contraire de .eq(null) ? C'est isNotNull(). Et le contraire de eq(), c'est notEq()
List<Book> booksList = Select.from(Book.class).where(Condition.prop("description").isNotNull()).list();
List<Book> booksList = Select.from(Book.class).where(Condition.prop("id_editeur").notEq(idEditeur)).list();
Faire une vraie requĂŞte SQL
Vous aussi, vous prĂ©fĂ©rez vous mettre les mains dans le cambouis ? Allons-y…
List<Book> booksList = Book.findWithQuery(Book.class, "Select * FROM Book WHERE id_editeur = "+idEditeur
+"Â ORDER BY title ASC ");
Vous pouvez mĂŞme faire des jointures
List<Book> booksList = Book.findWithQuery(Book.class, "Select * FROM Book LEFT JOIN Editeur ON Editeur.id=Book.id_editeur WHERE description IS NULL ORDER BY Editeur.nom");
Encore plus fort…
Book.executeQuery("INSERT INTO \"main\".\"BOOK\" (\"ID\", \"ID_EDITEUR\", \"TITLE\") VALUES ('1', '2', 'Votre livre de chevet');")
Attention Ă l'unicitĂ© de la clĂ© primaire, hein…
Ajouter une nouvelle colonne Ă la table
Alors lĂ , bienvenue en enfer !
Créer un répertoire nommé /sugar_upgrades dans le répertoire /assets de votre projet (si vous en avez déjà un, sinon il se crée dans app/src/main/)
A l'intĂ©rieur de /sugar_upgrades, crĂ©er un fichier nommĂ© <version>.sql, dont le numĂ©ro de version correspond Ă celui de la base. Par exemple 1.sql, 2.sql… Ce fichier contient le script SQL correspondant Ă l'Ă©volution de la base. Par exemple
alter table NOTE add NAME TEXT;
Adapter ce numéro de version à celui de la balise <meta-data android:name= »VERSION »> dans le manifest
Bref, c'est la grosse galère, et il vaut mieux avoir bien rĂ©flĂ©chi Ă la structure de sa base de donnĂ©es… Et si cela ne fonctionne toujours pas, je vous invite Ă consulter ce ticket
En conclusion, oui SugarORM facilite bien la tâche, mais non, on ne peut pas encore parler de « insanely easy way to work with Android databases »