Android數據庫-從SQLite到ORMLite封裝
前言
幾乎每一個android項目中,都必不可少的會使用數據庫的操作。SqlBrite是對 Android 系統的 SQLiteOpenHelper 的封裝,對SQL操作引入了響應式語義 (Rx)(用來在 RxJava 中使用)。在那之后確實也使用過一段時間的SqlBrite,不過可能是本人能力原因 ,在我的業務開發中,SqlBrite使用起來也并沒有多么的方便 ,反而對整體的封裝起到了一定的阻礙。所以后來也就繼續回歸使用 ORMLite 做數據庫操作。下面文章還是從基礎到封裝再到實例講講我的項目中的 ORMLite 是怎么使用的吧。
ORMLite的引入
1 從SQLite到ORM
SQLite是在世界上使用的最多的數據庫引擎,并且還是開源的。它實現了無配置,無服務要求的事務數據庫引擎。SQLite可以在Mac OS-X, iOS, Android, Linux, 和 Windows上使用。android中使用的正是SQLite。在Android開發中,使用SQLite作為基礎部分,想必大家對繼承 SQLiteOpenHelper 創建數據庫,調用 SQLiteDatabase 的 execSQL() 方法執行 INSERT, UPDATE, DELETE 等語句來更新表的數據,不管你如何執行查詢都會返回一個Android 的 SQLite 數據庫游標......這一系列概念并不陌生啊 。想必很多人都和我一樣,并不想寫任何SQL語句,因為一不小心就寫錯了,而且各種重復的SQL語句寫著真的心煩,大大的影響了開發的效率。
我們當然希望不需要再去和復雜的SQL語句打交道,在面向對象的編程中只要像平時操作對象一樣操作它就可以了。這就引入了ORM。
ORM是對象關系映射(Object Relational Mapping)的縮寫,對象和關系數據是業務實體的兩種表現形式,業務實體在內存中表現為對象,在數據庫中表現為關系數據,ORM實現了對象和關系數據庫之間的 轉換 。
2 java中ORM的原理
要實現JavaBean的屬性到數據庫表的字段的映射,任何ORM框架不外乎是讀某個配置文件把JavaBean的屬性和數據庫表的字段自動關聯起來,當從數據庫Query時,自動把字段的值塞進JavaBean的對應屬性里,當做INSERT或UPDATE時,自動把 JavaBean的屬性值綁定到SQL語句中。
3 從ORM到ORMLite
ORM框架廣泛引用于各種語言中,對于java開發者比較熟悉的有 Hibernate , Ormlite 等, Ormlite 作為一個Java ORM。支持JDBC連接,Spring以及Android平臺。除此之外Android中使用的ORM框架還有 Greendao , ActiveAndroid , SugarORM , Realm 等。后續項目會考慮使用 Realm ,到時候再進行講解。
ORMLite基礎
ORMLite provides a lightweight Object Relational Mapping between Java classes and SQL databases. There are certainly more mature ORMs which provide this functionality including Hibernate and iBatis. However, the author wanted a simple yet powerful wrapper around the JDBC functions, and Hibernate and iBatis are significantly more complicated with many dependencies.
ORMLite 提供了一個輕量級的java對象和數據庫的對象關系操作,相比于Hibernate 和iBatis 等成熟的ORM框架的繁重,ORMLite旨在提供一個簡單而有效的解決方案。
當然和之前的所有文章一樣, 基礎部分都回歸 官方文檔 。這里會對android使用中的重點的基礎部分進行提及并對官網的例子做出改動。
首先看看封裝之前的ORMLite在我項目中的使用步驟:
1 下載ORMLite的jar包
首先去 http://ormlite.com/releases/ 下載jar包,對于Android目前版本為:ormlite-android-5.0.jar 和 ormlite-core-5.0.jar ;在我項目中添加的是之前的4.49的jar包。
2 創建實體類,這里利用新聞信息實體NewsItem類
@DatabaseTable(tableName = "tb_news_item")
public class NewsItem implements Serializable{
@DatabaseField(generatedId = true, columnName = "i_id")
private int i_id;
@DatabaseField(columnName = "channelId")
@SerializedName(value = "channelId")
private int channelId;
@DatabaseField(columnName = "id")
@SerializedName(value = "id", alternate = {"docid", "docId"})
private int id;
@DatabaseField(columnName = "title")
@SerializedName(value = "MetaDataTitle", alternate = {"title", "name"})
private String title;
@DatabaseField(columnName = "content")
@SerializedName(value = "content")
private String content;
@DatabaseField(columnName = "type")
@SerializedName(value = "type", alternate = {"t", "docType"})
private int type;
@DatabaseField(columnName = "img", dataType = DataType.SERIALIZABLE)
@SerializedName(value = "image", alternate = {"ic", "images", "picture", "pic", "img"})
private ArrayList<String> images;
private String icon;
@DatabaseField(columnName = "url")
@SerializedName(value = "url", alternate = {"link", "docURL","channelUrl"})
private String url;
@DatabaseField(columnName = "date")
@SerializedName(value = "date", alternate = {"PubDate", "time"})
private String date;
@DatabaseField(columnName = "source")
@SerializedName(value = "source")
private String source;
@DatabaseField(columnName = "media")
@SerializedName(value = "media")
private String media;
@DatabaseField(columnName = "relPhotos")
@SerializedName(value = "RelPhotos")
private String relPhotos;
@DatabaseField(columnName = "isTopic")
private boolean isTopic = false;
@DatabaseField(columnName = "isStar")
private boolean isStar = false;
@DatabaseField(columnName = "channelItems", dataType = DataType.SERIALIZABLE)
@SerializedName(value = "channelItems")
private ArrayList<NewsItem> newsItems;
@DatabaseField(columnName = "parentChannelType")
private int parentChannelType;
//...... get set 方法
}</code></pre>
除了通過 @SerializedName(value = "xxx") 支持Gson序列化。
這里有幾個需要注意的地方:
- 1 通過 @DatabaseTable(tableName = "tb_news_item") 指定了表名為 tb_news_item .
- 2 通過 @DatabaseField(generatedId = true, columnName = "i_id") 指定id字段為自動生成,并且名為i_id。
- 3 對于ArrayList<String> 序列化對象的的支持,需要使用 dataType = DataType.SERIALIZABLE .可以通過DataType控制數據庫表中字段類型。

3 繼承OrmLiteSqliteOpenHelper類
我們需要通過繼承OrmLiteSqliteOpenHelper類來編寫自己的數據庫幫助類,
需要實現的方法為 onCreate(SQLiteDatabase sqliteDatabase, ConnectionSource connectionSource) 以及 onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion) 。分別對應著數據庫第一次創建 以及版本更新的時候調用的方法。
比如針對上面的 NewsItem 類,
/**
- Database helper class used to manage the creation and upgrading of your database. This class also usually provides
the DAOs used by the other classes.
*/
public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
private Context mContext;
// the DAO object we use to access the NewsItem table
private Dao<NewsItem, Integer> simpleDao = null;
// name of the database file for your application -- change to something appropriate for your app
private static final String DATABASE_NAME = "dbtest.db";
// any time you make changes to your database objects, you may have to increase the database version
private static final int DATABASE_VERSION = 1;
/**
*
* Returns the Database Access Object (DAO) for our NewsItem class. It will create it or just give the cached
* value.
*/
public Dao<NewsItem, Integer> getNewsItemDao() throws SQLException {
if (simpleDao == null) {
simpleDao = getDao(NewsItem.class);
}
return simpleDao;
}
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
try {
TableUtils.createTable(connectionSource, NewsItem.class);
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource,
int oldVersion, int newVersion) {
try {
TableUtils.dropTable(connectionSource, NewsItem.class, true);
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 釋放資源
*/
@Override
public void close() {
super.close();
// instance = null;
mContext = null;
}
}</code></pre>
可以看到的是 NewsItem 就是我們的實體類,通過 TableUtils.createTable(connectionSource, NewsItem.class); 在 onCreate 方法中完成了對象表的創建。

繼承OrmLiteSqliteOpenHelper類,其實是間接繼承了SQLiteOpenHelper
4 提取DAO類并封裝其中的方法

可以通過上面的這張圖看到的是 當前 DatabaseHelper 類除了完成 onCreate 中創建以及 onUpgrade 中更新以外,也提供了 DAO 類 。
java的設計模式中這會經常遇到,我們需要將數據庫的操作獨立到數據庫連接類(也就是Data Access Objects 簡稱DAO )。也就是說每個DAO類提供增刪查改等操作。每個實體對象比如說上面的NewsItem.class 都對應著一個DAO類。OrmLiteSqliteOpenHelper 類正為我們提供了getDao方法,也就有了上面圖片中的操作。
public class NewsItemDaoOld {
private MyApplication myApplication;
public NewsItemDaoOld(MyApplication myApplication) {
this.myApplication = myApplication;
}
public void add(NewsItem t) {
try {
myApplication.dbHelper.getNewsItemDao().create(t);
} catch (SQLException e) {
e.printStackTrace();
}
}
public void delete(NewsItem t) {
try {
myApplication.dbHelper.getNewsItemDao().delete(t);
} catch (SQLException e) {
e.printStackTrace();
}
}
public void update(NewsItem t) {
try {
myApplication.dbHelper.getNewsItemDao().update(t);
} catch (SQLException e) {
e.printStackTrace();
}
}
public List<NewsItem> all() {
try {
return myApplication.dbHelper.getNewsItemDao().queryForAll();
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
public List<NewsItem> queryByColumn(String columnName, Object columnValue) {
try {
QueryBuilder builder = myApplication.dbHelper.getNewsItemDao().queryBuilder();
builder.where().eq(columnName, columnValue);
return builder.query();
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
}</code></pre>
為了區分后面封裝后的 NewsItemDao.class ,我這里命名為 NewsItemDaoOld .class 。
這里可以看到在構造方法中得到了通過Application的繼承類 MyApplication 。然后獲得 DatabaseHelper 的實例,也就能夠通過 DatabaseHelper 中的 getNewsItemDao() 方法得到其中的NewsItemDaoOld()的實例。這里也可以發現ORMLite中各種方法的便利,增刪改查都在 NewsItemDaoOld .class 中完成。
5 利用DAO類完成增刪改查

到這里就可以在任何類中使用 NewsItemDaoOld .class 進行數據庫的操作了。你同時也會發現操作數據庫的代碼變得異常簡潔。導出數據庫數據,通過SQLiteExpert查看數據,可以看到,新聞數據成功添加。

ORMLite還提供了一些基類ORMLiteBaseActivity,ORMLiteBaseService之類的,便于數據庫操作的,這里不做考慮,畢竟項目中很大可能自己也需要繼承自己的BaseActvity之類的。
ORMLite封裝
封裝之前我們先來總結需求和問題
上面的代碼總的來說,封裝到我的代碼和業務邏輯中,有幾個需要改進的地方。
1 對于NewsItemDao我們需要在DatabaseHelper中獲取,在自己封裝的NewsItemDaoOld 中進行數據操作。那么當一個app的表多了之后,我希望提供統一的增刪改查操作,也就需要一個BaseDao完成一些基本的操作。以后的類統一命名為xxDao,并且繼承自BaseDao。并且將DatabaseHelper中的getDAO獲取各種數據的DAO的操作移到BaseDao中。
2 對表進行統一的管理。
3 將DataBaseHelper添加到DataManager中,按照以前的思路,將DataManager作為唯一的數據入口。

4 結合Dagger2進行全局的DataBaseHelper對象的管理
對于數據庫操作需要有一個關注點,就是我們需要確保整個app中不同頁面的數據庫鏈接和操作應當都是一個 ,也就是說,不能讓不同的線程同時操作數據庫,這樣肯定會存在數據庫的紊亂和異常。對于官網提供的例子,建議通過繼承 OrmLiteBaseActivity 作為Activity的基類來使用 OpenHelperManager (將會在第一次鏈接數據庫的時候創建,每次操作數據庫的時候使用,在釋放的時候進行關閉)。然后通過 getHelper() 類來獲取 OpenHelperManager 進行操作。

ORMLite提供的基類
當然也可以在自己的BaseActivity中封裝下面的操作。
private DatabaseHelper databaseHelper = null;
@Override
protected void onDestroy() {
super.onDestroy();
if (databaseHelper != null) {
OpenHelperManager.releaseHelper();
databaseHelper = null;
}
}
private DBHelper getHelper() {
if (databaseHelper == null) {
databaseHelper =
OpenHelperManager.getHelper(this, DatabaseHelper.class);
}
return databaseHelper;
}</code></pre>
然后這里由于我引入了Dagger2提供的全局單例對象我也就采取了自己的做法。我們全局使用的是同一個DatabaseHelper對象,也就避免了上方的操作。
解決這四個問題,那么一起來看看我的思路吧:
public class BaseDao<T> {
protected Class<T> clazz;
protected Dao<T, Integer> daoOpe;
/**
* get dao class through {@link com.anthony.app.common.data.database.DatabaseHelper}
*
* @param mApplication using this to get instance of DatabaseHelper
*/
public BaseDao(MyApplication mApplication) {
Class clazz = getClass();
while (clazz != Object.class) {
Type t = clazz.getGenericSuperclass();
if (t instanceof ParameterizedType) {
Type[] args = ((ParameterizedType) t).getActualTypeArguments();
if (args[0] instanceof Class) {
this.clazz = (Class<T>) args[0];
break;
}
}
clazz = clazz.getSuperclass();
}
try {
if (mApplication.dbHelper == null) {
throw new RuntimeException("No DbHelper Found!");
}
daoOpe = mApplication.dbHelper.getDao(this.clazz);
} catch (SQLException e) {
e.printStackTrace();
}
}
public void add(T t) {
try {
daoOpe.create(t);
} catch (SQLException e) {
e.printStackTrace();
}
}
public void delete(T t) {
try {
daoOpe.delete(t);
} catch (SQLException e) {
e.printStackTrace();
}
}
public void update(T t) {
try {
daoOpe.update(t);
} catch (SQLException e) {
e.printStackTrace();
}
}
public List<T> all() {
try {
return daoOpe.queryForAll();
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
public List<T> queryByColumn(String columnName, Object columnValue) {
try {
QueryBuilder builder = daoOpe.queryBuilder();
builder.where().eq(columnName, columnValue);
return builder.query();
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
}</code></pre>
針對問題1
BaseDao 的構造方法中得到了MyApplication 實例,也就能夠得到DatabaseHelper對象。我們也能通過反射獲取DAO子類的泛型,從而能在當前的BaseDao中通過 daoOpe = mApplication.dbHelper.getDao(this.clazz); 獲取到ORMLite中的Dao對象。從而可以進行增刪改查。
public class NewsItemDao extends BaseDao<NewsItem> {
public NewsItemDao(MyApplication mApplication) {
super(mApplication);
}
public List<NewsItem> queryLatest(int channelId, long limit) {
try {
QueryBuilder builder = daoOpe.queryBuilder();
builder.limit(limit)
.where()
.eq("channel_id", channelId)
.and()
.eq("isTopic", false);
return builder.query();
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
public List<NewsItem> queryTopic(int channelId) {
try {
QueryBuilder builder = daoOpe.queryBuilder();
builder.where()
.eq("channel_id", channelId)
.and()
.eq("isTopic", true);
return builder.query();
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
}</code></pre>
這里NewsItem的Dao類NewsItemDao繼承了BaseDao,并且添加兩個查詢方法對數據庫對象進行操作。
針對問題2
這里為了統一管理,我將所有的表的類名寫到array.xml文件中,從而可以在DatabaseHelper中進行獲取和統一創建。


針對上面的問題3 和4 結合在一起進行講解
將DatabaseHelper封裝到DataManager中,讓DataManager作為數據的入口。

在ApplicationModule中提供幾個DAO類的實例。也就是在全局中都是用的是這些Dao類的實例。也就解決了問題4中提及的數據庫操作的問題。

同時在ApplicationComponent中進行實例的暴露。這樣我們可以在任何進行了注入的類中使用這三個實例了 。這三個實例的初始化已經在上面這張圖中進行了說明。

最后在Application的實現類中進行DatabaseHelper的實例對象的獲取,大功告成。

項目效果和源碼
這里結合新聞列表,提供了一個導出數據庫的操作,數據庫可以在
Android - data - com.anthony.app - cache 中找到對應的數據庫表。利用SQLiteExpert進行數據庫的查看。

這里可以看到我這里有三個表,但是目前并沒有對channel表和offline_res表進行添加操作。tb_news_item是我們之前在實體類中定義的表名。里面有對應的數據。

參考資料
來自:http://www.jianshu.com/p/776a01485d91