9.5. 重构数据库访问
9.5.重构数据库访问
前面我们重构了UpdaterService,使它能够访问数据库。但这对整个程序来讲仍不理想,因为程序的其它部分可能也需要访问数据库,比如TimelineActivity。因此好的做法是,将数据库的相关代码独立出来,供UpdaterService与TimelineActivity重用。
为实现代码的重用,我们将创建一个新类StatusData,用以处理数据库的相关操作。它将SQLite操作封装起来,并提供接口,供Yamba中的其它类调用。这一来Yamba中的构件若需要数据,那就访问StatusData即可,而不必纠结于数据库操作的细节。在第十二章中,我们还将基于这一设计,提供ContentProvider的实现。
例 9.3. StatusData.java
package com.marakana.yamba4;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class StatusData { //
private static final String TAG = StatusData.class.getSimpleName();
static final int VERSION = 1;
static final String DATABASE = "timeline.db";
static final String TABLE = "timeline";
public static final String C_ID = "_id";
public static final String C_CREATED_AT = "created_at";
public static final String C_TEXT = "txt";
public static final String C_USER = "user";
private static final String GET_ALL_ORDER_BY = C_CREATED_AT + " DESC";
private static final String[] MAX_CREATED_AT_COLUMNS = { "max("
+ StatusData.C_CREATED_AT + ")" };
private static final String[] DB_TEXT_COLUMNS = { C_TEXT };
// DbHelper implementations
class DbHelper extends SQLiteOpenHelper {
public DbHelper(Context context) {
super(context, DATABASE, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.i(TAG, "Creating database: " + DATABASE);
db.execSQL("create table " + TABLE + " (" + C_ID + " int primary key, "
+ C_CREATED_AT + " int, " + C_USER + " text, " + C_TEXT + " text)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("drop table " + TABLE);
this.onCreate(db);
}
}
private final DbHelper dbHelper; //
public StatusData(Context context) { //
this.dbHelper = new DbHelper(context);
Log.i(TAG, "Initialized data");
}
public void close() { //
this.dbHelper.close();
}
public void insertOrIgnore(ContentValues values) { //
Log.d(TAG, "insertOrIgnore on " + values);
SQLiteDatabase db = this.dbHelper.getWritableDatabase(); //
try {
db.insertWithOnConflict(TABLE, null, values,
SQLiteDatabase.CONFLICT_IGNORE); //
} finally {
db.close(); //
}
}
/**
*
* @return Cursor where the columns are _id, created_at, user, txt
*/
public Cursor getStatusUpdates() { //
SQLiteDatabase db = this.dbHelper.getReadableDatabase();
return db.query(TABLE, null, null, null, null, null, GET_ALL_ORDER_BY);
}
/**
*
* @return Timestamp of the latest status we ahve it the database
*/
public long getLatestStatusCreatedAtTime() { //
SQLiteDatabase db = this.dbHelper.getReadableDatabase();
try {
Cursor cursor = db.query(TABLE, MAX_CREATED_AT_COLUMNS, null, null, null,
null, null);
try {
return cursor.moveToNext() ? cursor.getLong(0) : Long.MIN_VALUE;
} finally {
cursor.close();
}
} finally {
db.close();
}
}
/**
*
* @param id of the status we are looking for
* @return Text of the status
*/
public String getStatusTextById(long id) { //
SQLiteDatabase db = this.dbHelper.getReadableDatabase();
try {
Cursor cursor = db.query(TABLE, DB_TEXT_COLUMNS, C_ID + "=" + id, null,
null, null, null);
try {
return cursor.moveToNext() ? cursor.getString(0) : null;
} finally {
cursor.close();
}
} finally {
db.close();
}
}
}
- StatusData 中的多数代码都是来自原先的 DbHelper.java 。现在 DbHelper 已经成为了 StatusData 的内部类。 仅供 StatusData 内部使用。 这意味着,在 StatusData 之外的调用者不必知道 StatusData 中的数据是以什么方式存储在什么地方。这样有助于使得系统更加灵活,而这也正是后面引入的ContentProvider的基本思想。
- 声明一个不可变的类成员dbHelper。final关键字用于保证某类成员只会被赋值一次,
- 在构造函数中初始化DbHelper的实例。
- 将dbHelper的close()方法暴露出去,允许他人关闭数据库。
- 我们对DbHelper中db.insert...()方法的改进。
- 仅在必要时打开数据库,也就是在写入之前。
- 在这里,我们调用insertWithOnConflict(),并将SQLiteDatabase.CONFLICT_IGNORE作为最后一个参数,表示如果有数据重复,则忽略异常。原因在前文的“数据库约束”一节已有说明。
- 不要忘记关闭数据库。我们把它放在finally子句中,这就可以保证无论出现何种错误,数据库都可以正确地关闭。同样的风格还可以在getLatestStatusCreatedAtTime()和getStatusTextById()中见到。
- 返回数据库中的所有消息数据,按时间排序。
- getLatestStatusCreatedAtTime()返回表中最新一条消息数据的时间戳(timestamp)。通过它,我们可以得知当前已存储的最新消息的日期,在插入新消息时可作为过滤的条件。
- getStatusTextById()返回某一条消息数据的文本内容。
到这里,数据库的相关操作已都独立到了StatusData中。接下来把它放到Application对象里面,这一来即可令其它构件(比如UpdaterService与TimelineActivity)与StatusData建立起has-a关系,方便访问。
例 9.4.YambaApplication.java
...
private StatusData statusData; //
...
public StatusData getStatusData() { //
return statusData;
}
// Connects to the online service and puts the latest statuses into DB.
// Returns the count of new statuses
public synchronized int fetchStatusUpdates() { //
Log.d(TAG, "Fetching status updates");
Twitter twitter = this.getTwitter();
if (twitter == null) {
Log.d(TAG, "Twitter connection info not initialized");
return 0;
}
try {
List<Status> statusUpdates = twitter.getFriendsTimeline();
long latestStatusCreatedAtTime = this.getStatusData()
.getLatestStatusCreatedAtTime();
int count = 0;
ContentValues values = new ContentValues();
for (Status status : statusUpdates) {
values.put(StatusData.C_ID, status.getId());
long createdAt = status.getCreatedAt().getTime();
values.put(StatusData.C_CREATED_AT, createdAt);
values.put(StatusData.C_TEXT, status.getText());
values.put(StatusData.C_USER, status.getUser().getName());
Log.d(TAG, "Got update with id " + status.getId() + ". Saving");
this.getStatusData().insertOrIgnore(values);
if (latestStatusCreatedAtTime < createdAt) {
count++;
}
}
Log.d(TAG, count > 0 ? "Got " + count + " status updates"
: "No new status updates");
return count;
} catch (RuntimeException e) {
Log.e(TAG, "Failed to fetch status updates", e);
return 0;
}
}
...
- 在YambaApplication中添加私有成员StatusData。
- 其它部分若要访问它,只有通过这个方法。
- 这里的代码几乎都是来自原先的UpdaterService。它将运行在独立的线程中,连接到服务端抓取数据,并保存到数据库里面。
接下来可以利用新的YambaApplication,重构原先的UpdaterService。现在run()中的代码已都移到了fetchStatusUpdates()中,而且UpdaterService也不再需要内部的StatusData对象了。
例 9.5. UpdaterService.java
...
private class Updater extends Thread {
public Updater() {
super("UpdaterService-Updater");
}
@Override
public void run() {
UpdaterService updaterService = UpdaterService.this;
while (updaterService.runFlag) {
Log.d(TAG, "Running background thread");
try {
YambaApplication yamba = (YambaApplication) updaterService
.getApplication(); //
int newUpdates = yamba.fetchStatusUpdates(); //
if (newUpdates > 0) { //
Log.d(TAG, "We have a new status");
}
Thread.sleep(DELAY);
} catch (InterruptedException e) {
updaterService.runFlag = false;
}
}
}
} // Updater
...
- 从Service对象中可以获取YambaApplication的实例。
- 调用刚才新加入的fetchStatusUpdates()方法,功能与原先的run()基本一致。
- fetchStatusUpdates()以获取的消息数量作为返回值。暂时我们只利用这个值来调试,不过后面将有其它作用。