很多应用程序不是SQLite的重度使用者,因此非常幸运的不需要去担心太多处理数据库的性能问题。然而,如果需要去优化Android应用中SQLite相关的代码的情况下需要知道几个概念:
(1) SQLite状态
(2) 事务(Transaction)
(3) 查询
NOTE:本节不打算成为SQLite的完整指南,而是提供给你几个点去保证有效的使用数据库。完整的指南,参考www.sqlite.org和Android在线文档。
本节所涉及到的优化不会使得代码更加难以阅读和维护,所以需要养成使用它们的习惯。
开始的时候,SQL 语句是简单的字符串,比如:
(1) CRTEATE TABLE cheese (name TEXT, origin TEXT)
(2) INSERT INTO cheese VALUES(‘Roquefort’, ‘Roquefort-sur-Soulzon’)
第一条语句创建一个名字为cheese,包含两列名字分别为name和origin的表。第二行将在表中插入新的一行。因为它们是简单的字符串,语句在执行前需要被解释或者编译。当你调用比如SQLiteDatabase.execSQL的时候,这些语句的编译在内部执行,如Listing 1-18所示。
Listing 1-18 执行简单的SQLite语句
SQLiteDatabase db = SQLiteDatabase.create(null); // memory-backed database
db.execSQL("CREATE TABLE cheese (name TEXT, origin TEXT)");
db.execSQL("INSERT INTO cheese VALUES ('Roquefort', 'Roquefort-sur-Soulzon')");
db.close(); // 记得关闭数据库
NOTE:许多SQLite相关的方法会抛异常。
事实证明,执行SQLite语句需要相当一段时间。除了编译,语句本身可能需要被创建。因为String是不可变的,这可能导致和在computeRecursivelyFasterUsingBigInteger()使用大数字BigInteger同样的性能问题。我们现在聚焦于插入语句的性能问题,毕竟,一个表可能仅被创建一次,但是许多行可能会添加、修改或者删除。Listing 1-19 创建一个全面的Cheeses数据库
public class Cheeses {
private static final String[] sCheeseNames = {
"Abbaye de Belloc",
"Abbaye du Mont des Cats",
...
"Vieus Boulogne"
};
private static final String[] sCheeseOrigins = {
"Notre-Dame de Belloc",
"Mont des Cats",
...
"Boulogne-sur-Mer"
};
private final SQLiteDatabase db;
public Cheeses() {
db = SQLiteDatabase.create(null); // memory-backed database
db.execSQL("CREATE TABLE cheese (name TEXT, origin TEXT)");
}
public void populateWithStringPlus() {
int i = 0;
for(String name:sCheeseNames) {
String origin = sCheeseOrigins[i++];
String sql = "INSERT INTO cheese VALUES(\"" + name + "\",\"" + origin + "\")";
db.execSQL(sql);
}
}
}
Listing 1-20 创建SQL语句字符串更快的方式
public void populateWithStringFormat() {
int i=0;
for (String name : sCheeseNames) {
String origin = sCheeseOrigins[i++];
String sql = String.format("INSERT INTO cheese VALUES(\"%s\", \"%s\")", name, origin);
db.execSQL(sql);
}
}
public void populateWithStringBuilder() {
StringBuilder builder = new StringBuilder();
builder.append("INSERT INTO cheese VALUES(\"");
int resetLength = builder.length();
int i=0;
for(String name : sCheeseNames) {
String origin = sCheeseOrigins[i++];
builder.setLength(resetLength); // 重设位置
builder.append(name).append("\", \"").append(origin).append("\")"); // chain calls
db.execSQL(builder.toString());
}
}
Listing 1-21 编译SQLite语句
public void populateWithCompileStatement() {
SQLiteStatement stmt = db.compileStatement("INSERT INTO cheese VALUES(?,?)");
int i=0;
for (String name : sCheeseNames) {
String origin = sCheeseOrigins[i++];
stmt.clearBindings();
stmt.bindString(1, name); // 用name替换第一个问号
stmt.bindString(2, origin); // 用origin替换第二个问号
stmt.executeInsert();
}
}
Listing 1-22 使用ContentValues填充数据库
public void populateWithContentValues() {
ContentValues values = new ContentValues();
int i=0;
for(String name : sCheeseNames) {
String origin = sCheeseOrigins[i++];
values.clear();
values.put("name", name);
values.put("origin", origin);
db.insert("cheese", null, values);
}
}
NOTE: Android 3.0中android.database和android.database.sqlite做了许多变化。比如,Activity类中的managerdQuery, startManagingCursor和stopManagingCursor方法被弃用,用CursorLoader代替。
Android同样定义了几个可以提高性能的类。比如,你可以使用DatabaseUtils.InsertHelper在数据库中插入多行,这样编译SQL的插入语句仅一次。现在它的实现和我们实现populateWithCompileStatement()的方式一致,尽管它并不提供同样的灵活性(比如, FAIL或者ROLLBACK)。
即使性能不是必须的,你也可以使用DatabaseUtils类中的静态方法去简化你的实现。
上面的实例没有显式的创建任何事务,然而每次插入操作会自动创建一个,并且在插入完成后立即提交。显式的创建一个事务允许两件事情:
1) 原子提交(Atomic Commit)
2) 更好的性能
第一个Feature很重要但是不是从性能角度看的。原子提交(Atomic commit)意味着全部或者没有对数据库的修改发生。一个事务不可以仅提交一部分。在我们的例子中,我们可以认为所有的650个cheese插入是一个事务。我们成功的创建了cheese列表或者没有,但是我们对一部分成功没有兴趣。实现如Listing 1-23所示:
Listing 1-23 在一个事务中插入所有的Cheese
public void populateWithComileStatementOneTransaction () {
try {
db.beginTransaction();
SQLiteStatement stmt = db.compileStatement("INSERT INTO cheese VALUES(?,?)");
int i=0;
for(String name : sCheeseNames) {
String origin = sCheeseOrigins[i++];
stmt.clearBindings();
stmt.bindString(1, name); // 使用name替换第一个问号
stmt.bindString(2, origin); // 使用origin替换第二个问号
stmt.executeInsert();
}
db.setTransactionSuccessful(); // 移除这个调用,不会有任何改变提交
} catch (Exception e) {
// 在这里处理异常
} finally {
db.endTransaction(); // 这个调用必须在finally块中
}
}
Listing 1-24 在存储器上创建数据库
public Cheeses (String path) {
// path可能已经使用getDatabasePath("fromage.db")创建好了
// 可能需要用一个mkdirs保证path存在
// File file = new File(path);
// File parent = new File(file.getParent());
// parent.mkdirs();
db = SQLiteDatabase.openOrCreateDatabase(path, null);
db.execSQL("CREATE TABLE cheese (name TEXT, origin TEXT)"):
}
NOTE:当在存储器上创建数据库的时候保证父路径存在。参考Context.getDatabasePath和File.mkdirs。为了方便,使用SQLiteOpenHelper代替手动创建数据库。
使查询更快的方式也限制了对数据库的访问,特别是在存储器上。一个数据库的查询简单的返回一个Cursor对象,可以用来迭代所有的结果。Listing 1-25给出了迭代所有行的两个方法。第一个方法创建了一个Cursor获取数据库的所有的两列,第二个方法仅获取第一列。
Listing 1-25 迭代所有的行
public void iterateBothColumns() {
Cursor c = db.query("cheese", null, null, null, null, null, null);
if (c.moveToFirst()) {
do {
} while (c.moveToNext());
}
c.close(); // 记得完成的时候关闭cursor(否则会在某点发生Exception)
}
public void iterateFirstColumn() {
Cursor c = db.query("cheese", new String[] {"name"}, null, null, null, null, null); // 唯一的不同
if(c.moveToFirst()) {
do {
} while (c.moveToNext());
}
c.close();
}
TIP:更加深入的查找功能(使用indexing)考虑使用SQLite的FTS(full-text search)扩展(使用索引)。参考:www.sqlite.org/fts3.html