9.5. 重构数据库访问

优质
小牛编辑
142浏览
2023-12-01

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();

}

}

}

  1. StatusData 中的多数代码都是来自原先的 DbHelper.java 。现在 DbHelper 已经成为了 StatusData 的内部类。 仅供 StatusData 内部使用。 这意味着,在 StatusData 之外的调用者不必知道 StatusData 中的数据是以什么方式存储在什么地方。这样有助于使得系统更加灵活,而这也正是后面引入的ContentProvider的基本思想。
  2. 声明一个不可变的类成员dbHelper。final关键字用于保证某类成员只会被赋值一次,
  3. 在构造函数中初始化DbHelper的实例。
  4. 将dbHelper的close()方法暴露出去,允许他人关闭数据库。
  5. 我们对DbHelper中db.insert...()方法的改进。
  6. 仅在必要时打开数据库,也就是在写入之前。
  7. 在这里,我们调用insertWithOnConflict(),并将SQLiteDatabase.CONFLICT_IGNORE作为最后一个参数,表示如果有数据重复,则忽略异常。原因在前文的“数据库约束”一节已有说明。
  8. 不要忘记关闭数据库。我们把它放在finally子句中,这就可以保证无论出现何种错误,数据库都可以正确地关闭。同样的风格还可以在getLatestStatusCreatedAtTime()和getStatusTextById()中见到。
  9. 返回数据库中的所有消息数据,按时间排序。
  10. getLatestStatusCreatedAtTime()返回表中最新一条消息数据的时间戳(timestamp)。通过它,我们可以得知当前已存储的最新消息的日期,在插入新消息时可作为过滤的条件。
  11. 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;

}

}

...

  1. 在YambaApplication中添加私有成员StatusData。
  2. 其它部分若要访问它,只有通过这个方法。
  3. 这里的代码几乎都是来自原先的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

...

  1. 从Service对象中可以获取YambaApplication的实例。
  2. 调用刚才新加入的fetchStatusUpdates()方法,功能与原先的run()基本一致。
  3. fetchStatusUpdates()以获取的消息数量作为返回值。暂时我们只利用这个值来调试,不过后面将有其它作用。