Android és adatbázis

2010. május 22. szombat - kepes

Előző bejegyzésekben már írtam a HTC Desire felhasználhatóságáról, tapasztalataimról, mint end-user. Mivel szoftverfejlesztőként is érdekelt a telefon és az Android, ezért amint megvolt a készülék, azonnal elkezdtem nézni az Android SDK-t. Elsőként szerettem volna egy egyszerű kis alkalmazást összerakni, amely tartalmaz beviteli mezőket, és egy listát, ahol a már felvitt elemek módosíthatók.

A lista természetesen legyen a tapifonokon már megszokott szépen gördíthető lista, a beviteli képernyő standard beviteli mezőkből álljon össze. Egy ilyen alkalmazás leírását a Developer portál elég jól leírja a Guideokban erre nem térek most ki. Ami szerintem egy kicsit elbonyolított, és nem épp lényegre törő rész ebben az útmutatóban az az adatok tárolása, ezen belül is az Androidba beépített SQLite adatbázis kezelő leírása. Ezt egy külön részben tovább boncolgatja a Notepad tutorial, de itt sajnos már belekeverik a Content Provider-eket is, így nem kapunk kellő instrukciót, hogyan is kellene használni az adatbázist.

A neten található leírások általában azt a módszert erőltetik, hogy a Cursor objektumon végig iterálva, vagy azt felhasználva a ListView objektumhoz a SimpleCursorAdatpter segítségével kezeljük mi az adatbázis kapcsolatot, a kurzort és az ezzel járó minden nyűgös dolgot. Ez nekem nem igazán tetszett, hiszen a Java világában már jópár éve létezik az ORM technika, amit általában egy J2EE alkalmazásban a Hibernate-el szoktunk használni (vagy JPA kinek mi tetszik). Így nincs szükség az adatbázis kapcsolat közvetlen kezelésér, kurzorok bezárogatására, adatbázis kivételek kezelésére. Bánatomra a Hibernate és a JPA nem tűnik jó megoldásnak Android alatt, mert a Hibernate nem kezeli megfelelően az SQLite-ot, és különben is miért lövünk ágyúval verébre?

Így arra gondoltam, ORM technikát nem felejtem el, de az itt felmerülő igényeknek megfelelően alakítom ki. Amit meghatároztam igényként:

  • Lightweight megoldás, lehetőleg J2EE technológiák nélkül
  • Az alkalmazás felületének és üzelti logikájának elkülönítése az adatbázistól
  • Az adatbázis POJO objektumokon keresztül reprezentált

Nem tűnik nagy feladatnak, mégis sokat segít majd később. Elsőként kitaláltam egy alkalmazást. Mindig is szerettem a helyzetmeghatározással foglalkozni, tároljunk hát térkép pontokat adatbázisban.

A tábla szerkezete:

  • id
  • név
  • szélesség
  • hosszúság
  • magasság

Az adatbázis műveletekhez létrehoztam egy SQLiteOpenHelper osztályt a developer portálon megfogalmazottak szerint:

public class DatabaseHelper extends SQLiteOpenHelper {

private static final String TAG = "DatabaseHelper";
private static final String DATABASE_NAME = "mydatabase.db";
private static final int DATABASE_VERSION = 1;

public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}

@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("create table " + MapPoint.TABLE_NAME + " (" +
MapPoint._ID + " integer primary key," +
MapPoint.NAME + " text," +
MapPoint.LATITUDE + " real," +
MapPoint.LONGTITUDE + " real," +
MapPoint.ALTITUDE + " integer);");
}

@Override

public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

Log.w(TAG, "Upgrading database from version " + oldVersion + " to "

+ newVersion + ", which will destroy all old data");

db.execSQL("DROP TABLE IF EXISTS mappoint");

onCreate(db);

}

}

Az osztályból látszik, hogy szükséges egy MapPoint nevű osztály. Az ORM-ben ez az osztály lesz a Model osztályom. Így néz ki:

public class MapPoint implements BaseColumns {

 public static String NAME = "name";
 public static String LATITUDE = "latitude";
 public static String LONGTITUDE = "longtitude";
 public static String ALTITUDE = "altitude";

 public static String TABLE_NAME = "mappoint";

 public static final String DEFAULT_SORT_ORDER = "name";

 private Long _id;
 private String name;
 private Double latitude;
 private Double longtitude;
 private Integer altitude;

 public Long get_id() {
 return _id;
 }
 public void set_id(Long _id) {
 this._id = _id;
 }
 public String getName() {
 return name;
 }
 public void setName(String name) {
 this.name = name;
 }
 public Double getLatitude() {
 return latitude;
 }
 public void setLatitude(Double latitude) {
 this.latitude = latitude;
 }
 public Double getLongtitude() {
 return longtitude;
 }
 public void setLongtitude(Double longtitude) {
 this.longtitude = longtitude;
 }
 public Integer getAltitude() {
 return altitude;
 }
 public void setAltitude(Integer altitude) {
 this.altitude = altitude;
 }
}

A Model osztály az Adroidos BaseColumn-ból származik, ami miatt egy _ID nevű mező van definiálva benne. Ez azért jó, mert ilyen mezője minden SQLite adatbázs táblának kell legyen, és így nem kell külön foglalkozni vele.
A Model osztályban nem biztos, hogy jó helyen vannak a statikus tulajdonságok, de egyelőre itt hagyom őket. Ezeket a Notepad tutorial a ContentProvider-be rakja, de nekünk ilyenünk nem lesz.

Végül a legfontosabb elem, a DAO osztály következik. Ez az osztály intézi el végül is az ORM-et, lekéri az adatbázisból a rekordokat, map-eli őket a Model osztályba, jelen esetben a MapPoint-be.

public class MapPointDao {

 private DatabaseHelper databaseHelper;

 public MapPointDao(Context context) {
 this.databaseHelper = new DatabaseHelper(context);
 }

 public int deleteById(long id) {
 return -1;
 }

 public long insert(MapPoint mapPoint) {
 ContentValues values = new ContentValues();
 values.put(MapPoint.NAME, mapPoint.getName());
 values.put(MapPoint.LATITUDE, mapPoint.getLatitude());
 values.put(MapPoint.LONGTITUDE, mapPoint.getLongtitude());
 values.put(MapPoint.ALTITUDE, mapPoint.getAltitude());

 SQLiteDatabase db = databaseHelper.getWritableDatabase();        

 long rowId = db.insertOrThrow(MapPoint.TABLE_NAME, MapPoint.NAME, values);
 db.close();

 if (rowId > 0) {
 return rowId;
 }

 throw new SQLException("Failed to insert row into ");
 }

 public List find(boolean distinct, String[]  columns,
 String  selection, String[]  selectionArgs, String  groupBy, String  having,
 String  orderBy, String  limit) {

 MapPoint mapP = new MapPoint();
 mapP.set_id(new Long(1));
 mapP.setAltitude(new Integer(2));
 mapP.setLongtitude(new Double(200));

 findByExample(mapP);

 if (TextUtils.isEmpty(orderBy)) {
 orderBy = MapPoint.DEFAULT_SORT_ORDER;
 }

 SQLiteDatabase db = databaseHelper.getReadableDatabase();
 Cursor c = db.query(distinct, MapPoint.TABLE_NAME, columns, selection, selectionArgs, groupBy, having,
 orderBy, limit);

 List list = new ArrayList();

 while (c.moveToNext()) {
 MapPoint mapPoint = new MapPoint();

 mapPoint.set_id(c.getLong(c.getColumnIndex(MapPoint._ID)));
 mapPoint.setName(c.getString(c.getColumnIndex(MapPoint.NAME)));
 mapPoint.setAltitude(c.getInt(c.getColumnIndex(MapPoint.ALTITUDE)));
 mapPoint.setLatitude(c.getDouble(c.getColumnIndex(MapPoint.LATITUDE)));
 mapPoint.setLongtitude(c.getDouble(c.getColumnIndex(MapPoint.LONGTITUDE)));

 list.add(mapPoint);
 }
 c.close();
 db.close(); 

 return list;
 }

 public List find(String  selection, String[]  selectionArgs) {
 return find(false,null,selection, selectionArgs, null, null, null, null);
 }

 public List findAll() {
 return find(null,null);
 }

 public int update(MapPoint mapPoint) {
 return -1;
 }

 public List<Map<String, String>> convertForAdapter(List<MapPoint> mapPointList) {
 List<Map<String, String>> list = new ArrayList<Map<String, String>>();
 for (int i=0; i<mapPointList.size(); i++) {
 Map<String, String> map = new HashMap<String, String>();

 map.put(MapPoint._ID, String.valueOf(mapPointList.get(i).get_id()));
 map.put(MapPoint.NAME, mapPointList.get(i).getName());
 map.put(MapPoint.LATITUDE, String.valueOf(mapPointList.get(i).getLatitude()));
 map.put(MapPoint.LONGTITUDE, String.valueOf(mapPointList.get(i).getLongtitude()));
 map.put(MapPoint.ALTITUDE, String.valueOf(mapPointList.get(i).getAltitude()));
  list.add(map);
 }
 return list;
 }
}

A DAO osztály-t nem fejeztem be, csak a find() és insert() metódusokat programoztam le, a delete() és update() mindeninek házi feladat :) . Az igazság az, hogy már most rájöttem ez nem egészen lesz így jó, mert a DAO osztályt így minden adatbázis táblára létre kell hoznom, minden mezőt egyesével map-elni kell. Nem szeretem az ilyet.

A DAO minden lekérdező metódusa a Model osztályt, vagy az ebből generált lista objektumot adja vissza lekérdezésnél. A módosító metódusok általában azt adják vissza, mennyi rekord módosult, az insert() pedig az új rekord id-t.

Lényeges kiemelni a DAO osztály convertForAdapter() metódusát, ami a SimpleAdatpter számára fog használható objektumstruktúrát előállítani. A find() által visszaadott List objektumból egy más struktúrájú listát állít elő, aminek az elemei már nem Model osztályok.

Végül lássuk, hogyan jelenik meg az Activity-ben a ListView. Az egész Activity-t nem másolom ide be, csak egyes részeit.

 private ListView listMapPoints;

 private List mapPointList;
 private MapPointDao mapPointDao;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);

 this.listMapPoints = (ListView) findViewById(R.id.listMapPoints);

 mapPointDao = new MapPointDao(this);                

 refreshList();
 }

 private void refreshList() {
 mapPointList = mapPointDao.findAll();

 int[] i = { R.id.textName, R.id.textDesc };
 String[] s = { "name", "_id" };

 this.listMapPoints.setAdapter(
 new SimpleAdapter(this, mapPointDao.convertForAdapter(mapPointList),
 R.layout.pointlistitem,s,i));
 }

Először az onCreate()-ben kikeressük a ListView objektumot a layout fájlból, majd létrehozzuk a DAO-t. A refreshList() metódusban programoztam le az lista elemek lekérdezését, mert így a refresh esetleg máshonnan is meghívható. Például egy dialógus ablakot definiálhatunk, ami törölni tud lista elemeket, és a törlés után jó ha a lista frissül.

Miért jó?

  • Szerintem elég lightweight lett, nem igényel külső lib-et
  • Az adatbázis kezelés a program egyetlen pontján van, nem kell az Activity-ben foglalkozni a kurzorral
  • POJO osztályok
  • Elkülönül az üzleti logika, a megjelenítés és a perzisztencia réteg (MVC)

Problémák

Természetesen ezzel nem állítottam elő egy Hibernate funkcionalitású ORM-et. A fentebbi programban elférnének a következő kiegészítések:

  • A DAO egyes metódusainak paraméterezése történhetne Model osztállyal, így szűrőket lehetne megadni. Például findByExample(Model model) vagy deleteByExample(Model model)
  • Jó lenne a DAO-t generikusra megírni, hogy ne kelljen mindig a mapeléseket megírni
  • A DatabaseHelper az adatbázis szkriptet betölthetné külső fájlból vagy resource-ból
  • A DAO-ban az adatbázis kapcsolatokat nem kellene mindig bezárni, egyszer kéne a konstruktorban létrehozni, és a nyitottat használni
  • Jó lenne megoldani a foreign key-ek kezelését a Model-ben és a DAO-ban, hogy ne kelljen többször bekérdezni, ha például egy lista több táblából épül fel.
  • Hibakezelés és logolás

Tovább fogom fejleszteni kis alkalmazásomat, így a fentebbi problémákra megoldást fogok kidolgozni, amit hasonló módon publikálni fogok.

Címkék: , , ,

Itt lehet hozzászólni !