在开发Android应用的过程中,少不了要用到SQLite数据库操作,各种增删查改。。。一般看来,对于不同的实体类的数据库操作,貌似我们只能使用不同的“增删查改”方法来实现,本次的想法就是,能不能抽取出一个通用的框架,使得对于不同的实体类的数据库操作,可以使用同一个接口的实现来做。。。废话少说,进入正题。
一、普通的数据库操作的实现
现在有一个Book的类,里面有三个成员变量,id,tittle和summary,其中id将会作为主键,且自增。Book代码如下:
package com.alex.db.domain; public class Book { private int id; private String tittle; private String summary; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getTittle() { return tittle; } public void setTittle(String tittle) { this.tittle = tittle; } public String getSummary() { return summary; } public void setSummary(String summary) { this.summary = summary; } @Override public String toString() { return "Book [id=" + id + ", tittle=" + tittle + ", summary=" + summary + "]"; } public Book(int id, String tittle, String summary) { super(); this.id = id; this.tittle = tittle; this.summary = summary; } public Book() { super(); } }
一般来说,都会使用SQLiteOpenHelper这个类来打开/创建 一个数据库,这里也一样,我们创建一个DBHelper的类来继承SQLiteOpenHelper,并在DBHelper放入一些public的常量,来代表数据库要创建的表名,表里的字段名,以供使用。
package com.alex.db.dao; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; public class DBHelper extends SQLiteOpenHelper { private static final String NAME = "my.db"; private static final CursorFactory FACTORY = null; private static final int VERSION = 1; public static final String COLUMN_ID = "_id"; public static final String COLUMN_TITLE = "title"; public static final String COLUMN_SUMMARY = "summary"; public static final String TABLE_BOOK_NAME = "books"; public DBHelper(Context context) { super(context, NAME, FACTORY, VERSION); } @Override public void onCreate(SQLiteDatabase db) { String sql = "create table " + TABLE_BOOK_NAME + "(" + COLUMN_ID + " integer primary key autoincrement," + COLUMN_TITLE + " varchar(50)," + COLUMN_SUMMARY + " varchar(200));"; System.out.println("sql=" + sql); db.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // TODO Auto-generated method stub } }
到此,就可以来进行数据库操作了,一般的,我们会写一个IBookDao的接口,里面包含“增删查改”等方法;然后再写一个实现类BookDaoImpl的实现类来实现这个接口,这样能达到隔离业务的目的。为了简便,我们这里查找暂时只实现findAll(全部查找无条件限制)。
IBookDao:
package com.alex.db.dao; import java.util.List; import com.alex.db.domain.Book; public interface IBookDao { /** * 插入一个条目 * * @param book * @return */ long insert(Book book); /** * 根据id来删除一个条目 * * @param id * @return */ int delete(int id); /** * 更新一个条目 * * @param book * @return */ int update(Book book); /** * 查找所有的表中的所有条目,并且返回一个List集合 * * @return */ List<Book> findAll(); }
BookDaoImpl实现类:
package com.alex.db.dao.impl; import java.util.ArrayList; import java.util.List; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import com.alex.db.dao.DBHelper; import com.alex.db.dao.IBookDao; import com.alex.db.domain.Book; public class BookDaoImpl implements IBookDao { private Context context; private DBHelper helper; private SQLiteDatabase db; public BookDaoImpl(Context context) { this.context = context; helper = new DBHelper(context); db = helper.getWritableDatabase(); } public long insert(Book book) { ContentValues values = new ContentValues(); // values.put(DBHelper.COLUMN_ID, book.getId());//这一句不要,因为id是主键且自增 values.put(DBHelper.COLUMN_TITLE, book.getTittle()); values.put(DBHelper.COLUMN_SUMMARY, book.getSummary()); return db.insert(DBHelper.TABLE_BOOK_NAME , null, values); } @Override public int delete(int id) { return db.delete(DBHelper.TABLE_NAME, DBHelper.COLUMN_ID + "=?", new String[] { String.valueOf(id) }); } @Override public int update(Book book) { ContentValues values = new ContentValues(); values.put(DBHelper.COLUMN_SUMMARY, book.getSummary()); values.put(DBHelper.COLUMN_TITLE, book.getTittle()); return db.update(DBHelper.TABLE_NAME, values, DBHelper.COLUMN_ID + "=?", new String[] { String.valueOf(book.getId()) }); } @Override public List<Book> findAll() { List<Book> books = null; Cursor cursor = db.query(DBHelper.TABLE_NAME, null, null, null, null, null, null); if (cursor != null && cursor.getCount() > 0) { books = new ArrayList<Book>(); while (cursor.moveToNext()) { Book book = new Book(); book.setId(cursor.getInt(cursor .getColumnIndex(DBHelper.COLUMN_ID))); book.setSummary(cursor.getString(cursor .getColumnIndex(DBHelper.COLUMN_SUMMARY))); book.setTittle(cursor.getString(cursor .getColumnIndex(DBHelper.COLUMN_TITLE))); books.add(book); } } return books; } }
至此,基本的一个数据库操作就构建完毕了,写一个简单的TestCase来看看:
package com.alex.db.test; import java.util.List; import com.alex.db.dao.IBookDao; import com.alex.db.dao.impl.BookDaoImpl; import com.alex.db.domain.Book; import android.test.AndroidTestCase; public class DBTest extends AndroidTestCase { public void testDB() { IBookDao bookDao = new BookDaoImpl(getContext()); bookDao.insert(new Book(0, "测试标题一", "测试摘要一")); bookDao.insert(new Book(0, "测试标题二", "测试摘要二")); bookDao.insert(new Book(0, "测试标题三", "测试摘要三")); List<Book> books = bookDao.findAll(); for (Book item : books) { System.out.println(item.toString()); } System.out.println("------删除操作分割线------"); bookDao.delete(3); books = bookDao.findAll(); for (Book item : books) { System.out.println(item.toString()); } } }
结果:
二、抽取通用框架要解决的问题:
经过上面的工作,我们基本可以实现Book类的数据库操作了,但是如果现在多出来一个别的类,比如News类,如果按照之前的思路就不得不重新写一套数据库操作接口及其实现类了。但是我们现在不想这样做,我们想抽取一个框架来,让别的类News也能通过它来实现基本数据库的操作。怎么做呢?
既然需要用到不同的类,那么就势必要用到泛型。思路是:
a、写一个带泛型的接口IDaoSupport<M>来让IBookDao去extends;
b、写一个带泛型的抽象类DaoSupportImpl<M> implements IDaoSupport<M>,然后让我们的实现类BookDaoImpl extends DaoSupportImpl<Book>;
1、IDaoSupport<M>
对于第一点而言,实现起来比较简单,可以简单的将IBookDao里的内容 剪切之后,将Book改为泛型M,然后粘贴到 IDaoSupport<M>中。但是要注意一点,原来的int delete(int id);方法,参数id是int类型的主键,但是不一定所有的类都是integer类型作为主键,也有可能是String或者long等类型的,我们可以去数据手册上翻看一下integer,string和long这三个类,会发现他们都实现了Serializable这个接口:
所以,这里选择Serializable来作为id的类型。
IDaoSupport<M>:
package com.alex.db.dao.base; import java.io.Serializable; import java.util.List; import com.alex.db.domain.Book; public interface IDaoSupport<M> { /** * 插入一个条目 * * @param book * @return */ long insert(M m); /** * 根据id来删除一个条目 * * @param id * @return */ int delete(Serializable id); /** * 更新一个条目 * * @param book * @return */ int update(M m); /** * 查找所有的表中的所有条目,并且返回一个List集合 * * @return */ List<M> findAll(); }
IBookDao:
package com.alex.db.dao; import com.alex.db.dao.base.IDaoSupport; import com.alex.db.domain.Book; public interface IBookDao extends IDaoSupport<Book> { }
2、DaoSupportImpl<M>
先来看一下继承关系:public class BookDaoImpl extends DaoSupportImpl<Book> implements IBookDao
DaoSupportImpl<M>将要作为所有实体类数据库操作实现的一个公共类,所以原来在BookDaoImpl中的增删查改方法,还有一些成员变量都要放到DaoSupportImpl<M>里面去。但是在这里,不能单纯的剪切粘贴了,有一些麻烦的问题需要解决,我们一个个的来看是些什么问题:
对于insert和delete方法:
public long insert(Book book) { ContentValues values = new ContentValues(); // values.put(DBHelper.COLUMN_ID, book.getId());//这一句不要,因为id是主键且自增 values.put(DBHelper.COLUMN_TITLE, book.getTittle()); values.put(DBHelper.COLUMN_SUMMARY, book.getSummary()); return db.insert(DBHelper.TABLE_BOOK_NAME, null, values); }
@Override public int delete(Serializable id) { return db.delete(DBHelper.TABLE_BOOK_NAME, DBHelper.COLUMN_ID + "=?", new String[] { String.valueOf(id) }); }
如果一旦改为
public long insert(M m)
那么:
(一)、如何知道M对应的那个表的表名?
我们知道每个实体类一般都会对应一个单独的表,比如Book对应books表,News对应news表;而上面是使用的DBHelper.TABLE_BOOK_NAME来获取的,这显然不合适,因为不可能每个实体类对应的表名都用DBHelper.TABLE_BOOK_NAME来获取,关键在于DaoSupportImpl<M>里,我们只有泛型M,而不知道具体是哪个实体类。
(二)、如何将实体中的数据,按照对应关系导入到数据库表中?
对于上面的insert操作,会将title,summary的内容导入数据库中,那么对于别的实体类呢?它们不一定是这几个成员变量,如何确定实体类的Field和数据库表中的字段的对应关系?
对于update和findAll方法:
@Override public int update(Book book) { ContentValues values = new ContentValues(); values.put(DBHelper.COLUMN_SUMMARY, book.getSummary()); values.put(DBHelper.COLUMN_TITLE, book.getTittle()); return db.update(DBHelper.TABLE_BOOK_NAME, values, DBHelper.COLUMN_ID + "=?", new String[] { String.valueOf(book.getId()) }); } @Override public List<Book> findAll() { List<Book> books = null; Cursor cursor = db.query(DBHelper.TABLE_BOOK_NAME, null, null, null, null, null, null); if (cursor != null && cursor.getCount() > 0) { books = new ArrayList<Book>(); while (cursor.moveToNext()) { Book book = new Book(); book.setId(cursor.getInt(cursor .getColumnIndex(DBHelper.COLUMN_ID))); book.setSummary(cursor.getString(cursor .getColumnIndex(DBHelper.COLUMN_SUMMARY))); book.setTittle(cursor.getString(cursor .getColumnIndex(DBHelper.COLUMN_TITLE))); books.add(book); } } return books; }
出去上面两个问题之外,还需要解决三个问题:
(三)、如何将数据表中列的数据,按照对应关系导入到实体中
这个问题跟问题二是相反的过程,在于update时从数据库中拿数据的过程
(四)、明确实体中主键,获取到主键中封装的值
对于update方法,参数为一个对象,我们要拿到对象中的主键,然后才能去数据库中查到需要更新的那个对应的条目。
(五)、实体的对象创建
findAll方法返回一个List<M>,而对应的M的对象要如何创建呢?
三、问题的解决
下面就是要解决的五个问题:
// 问题一:表名的获取 // 问题二:如何将实体中的数据,按照对应关系导入到数据库表中 // 问题三:如何将数据表中列的数据,按照对应关系导入到实体中 // 问题四:明确实体中主键,获取到主键中封装的值 // 问题五:实体的对象创建
问题一:表名的获取
如何获得实体类对应的表名,有两种方案:
1、如果能够获取到实体,获取都该实体的简单名称,然后将首字母小写,比如Book类,取表名为book,News类,取表名为news
这种方法是可行的,但是有其局限性,我们需要在刚写代码的时候,就将数据库的表名也一起定好,而且不方便改动,如果一旦中途改了类名,那么数据库的表名也要改。。。
2、利用注解来获得实体类对应的表名,让实体名和数据库表名脱离关系。
这种方法比较合适,通用,但是需要用到注解。下面就来看看如何利用注解来获得实体类对应的表名吧:
实际上,要解决问题一,需要先解决问题五:如何获得实体类对象的问题,显然不能用new来获得M m = new M();这里暂时用getInstance()方法来代替。
我们在DaoSupportImpl<M>里写一个方法getTableName()来获取表名:
1、先获得一个M的对象:
M m = getInstance();
2、添加注解:
在Book类的头上加上一行注解:@TableName(DBHelper.TABLE_BOOK_NAME),同时在工程中新建一个注解类TableName,在其中添加方法:String value();,代码如下:
package com.alex.db.dao.annotation; public @interface TableName { String value(); }
@TableName(DBHelper.TABLE_BOOK_NAME) public class Book { private int id; private String tittle; private String summary; ...... }
做完这些之后,我们就可以从Book类中拿到注解的value()方法返回的值,也就是DBHelper.TABLE_BOOK_NAME了。
3、如何拿到Book的类
但是问题又来了,我们在DaoSupportImpl<M>使用的是M泛型,我们如何拿到Book的类呢,这里就需要看一下文档了,getClass方法,返回的是运行时的那个类,所以我们运行时,泛型载入的是Book类,拿到的就是Book的class,载入的是News类,拿到的就是News的class。
所以我们根据class,就可以拿到该类对应的注解对象:
TableName tableName = m.getClass().getAnnotation(TableName.class);
然后调用value方法就可以得到类对应的表名:
if (tableName != null) { return tableName.value(); }
这样问题一就解决了。
问题二:如何将实体中的数据,按照对应关系导入到数据库表中
问题二,也可以按照问题一的思路来解决。在问题一中,我们在Book类上添加注解来在表名和Book类上建立起了对应关系,那么我们也可以在Book类的几个成员变量(Field)上添加注解,来让它们与对应的表中的字段名建立起关系。如下:
@TableName(DBHelper.TABLE_BOOK_NAME) public class Book { @ColumnName(DBHelper.COLUMN_ID) private int id; @ColumnName(DBHelper.COLUMN_TITLE) private String tittle; @ColumnName(DBHelper.COLUMN_SUMMARY) private String summary; ... }
然后再新建对应的ColumnName注解类,并实现value方法。
package com.alex.db.dao.annotation; public @interface ColumnName { String value(); }
通过上面的操作,我们就可以实现insert操作了:
public long insert(M m) { ContentValues values = new ContentValues(); fillColumn(m, values); return db.insert(getTableName(), null, values); } //解决问题二 private void fillColumn(M m, ContentValues values) { Field[] fields = m.getClass().getDeclaredFields(); for (Field item : fields) { ColumnName columnName = item.getAnnotation(ColumnName.class); if (columnName != null) { String key = columnName.value(); String value; try { item.setAccessible(true);//让私有的Field也可以被操作,赋予权限 value = item.get(m).toString(); values.put(key, value);// 会存在问题,没有把主键id给剔除,也存进去;而且id是int类型,类型转换异常 } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } } } }
问题四:明确实体中主键,获取到主键中封装的值
我们先来解决问题四,再来看问题三。这是因为上面的insert操作是存在漏洞的,可以看上面的注解,那就是主键自增和类型的问题没有解决。在插入数据库的时候,我们循环所有的Field的集合fields ,来获得每一个Filed,这里面势必会包含我们自增的主键id,而由于是自增的,我们不应该将id的值也设置到数据库,这样会让SQLite的主键值出现错误。所以这里我们必须要能够标记出主键具体是哪个Field。这里我们也可以用注解来实现。
可以在id的注解上在加一行注解,来标记该Field是主键:
@TableName(DBHelper.TABLE_BOOK_NAME) public class Book { @PrimaryKey(autoincrement=true) @ColumnName(DBHelper.COLUMN_ID) private int id; ...... }
然后再新建一个名为PrimaryKey的注解类,并添加boolean autoincrement();方法:
package com.alex.db.dao.annotation; public @interface PrimaryKey { boolean autoincrement(); }
这里注意到,我们没有使用一个值,而是一个boolean型的表达式:autoincrement=true,用来标记该Field是不是主键。
到这里我们就可以标识出主键,实现一个getIdName()的方法,来获取主键在表中的字段名称,因为不是所有的类的主键都会叫做_id;和一个getIdValue()方法,来获得Id的值:
private String getIdName(M m) { Field[] declaredFields = m.getClass().getDeclaredFields(); for (Field item : declaredFields) { item.setAccessible(true);// 添加权限 PrimaryKey annotation_id = item.getAnnotation(PrimaryKey.class); if (annotation_id != null) { try { // return item.get(m).toString(); ColumnName columnName = item .getAnnotation(ColumnName.class); if (columnName != null) { return columnName.value(); } } catch (IllegalArgumentException e) { e.printStackTrace(); } } } return null; } private String getIdValue(M m) { Field[] fields = m.getClass().getDeclaredFields(); for (Field item : fields) { item.setAccessible(true); PrimaryKey primaryKey = item.getAnnotation(PrimaryKey.class); if (primaryKey != null) { try { return item.get(m).toString(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } } } return null; }
然后,我们就可以将上面的insert方法中调用的fillColumn()方法完善,加入剔除主键的部分:
// 解决问题二 private void fillColumn(M m, ContentValues values) { Field[] fields = m.getClass().getDeclaredFields(); for (Field item : fields) { ColumnName columnName = item.getAnnotation(ColumnName.class); if (columnName != null) { String key = columnName.value(); try { item.setAccessible(true); PrimaryKey primaryKey = item .getAnnotation(PrimaryKey.class); if (primaryKey != null && primaryKey.autoincrement()) { // 说明是主键,且是自增长的,则什么都不做,跳到下一个循环 continue; } String value = item.get(m).toString(); values.put(key, value);// 会存在问题,没有把主键id给剔除,也存进去;而且id是int类型,类型转换异常 } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } } } }
同时,也可以给出删除操作的方法delete():
@Override public int delete(Serializable id) { return db.delete(getTableName(), getIdName(getInstance()) + "=?", new String[] { String.valueOf(id) }); }
问题三:如何将数据表中列的数据,按照对应关系导入到实体中
问题三与问题二是相反方向的问题,解决的思路比较类似,要注意的就是主键id这里是int类型,需要判断主键,然后转换类型,否则会出类型转换错误,可以直接给出findAll和update方法的代码:
findAll方法:
@Override public List<M> findAll() { List<M> result = null; Cursor cursor = db.query(getTableName(), null, null, null, null, null, null); if (cursor != null && cursor.getCount() > 0) { result = new ArrayList<M>(); while (cursor.moveToNext()) { M m = getInstance(); fillField(cursor, m); cursor.close(); result.add(m); } } return result; } // 解决问题三 private void fillField(Cursor cursor, M m) { Field[] fields = m.getClass().getDeclaredFields(); for (Field item : fields) { item.setAccessible(true); ColumnName columnName = item.getClass().getAnnotation( ColumnName.class); if (columnName != null) { String key = columnName.value(); int columnIndex = cursor.getColumnIndex(key); String value = cursor.getString(columnIndex); PrimaryKey primaryKey = m.getClass().getAnnotation( PrimaryKey.class); try { if (primaryKey != null) { item.set(m, Integer.parseInt(value)); } else { item.set(m, value); } } catch (NumberFormatException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } } } }
update()方法:
@Override public int update(M m) { ContentValues values = new ContentValues(); fillColumn(m, values); return db.update(getTableName(), values, getIdName(m) + "=?", new String[] { getIdValue(m) }); }
问题五:实体的对象创建
问题五也是最后一个问题,要解决的是getInstance()这个方法如何实现。这里也要涉及到反射,通过上面的研究,我们知道了getClass()获得的是正在运行的那个类的Class,如果是Book类,实际跑的泛型就是Book,然后通过Class可以拿到所有泛型类的集合,就可以得到我们的类,因为这我们的泛型只有一个。然后再通过反射,就可以得到对象了。那么如何通过Class对象拿到所有泛型集合呢?实际上jdk会让泛型实现一个接口(参数化的类型--这个接口),所有的泛型都会实现这个接口(ParameterizedType),规定了泛型的通用操作。而通过ParameterizedType就可以拿到所有被使用的泛型的集合。这里要注意的一点是,在拿所有泛型类的集合的时候,要使用带泛型的那个API:getGenericSuperclass(),否则拿不到泛型集合:
/** * 问题五:实体的对象创建 * * @return */ public M getInstance() { // 实体是何时确定的 // ①哪个孩子调用的该方法 Class clazz = getClass();// 获取到了正在运行时的那个类,这里就会拿到实际在跑的那个impl类 // System.out.println(clazz.toString()); Log.i(TAG, clazz.toString()); // ②获取该孩子的父类(是支持泛型的父类) // clazz.getSuperclass();// 这个方法不行,拿不到泛型 Type genericSuperclass = clazz.getGenericSuperclass();// 可以拿到泛型 // jdk会让泛型实现一个接口(参数化的类型--这个接口),所有的泛型都会实现这个接口(ParameterizedType),规定了泛型的通用操作 if (genericSuperclass != null && genericSuperclass instanceof ParameterizedType) { Type[] arguments = ((ParameterizedType) genericSuperclass) .getActualTypeArguments(); try { return ((Class<M>) arguments[0]).newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } // ③获取到泛型中的参数 return null; }
至此,我们就完成了数据库操作框架的抽取,如果我们再要写一个实体类,比如User的数据库操作,只要像Book类那样处理,加入对应的注释,即可使用啦!
最后附一张工程目录结构: