Non Mutable Entities
Business Object
Für ein Kunden Projekt haben wir eine neues Konzept für den Datenbankzugriff unter Android ausprobiert: Non-Mutable-Entities. Non-Mutable-Objects sind ja an sich nichts neues, java.lang.String
, java.lang.Double
, java.lang.Date
sind Beispiele für bekannte Non-Mutable-Objects. Sie begegnen uns in Java also überall.
Zur Erinnerung was Non-Mutable-Objects ausmacht:
- Alle Attribute sind final.
- Sie werden klassisch durch einen Konstruktor erzeugt.
- Um ein Non-Mutable-Object zu ändern wird ein neues erstellt und das alte weggeworfen.
Diese Vorgehensweise hat drei Vorteile:
- Da die Objekte nicht geändert werden können gibt es keine Konflikte in Multitasking Umgebungen und es sich keine Synchronisation notwendig. Da in Android viele in Hintergrund Threads erledigt wird ist dies sehr hilfreich.
- Da alle Attribute final sind müssen diese auch nicht mit get und set Methoden geschützt werden sondern können
public
gemacht werden. - Die Klassen und Instanzen sind kleiner und Lesezugriffe sind schneller (kein indirekter zugriff mittels einer get Methode). Kleiner und schneller ist immer bessern in Mobilen Applikationen.
Der Nachteile ist natürlich auch klar:
- Schreibzugriffe sind langsamer.
- Schreibzugriffe benötigen viel Tippaufwand im Code.
Non-Mutable-Objects eignen sich also für Datenbank-Entitäten die häufig gelesen werden aber nur selten geschrieben werden. Wo häufig einzelne Attribute geändert werden sind sich dann im Gegenzug nicht so geeignet.
Natürlich kann man auch nicht das “Active Record” Entwurfsmuster einsetzen sondern muss “Data-Access-Objects” verwenden. Aber das sehe ich nicht als Nachteil da sich “Active Record” nicht wirklich bewährt hat.
Hier ein Beispiel einer solchen Entität (Kommentare entfernt, das Original hat natürlich JavaDoc für alle Attribute und Methoden):
package com.company.datamodel.bo;
public class Category
implements
java.io.Serializable
{
public enum Type
{
Null ("", (byte) 0),
Company ("company", (byte) 1),
Voucher ("voucher", (byte) 2);
public static Type value (final byte pos)
throws ArrayIndexOutOfBoundsException
{
Type retval; Search_Enum:
{
for (final Type i : Type.values ())
{
if (i.pos == pos)
{
retval = i;
break Search_Enum;
} // if
} // for
throw new IllegalArgumentException (
"No enum const " + Type.class + " for pos " +
pos);
} // Search_Enum
return retval;
} // value
public static Type value (final String image)
throws IllegalArgumentException
{
Type retval;
Search_Enum:
{
for (final Type i : Type.values ())
{
if (i.image.equalsIgnoreCase (image))
{
retval = i;
break Search_Enum;
} // if
} // for
throw new IllegalArgumentException (
“No enum const “ + Type.class + ” for image “ +
image);
} // Search_Enum
return retval;
} // value
public final String image;
public final byte pos;
Type (final String image, final byte pos)
{
this.image = image;
this.pos = pos;
} // Type
} // Type
private static final long serialVersionUID = 2L;
private static final String TAG = Category.class.getName ();
public static String convertListToString (
final Category [] categories)
{
final StringBuilder retval = new StringBuilder (64);
if (categories != null && categories.length > 0)
{
for (final Category category : categories)
{
if (retval.length () > 0)
{
retval.append (',');
} // if
retval.append (category.externalId);
} // for
} // if return retval.toString ();
} // convertListToString;
public static Category [] convertStringToList (
final String categories)
{
final Category [] retval;
if (categories != null && categories.length () > 0)
{
final String [] categoriesList = categories.split (",\s*");
retval = new Category [categoriesList.length];
int i = 0;
for (final String category : categoriesList)
{
final long categoryNumber = Long.parseLong (category);
retval [i] = new Category (categoryNumber);
i = i + 1;
} // for
}
else
{
retval = null;
} // if
return retval;
} // convertListToString;
public final long externalId;
public final Long revision;
public final String title;
public final Type type;
public Category (final long externalId)
{
// android.util.Log.d (Category.TAG, "+ Category");
// android.util.Log.v (Category.TAG, "> externalId = " + externalId);
this.externalId = externalId;
this.revision = null;
this.type = Type.Null;
this.title = null;
// android.util.Log.d (Category.TAG, "- Category");
return;
} // Category
public Category (
final long externalId,
final Long revision,
final Type type,
final String title)
{
// android.util.Log.d (Category.TAG, "+ Category");
// android.util.Log.v (Category.TAG, "> externalId = " + externalId);
// android.util.Log.v (Category.TAG, "> revision = " + revision);
// android.util.Log.v (Category.TAG, "> type = " + type);
// android.util.Log.v (Category.TAG, "> title = " + title);
this.externalId = externalId;
this.revision = revision;
this.type = type;
this.title = title;
// android.util.Log.d (Category.TAG, "- Category");
return;
} // Category
@Override
public String toString ()
{
final StringBuilder retval = new StringBuilder (64);
retval.append (Category.TAG);
retval.append ("[nsuper=");
retval.append (super.toString ());
retval.append (";nexternalId=");
retval.append (this.externalId);
retval.append (";nrevision=");
retval.append (this.revision);
retval.append (";ntype=");
retval.append (this.type);
retval.append (";ntitle='");
retval.append (this.title);
retval.append ("']");
return retval.toString ();
} // toString
} // Category
Diese Category hat noch zwei Spezialitäten:
- Es wird ein Enumeration-Type gespeichert. Man beachte das ich nicht die eingebauten
ordinal()
undname()
Funktionen verende das deren Werte sich beim hinzufügen und entfernen von Enumerationen verändern. - Es gibt zwei zusätzliche “convert” Funktionen. Diese Funktionen werden für die XML Kommunikation benötigt. Ich habe sie nicht für das Demo entfernt um zu zeigen das eine Entität durchaus auch (System) Logik anhalten kann.
Data Access Objects
In diesem zweiten Kapitel beschäftigen wir uns mit den Data Access Objects. Den Objekten die die als POJO programmierten Entitäten speichern.
Aktive Rekords, d.H. Datenbank-Entitäten die sich selber in einer DB speichern haben sich in der Praxis nicht so bewährt da sie nicht in allen Fällen eingesetzt werden können. Der Grund dafür ist das sie Abhängigkeiten mit der Datenbank Library enthalten. So können Sie z.B: nicht in RMI Aufrufen als Parameter verwendet werden.
In modernen Datenbank Design werden die Aufgaben daher geteilt:
- Entitäten werden als POJO (Plain Old Java Objects) implementiert die für alle Einsatzgebiete gut sind.
- Das Speicher und Laden wird von DAO (Data Access Objects) übernommen.
Hier nun eine mögliche Implementierung von DAOs für Non Mutable Entities unter Android. Die Klasse ist als Utility Klasse implementiert. Aber auch andere Implementations-Optionen wie Singletons sind denkbar.
Wie beim ersten Teil sind die Kommentare gekürzt worden. Im original Code sind alle Parameter, Return-Werte und Attribute mit JavaDoc Kommentaren beschrieben.
Auf SQL Statements kann man bei Verwertung der Android SQLite Schnittstelle zum größten Teil verzichten. Dann werden allerdings die Statement bei jeder Operation von Framework generiert und compiliert was die Performance reduziert. Im diesem Beispiel wurde daher das INSERT
Statements handgeschrieben da im Produkt mit vielen Einfüge-Operationen beim synchronisieren mit dem Backend zu rechnen
sind.
Speichern
Das speichern von Entitäten ist verhältnismäßig einfach. Die Werte werden gebunden und der vorbereitete INSERT
Befehl wird in einer Transaktion ausgeführt. Der Rest ist Fehlerbehandlung.
public static long insert (final Context context, final Category entity)
{
// android.util.Log.d (CategoryModel.TAG, "+ insert");
// android.util.Log.v (CategoryModel.TAG, "> entity : " + entity);
long retval;
if (entity == null)
{
final IllegalArgumentException exception =
new IllegalArgumentException (
"Cannot delete empty category from table " +
CategoryModel.TABLE_NAME + ". Entity must not be null");
android.util.Log.e (CategoryModel.TAG, "Throw", exception);
throw exception;
}
else
{
final android.database.sqlite.SQLiteDatabase db = DatabaseHelper.getDb (context);
assert db != null : "DB is initialised at system startup and should never be null.";
CategoryModel.open (db);
assert CategoryModel.insertStmt != null : "insertStmt should be set by open.";
try
{
CategoryModel.insertStmt.bindLong (1, entity.externalId);
CategoryModel.insertStmt.bindLong (2, entity.revision);
CategoryModel.insertStmt.bindType (3, entity.type);
CategoryModel.insertStmt.bindString (4, entity.title);
db.beginTransaction ();
retval = CategoryModel.insertStmt.executeInsert ();
db.setTransactionSuccessful ();
}
catch (final Exception ex)
{
CategoryModel.logger.log (
java.util.logging.Level.SEVERE, "Cannot insert " +
entity + "n into table " + CategoryModel.TABLE_NAME,
ex);
retval = -1;
}
finally
{
db.endTransaction ();
} // try
} // if
// android.util.Log.v (CategoryModel.TAG, "> return : " + retval);
// android.util.Log.d (CategoryModel.TAG, "- insert");
return retval;
}
Dank der INSERT OR REPLACE
Technik von SQLite ist Update besonders einfach:
public static long update (
final Context context,
final Category entity)
{
// android.util.Log.d (CategoryModel.TAG, "+ update");
// android.util.Log.v (CategoryModel.TAG, "> entity : " + entity);
final long retval = CategoryModel.insert (
/*context => */context,
/*entity => */entity);
// android.util.Log.v (CategoryModel.TAG, "> return : " + retval);
// android.util.Log.d (CategoryModel.TAG, "- update");
return retval;
} // update
Wichtig ist das dieser Trick nur funktioniert wenn die Tabelle einen Primär-Schlüssel hat. Wenn dies nicht der Fall ist muss UPDATE
ausprogrammiert werden.
Laden
Beim laden geht es meisten darum mehrere Elemente zu landen die dann in einer geeigneten collection zurückgeliefert werden. Wir haben uns für einfaches arrays entschieden da diese den geringsten Speicher- und Zeit-Bedarf haben. Eine andere gangbare alternative währen Non-Mutable-Collection die mit Java 1.4 eingeführt worden sind und leider viel zu selten benuzt werden. Diese würden sogar noch besser zu Non-Mutable-Entities passen.
private static Category [] selectElements (
final Context context,
final String selection,
final String [] selectionArgs)
{
Category [] retval;
final android.database.sqlite.SQLiteDatabase db = DatabaseHelper.getDb (context);
com.company.datamodel.Cursor cursor = null;
assert db != null : "DB is initialised at system startup and should never be null.";
try
{
cursor = new com.company.datamodel.Cursor (db.query (
/* table => */CategoryModel.TABLE_NAME,
/* columns => */new String [] {
"externalId", "revision", "type", "title" },
/* selection => */selection,
/* selectionArgs => */selectionArgs,
/* groupBy => */null,
/* having => */null,
/* orderBy => */null));
if (cursor.moveToFirst ())
{
retval = new Category [cursor.getCount ()];
int i = 0;
do
{
retval [i] = new Category (
/* externalId => */cursor.getLong (0),
/* revision => */cursor.getLong (1),
/* type => */cursor.getCategoryType (2),
/* title => */cursor.getString (3));
i = i + 1;
}
while (cursor.moveToNext ());
}
else
{
retval = null;
} // if
}
catch (final Exception ex)
{
CategoryModel.logger.log (
java.util.logging.Level.SEVERE,
"Cannot select rows for “" + selection + "” from table " + CategoryModel.TABLE_NAME,
ex);
retval = null;
}
finally
{
if (cursor != null)
{
cursor.close ();
} // if
} // try
return retval;
} // selectElements
Zu beachten ist das die select funktion private ist. Der Grund sind die Parameter selection und selectionArgs die zu “Low-Level” sind und daher gekapselt werden sollten. Jeder DAO sollte ein oder mehr select funtionen mit höherer abscraktions zur Verfügung stellen. Der simpleste währe dann der select all:
public static Category [] selectElements (final Context context)
{
return CategoryModel.selectElements (
/* context => */context,
/* selection => */null,
/* selectionArgs => */null);
} // selectElements
Oder auch das select by primary key:
public static Category selectElement (
final android.content.Context context,
final long categoryId)
{
// android.util.Log.d (CategoryModel.TAG, "+ selectElement");
// android.util.Log.v (CategoryModel.TAG, "> context : " + context);
// android.util.Log.v (CategoryModel.TAG, "> companyId : " + companyId);
final Category retval;
// the list must have 1 element only
final Category [] list = CategoryModel.selectElements (
/* context => */context,
/* selection => */"externalId=?",
/* selectionArgs => */new String [] {
String.valueOf (categoryId) });
if (list == null || list.length == 0)
{
android.util.Log.e (
CategoryModel.TAG, "> The table " + CategoryModel.TABLE_NAME + " has no element for category " + categoryId + ".");
retval = null;
}
else
{
retval = list [0];
} // if
// android.util.Log.v (CategoryModel.TAG, "> retval : " + retval);
// android.util.Log.d (Category Model.TAG, "- selectElement");
return retval;
} // selectElements
Löschen
Delete ist auch wieder recht einfach. Das meiste ist wieder Fehlerbehandlung. Zu beachten ist das auch delete transaktionsmanagement benötigt.
public static void delete (
final Category entity)
final Context context,
{
// android.util.Log.d (CategoryModel.TAG, "+ delete");
// android.util.Log.v (CategoryModel.TAG, "> entity : " + entity);
if (entity == null)
{
final IllegalArgumentException exception =
new IllegalArgumentException (
"Cannot delete empty category from table " +
CategoryModel.TABLE_NAME + ". Entity must not be null");
android.util.Log.e (CategoryModel.TAG, "Throw", exception);
throw exception;
}
else
{
final android.database.sqlite.SQLiteDatabase db = DatabaseHelper.getDb (context);
assert db != null : "DB is initialised at system startup and should never be null.";
try
{
db.beginTransaction ();
db.delete (
/* table => */CategoryModel.TABLE_NAME,
/* whereClause => */"externalId=?",
/* whereArgs => */new String [] {
String.valueOf (entity.externalId) });
db.setTransactionSuccessful ();
}
catch (final Exception ex)
{
CategoryModel.logger.log (
java.util.logging.Level.SEVERE,
"Cannot delete category from table " + CategoryModel.TABLE_NAME +
". Category = " + entity,
ex);
}
finally
{
db.endTransaction ();
} // try
} // if
// android.util.Log.d (CategoryModel.TAG, "- delete");
return;
} // delete
Natürlich ist dies ein sehr simples Beispiel da keine abhängen Tabellen vorhanden sind. Es sei aber gesagt das die Data Access Object sich auch damit zurecht kommen. Zudem lassen sich die Klassen durch kopieren und anpassen recht schnell entwickelt — der meiste des Fehlerbehandlung ist bei allen Klassen gleich.
Praktischer Einsatz
Im letzte Kapitel zeige ich wie man die Entitäten einsetzt. Die Beispiele stammen aus dem Original Code und zeigen mit wie wenig Aufwand die Entitäten verwendet werden können.
Laden
Zum Laden ruft man einfach ein passendes selectElements
auf.
// get all categories from db
final Category [] categories =
com.company.datamodel.dao.CategoryModel.selectElements (this.context);
if ((categories != null) && (categories.length > 0))
{
Speichern
Zum speichern wird ein Element über den Konstruktor erstellt und dann gespeichert. Dank der INSERT OR REPLACE
Technik ist es egal ob man insert
oder update
aufruft.
this.entity = new com.company.datamodel.bo.Category (
/* externalId => */id,
/* revision => */rev,
/* type => */type,
/* title => */title);
com.company.datamodel.dao.CategoryModel.insert (
this.entity);
this.context,
Löschen
Zum löschen wird auch ein Element über den Konstruktor erstellt. Da die Tabelle einen Primärschlüssel hat muss nur dieser befüllt sein. Hierfür bietet die Klasse einen eigenen Konstruktor.
final long id = com.company.ws.ElementListener.getID (
attributes);
// android.util.Log.v (Del.TAG, "> id: " + id);
final com.company.datamodel.bo.Category entity =
new com.company.datamodel.bo.Category (id);
com.company.datamodel.dao.CategoryModel.delete (
this.context,
entity);
Fazit
Die Verwendung von Non Mutable Entities im Projekt hat sich bewährt. Der Mehraufwand bei Speicher operationen hielt sich in Grenzen und die Applikation läuft stabil und ohne Multitasking
Probleme.