第十章 数据库进阶
Hack 41 使用ORMLite构建数据库
ORMLite是一种对象关系映射(Object-Relational Mapping,简称ORM)工具,也可用于读写数据。
应用程序中所有数据库操作都通过ORMLite完成,而不需要手动编写任何SQL语句。该方法可以通过减少创建数据库schema的代码数量来节省时间。
41.1 开始
使用ORMLite时,需要用到其release版本中的两个JAR包:core和android。本例使用的是4.41版。获取依赖包后,就可以开始创建数据库schema了。
使用ORMLite的第一步是实现应用程序中需要操作的java实体类。在这过程中需要注意:在类中包含注解(Annotation)以允许ORMLite创建所需的数据库表。在有复杂数据库的情况下,当从对象中访问数据库时,这些注解还可以为ORMLite提供任何操作的信息。注意,使用注解的方法只是指定ORMLite所生成的数据库schema的多种方式中的一种。
使用ORMLite最常用的两种注解是DatabaseTable和DatabaseField。这些注解可以分别用于类及其成员变量,并运行生成最终的数据库表。使用注解的Article类的简单实现如下:
@DatabaseTable
public class Article{
@DatabaseField(generatedId = true)
public int id;
@DatabaseField
public String title,text;
@DatabaseField
public Date publishedDate;
public Article(){}//ORMLite需要用到无参构造方法
}
该类,只是全部实现代码的一部分,它会生成如下创建表的SQL语句:
CREATE TABLE 'article'
('title' VARCHAR,'publishedDate' VARCHAR,'text' VARCHAR,'id' INTEGER PRIMARY KEY AUTOINCREMENT);
注意id字段对应的注解,对于该注解我们指定了generatedId = true参数,该参数表示该字段是主键(primary key),并且由SQLite数据库自动指定。另外还要注意,默认情况下,ORMLite使用类名作为SQL表名,使用成员变量名作为表的列名。
最后,我们可以注意到ORMLite需要类中提供一个无参构造函数。当ORMLite需要创建Article类的实例时,比如在查询并返回文章列表的情况下,ORMLite会使用无参构造函数,并通过反射机制设置成员变量(ORMLite也可以使用setter方法设置成员变量)。
41.2 坚如磐石的数据库schema
在这里我们会演示下述内容:
1.自定义数据库表和表列的名字
2.处理类之间的关系
3.关系的参照完整性(API Level 8 及以上)
4.级联删除(API Level 8 及以上)
5.交叉引用的唯一性约束
当定义schema时,第一个建议是使用final变量定义数据库表名和列名。因为在实践中,当成员变量被重构或移除时,该方法可以简化代码的维护工作。
下面我们来定义Category类,代码如下:
//指定表名
@DatabaseTable(tableName = Category.TABLE_NAME)
public class Category{
public static final String TABLE_NAME = "categories",ID_COLUMN = "_id",NAME_COLUMN = "name",PARENT_COLUMN = "parent";
//在DatabaseField中指定列名
@DatabaseField(generatedId = true,columnName = ID_COLUMN)
private int id;
//name成员不能为空
@DatabaseField(canBeNull = false,columnName = NAME_COLUMN)
private String name;
//标记为外键
@DatabaseField(foreign = true,columnName = PARENT_COLUMN)
private Category parent;
public Category(){}
}
我们在DatabaseTable注解里指定了数据库表名,在DatabaseField注解里指定了表的列名。我们可以在应用程序中的任何地方使用这些public变量用于查询。
parent的成员变量声明如下:
@DatabaseField(foreign = true,foreignAutoRefresh = true,columnName = PARENT_COLUMN,columnDefinition = "integer references"+TABLE_NAME+"("+ID_COLUMN+") on delete cascade")
private Category parent;
我们可以使用columnDefinition定义列来微调SQL语句。这里我们指定parent列有一个外键指向categories表(外键的值定义在该表中)。这表明parent列的值要么是空,要么存在于categories表的_id列中。我们还指定当parent指向的分类被删除时,parent的记录也会被删除,这就是级联删除。级联删除并不是数据库要求的,但为了演示,我们在代码中包含这部分内容。为Category类创建数据库表的SQL语句如下:
CREATE TABLE 'categories' ('parent' integer references categories(_id) on delete cascade,'name' VARCHAR NOT NULL,'_id' INTEGER PRIMARY KEY AUTOINCREMENT)
ArticleCategory类用于将articles表映射到categories表,我们会为其两个成员变量设置uniqueCombo = true,代码如下:
@DatabaseTable(tableName = ArticleCategory.TABLE_NAME)
public class ArticleCategory{
//final变量用于表示表名和列名
public static final String TABLE_NAME = "articlecategories",ARTICLE_ID_COLUMN = "article_id",CATEGORY_ID_COLUMN = "category_id";
//使用columnDefinition元素
@DatabaseField(foreign = true,canBeNull = false,uniqueCombo = true,columnName = ARTICLE_ID_COLUMN,columnDefinition = "integer references"+Article.TABLE_NAME+"("+Article.ID_COLUMN+") on delete cascade")
private Article article;
//设置foreign = true用于存储复杂对象
@DatabaseField(foreign = true,canBeNull = false,uniqueCombo = true,columnName = CATEGORY_ID_COLUMN,columnDefinition="integer references"+Category.TABLE_NAME+"("+Category.ID_COLUMN+") on delete cascade")
private Category category;
public ArticleCategory(){}
}
最终创建表的SQL语句如下所示:
CREATE TABLE 'articlecategories'('article_id' integer references articles(_id) on delete cascade,'category_id' integer references categories(_id) on delete cascade, UNIQUE('article_id','category_id'));
41.3 SQLiteOpenHelper——数据库通道
SQLiteOpenHelper是Android提供的抽象类,用于管理开发者与存储于设备上的数据库文件之间的交互。开发者负责创建SQLiteOpenHelper的子类,并实现两个方法:onCreate()和onUpgrade()。onCreate()用于开发者指定准确的数据库schema,onUpgrade()用于后续版本中需要改变数据库schema的情况。
使用ORMLite时,不需要继承SQLiteOpenHelper,取而代之的是通过继承OrmLiteSqliteOpenHelper来利用ORM工具的优势。即便如此,仍需要实现onCreate()和onUpgrade()方法。我们会用到TableUtils类的静态方法创建所有需要的表。在底层,ORMLite会通过Java反射机制相关的API读取注解并构建之前我们所看到的创建数据库表的SQL语句。
onCreate()方法的实现代码如下:
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase,ConnectionSource connectionSource){
try{
TableUtils.createTable(connectionSource,Category.class);
TableUtils.createTable(connectionSource,Article.class);
TableUtils.createTable(connectionSource,ArticleCategory.class);
TableUtils.createTable(connectionSource,Author.class);
TableUtils.createTable(connectionSource,ArticleAuthor.class);
TableUtils.createTable(connectionSource,Comment.class);
}catch(SQLException e){
Log.e(TAG,"Unable to create tables.",e);
throw new RuntimeException(e);
}
}
注意,使用外键时,上述语句有严格的顺序。既然ArticleCategory对应的表引用了Article和Category对应的表,因此ArticleCategory对应的表必须在其依赖的表创建之后在创建。
在运行时,当ORMLite第一次用于操作数据库时,onCreate()方法就会被调用。此时,查看logcat的输出,会发现这个创建过程中使用的完整SQL语句,举例如下:
INFO/TableUtils(2133):executed create table statement changed 1 rows:CREATE TABLE 'categories'('parent' integer references categories(_id) on delete cascade, 'name' VARCHAR NOT NULL,'_id' INTEGER PRIMARY KEY AUTOINCREMENT)
对于onUpgrade()方法的实现,每个应用程序的每个升级流程都可能不同。在最简单的实现中,首先通过TableUtils.dropTable()方法丢弃每个表,然后调用onCreate() 方法。
支持外键的功能在默认情况下并没有开启,要支持外键需要执行一条SQL语句,可以通过重写onOpen()方法,在打开数据库时执行这条SQL语句,代码如下:
@Override
public void onOpen(SQLiteDatabase db){
super.onOpen(db);
db.execSQL("PRAGMA foreign_keys=ON;");
}
41.4 用于数据库访问的单例模式
LiteSqliteOpenHelper的子类我们以单例的形式使用,实现代码如下(为了简单书写,非单例部分代码省略了):
public class DatabaseHelper extends OrmLiteSqliteOpenHelper{
public static final String DATABASE_NAME = "demo.db";
private static final int DATABASE_VERSION = 1;
private static DatabaseHelper instance;
public static synchronized DatabaseHelper getInstance(Context c){
if(instance == null){
instance = new DatabaseHelper(c);
}
return instance;
}
private DatabaseHelper(Context context){
super(context,DATABASE_NAME,null,DATABASE_VERSION);
}
}
在private构造方法中,指定了数据库文件名及版本号。传入构造方法的版本号与之前提到的onUpgrade()方法一起使用。
41.5 CRUD操作一点通
在讨论数据库的时候我们经常会提到CRUD(crate,read,update,delete;创建,读取,更新,删除)这个缩写词。
我们通过ORMLite提供的一个称为DAO(data access object,数据访问对象)的类来访问数据库中的对象。DAO是一个泛型类,有两个泛型参数:需要持久化的对象的类型、对象ID的类型。对于没有ID的交叉引用对象而言,例如ArticleCategory,我们使用Void作为泛型参数。在DatabaseHelper单例中,我们以指定类为参数调用getDao()方法,就可以得到每个类对应的DAO对象。从方便的角度看,上述方法可以根据实际的泛型参数来转化返回结果,正如下述示例代码所示。
public class DatabaseHelper extends OrmLiteSqliteOpenHelper{
/* Remainder omitted */
public Dao<> getArticleDao(Article,Integer) throws SQLException{
return getDao(Article.class);
}
}
得到了Dao对象,就可以通过其暴露的大量方法创建、更新、删除、和查询数据库对象。假如要在数据库中创建一条Category记录,只需要创建一个Category实例,用需要持久化的信息填充该实例,然后调用DAO提供的create()方法即可,ORMLite会将数据库指定的ID值设置给相应对象。
假设我们希望创建两个Category,其中一个Category需要嵌套在另一个Category中,可以通过以下代码实现这种需求:
//创建所需对象
Category tutorials = new Category();
tutorials.setName("Tutorials");
//获取DatabaseHelper单例类的实例
DatabaseHelper helper = DatabaseHelper.getInstance(context);
Dao<Category,Integer> categoryDao = helper.getCategoryDao();
//调用create方法
categoryDao.create(tutorials);
Category programmingTutorials;
String title = "Programming Tutorials";
//Tutorials对象设置了自己的ID,可以在新的Category中用作父Category
programmingTutorials = new Category(title,tutorials);
categoryDao.create(programmingTutorials);
读取一个指定ID的对象,只需要简单地调用DAO提供的queryForId()方法。DAO还提供了更新和删除一个对象的方法,使用起来同样简单。传入一个已经设置过ID的对象,上述操作可以很简单的实现。
假设已经知道在上段代码中创建的第一个数据记录的ID,可以通过以下代码重命名该纪录:
Category renamed = new Category(1,"Android Tutorials",null);
categoryDao.update(renamed);
//也可以删除该记录
Category toDelete = new Category();
toDelete.setId(2);
categoryDao.delete(toDelete);
执行更新操作时,重要的一点是填充源对象的所有相关成员变量。执行删除操作时,仅仅需要传入ID值。
41.6 查询构建器
首先,写一个查询,用于返回数据库中所有顶层Category。我们仍然使用之前实现的那个泛型参数为Dao
PreparedQuery<Category> query = categoryDao.queryBuilder().selectColumns(Category.NAME_COLUMN).where().isNull(Category.PARENT_COLUMN).prepare();
List<Category> topLevelNames = categoryDao.query(query);
QueryBuilder中定义的方法可以使用特有的SQL运算符生成一条查询语句。开发者可以组合使用and()、or(),表示相等的eq()、not(),表示大于或等于的ge()以及其它方法来生成where子句。
QueryBuilder以及与其类似用于更新和删除的相关类都采用流畅接口(fluent interface),流畅接口意味着每个方法都返回相同对象的引用,因此开发者通常使用链式调用的方式一起调用这些方法以提高可读性。
在上例中,我们调用selectColumns()方法并为该方法指定数据库中哪些列需要填充到最终返回到对象中(只有name列),这样便对数据库做了一次投影(projection,投影是数据库理论的核心概念之一,用于从数据库表中查询或显示一部分列)。
41.7 数据类型和棘手的外部类型
到目前为止,我们可以令ORMLite处理Java类型与SQLite存储类型之间的映射。
我们在创建数据库schema时使用过注解,所以我们可以使用相同的注解来调整ORMLite的行为。
我们可以完成的最简单的更改是改变成员变量的存储类型,例如日期类型。默认情况下,ORMLite会把java.util.Date映射为VARCHAR,并且以yyyy-MM-dd HH:mm:ss.SSSSSS 的格式存储日前。
如,想以数字的形式存储日期(从纪元开始至今经历的毫秒数),我们可以在Article类中使用下述修改后的注解:
@DatabaseField(canBeNull = false,dataType = DataType.DATE_LONG,columnName = PUBLISHED_DATE_COLUMN)
private Date publishedDate;
上述代码会生成一条创建数据库表带语句,并使用BIGINT类型存储日期。
现在,我们考虑有外部引用对象的情况。已知Category可以有父分类,但当我们检索一个有父分类的Category时,ORM应该怎么处理呢?应该返回父分类的全部内容吗?那么父分类的父分类呢?
ORMLite引入了foreign auto refresh来处理上述行为,并且提供了foreign refresh level实现可配置化。在默认情况下,查询一个Category时,会设置其父分类,不过只会设置父分类的ID信息。从ORM执行SQL查询的角度看,默认行为是最有效率的。当开启自动刷新功能时,开发者应该意识到可能存在大量语句同时执行的情况。
下面介绍一对一关系的具体案例。假如我们希望Category的父分类总是可以被刷新,我们可以在父分类对应的成员变量的注解中设置foreignAutoRefresh = true,代码如下:
@DatabaseField(foreign = true,foreignAutoRefresh = true,canBeNull = true,columnName = PARENT_COLUMN,columnDefinition = "integer references"+TABLE_NAME+"("+ID_COLUMN+") on delete cascade")
private Category parent;
开启上述功能时,ORMLite默认会执行2级刷新。对于上述代码定义的注解,ORMLite会填充Category、Category的父分类以及父分类的父分类(如果有的话)。可以在注解中使用maxForeignAutoRefreshLevel元素来改变默认的2级刷新。总之,将上述元素的值修改为1是比较常见的情况(此外,增加该值会导致更多SQL查询语句被执行)。
下面实现一对多多示例,代码如下:
@DatabaseTable(tableName = Article.TABLE_NAME)
public class Article{
...
@ForeignCollectionField(eager = true)
private ForeignCollection<Comment> comments;
}
对于上述定义,ORMLite不会为Article类对应的数据库表添加任何额外的列。这里我们看看如何使用非eager的方式收集数据,这或许会很棘手。我们从注解中移除eager = true元素(默认为false):
@ForeignCollectionField
private ForeignCollection<Comment> comments;
在处理comments变量时,我们必须非常小心,因为其类型是ForeignCollection。当一个Collection是非eager时,触发Collection上的任何方法都会导致I/O操作,如size()和iterator()方法。此外,调试器可能正在调用iterator()方法,这样会导致不可预料的I/O操作并产生一个被填充列奇怪数据的Collection。ORMLite开发文档建议在Collection上使用toArray()方法来填充这种形式的Collection。
41.8 原生SQL查询
编写一条SQL查询语句往往比依赖于ORM工具构建和执行所需的查询更有效率。在性能要求严格的领域,一条SQL链接语句(join)的效率要比依赖于DAO方法自动更新对象或人工选择式更新对象的效率要高的多。
要执行原生SQL查询,首先需要获取一个DAO,然后调用queryRaw()方法的某个重载方法。每个queryRaw()方法的签名中都以一个可变数量的字符串作为最后一个参数。这是为了让开发者可以将查询语句参数化,并通过ORM来转义(escaping)其取值。这对于需要基于用户输入来执行某些查询操作的情况很重要,但如果处理不好,数据库就会暴露在SQL注入攻击(SQL injectionattack)之下。
queryRaw()方法的重载方法允许我们对得到的查询结果集的类型进行微调,可选的微调类型如下所示:
String数组的列表,每个结果对应一个数组,每个数组保存所选数据列的原始字符串。
Object数组的列表,每个结果对应一个数组,Object的具体类型由输入类型决定。
自定义类的列表,需要用到参数化的RawRowMapper。
41.9 事务
事务(Transaction)是数据库操作的重要组成部分。事务允许将多条数据库操作语句视为一个原子单位。一个事务需要保证以下两种可能情况中的一种会发生。
如果没有错误发生,所有数据库语句都会被执行并提交。
如果在事务的任何一个执行点发生错误,整个事务都需要被回滚(roll back)。
为了便于使用事务,ORMLite提供了一个名为TransactionManager的类,该类封装了开始事务、标记事务成功以及结束事务的细节。TransactionManager只暴露了一个callInTransaction()方法,该方法接收一个Callable参数,该参数类似于Runnable,只是多了一个返回值。
为了运行一个事务,我们在DatabaseHelper的子类OrmLiteSqliteOpenHelper中提供事务的特性:
public class DatabaseHelper extends OrmLiteSqliteOpenHelper{
public <T>T callInTransaction(Callable<T> callback){
try{
TransactionManager manager;
manager = new TransactionManager(getConnectionSource());
return manager.callInTransaction(callback);
}catch(SQLException e){
Log.e(TAG,"Exception occurred in transaction.",e);
throw new RuntimeException(e);
}
}
}
运行事务只需要将数据库操作放在Callable中。下面介绍一个示例方法,该方法在事务中执行了两次写操作,并返回Article,代码如下:
public Article createArticleInCategory(Context,context,final String title,final String text,final Category category){
final DatabaseHelper helper = DatabaseHelper.getInstance(context);
return helper.callInTransaction(new Callable<Article>(){
@Override
public Article call() throws SQLException{
//新建Article实例
Article article = new Article(new Date(),text,title);
Dao<Article,Integer> articleDao;
//使用DAO将其添加到数据库中
articleDao = helper.getArticleDao();
articleDao.create(article);
Dao<ArticleCategory,Void> articleCategoryDao;
//添加交叉引用实例
articleCategoryDao = helper.getArticleCategoryDao();
articleCategoryDao.create(new ArticleCategory(article,category));
return article
}
});
}
在一些情况下,事务可以提高多条组合语句的性能,特别是读写操作混用的情况。
ORMLite可以大大简化Android应用程序的数据库开发,只要正确注解了Java类,就可以通过ORMLite创建整个数据库实例。ORMLite还可以处理数据库查询结果与类实例之间的映射,这样就不需要编写大量样板代码了。
使用手工编写join语句,然后使用DAO提供的queryRaw()方法。这种方式会比逐一查询依赖的附加表要高效的多,此外,考虑使用事务来批量处理多个写操作以确保数据一致性。最后,建议为SQLiteOpenHelper的子类使用单例模式以避免多线程同时执行写操作时出现问题。
外链地址1
外链地址2
外链地址3
外链地址4——介绍如何在迭代器上正确调用close()方法
Hack 42 为SQLite添加自定义功能
Android使用SQLite作为内置数据库。尽管Android为其提供良好的API接口,但有时还是会感觉接口有限。
所以SQLite的一大限制就是缺乏数学函数,导致某些查询无法实现。
在这里我们会展示如何使用Android NDK为SQLite提供自定义查询功能。
我们创建创建一个应用程序,该应用程序使用自定义SQLite功能计算存储在数据库中的两个不同POI(points of interest,关注点)间的距离。该功能会用到POI的GPS坐标,并使用半正矢公式(haversine formula,半正矢函数是非常罕见三角函数的一种)以公里为单位返回距离信息。
42.1 Java代码
实现的思路是:使用Java API处理简单的数据库查询,只有在需要使用自定义功能时才使用NDK。Java部分比较关键的代码是DatabaseHelper类,该类负责在必要的时候调用NDK。
DatabaseHelper的实现代码如下:
public class DatabaseHelper extends SQLiteOpenHelper{
public static final String DATABASE_NAME = "pois.db";
private static final int DATABASE_VERSION = 1;
private Context mContext;
//加载native库
static{
System.loadLibrary("hack042-native");
}
public DatabaseHelper(Context context){
super(context,DATABASE_NAME,null,DATABASE_VERSION);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase db){
//POI数据库表的schema
db.execSQL("CREATE TABLE"+"pois("+"_id INTEGER PRIMARY KEY AUTOINCREMENT,"+"title TEXT,"+"longitude FLOAT,"+"latitude FLOAT);");
}
@Override
public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion){
db.execSQL("DROP TABLE IF EXISTS pois;");
}
//getNear()方法的java实现
public List<Poi> getNear(){
File file = mContext.getDatabasePath(DATABASE_NAME);
return getNear(file.getAbsolutePath(),latitude,longitude);
}
//getNear()方法的native实现方法签名
private native List<Poi> getNear(String dbPath,float latitude,float longitude);
}
第一行重要代码是加载native库。通常从static代码块中调用System.loadLibrary(),表示加载类的同时加载名为hack042-native的native库。在onCreate()方法中,可以看到数据库schema的形式。DatabaseHelper类定义了一个getNear()方法,该方法会在用户点击Search按钮的时候调用。不过,上述方法只是对其native版本方法的封装,Java版本的方法是public的,这是因为native方法的实现需要知道数据库路径,只有DatabaseHelper类知道该路径。
42.2 native代码
当使用自定义功能时,我们通过NDK查询数据库。要完成这个功能,需要在NDK中操作SQLite,这意味着需要编译代码。我们只需简单的添加.c和.h文件,讲sqlite3.c添加到Android.mk的LOCAL_SRC_FILES中就可以使用。
在main.cpp中实现了所有NDK代码,需要完成下列功能:
1.使用JNI创建Java对象
2.使用SQLite的C/C++API查询数据库
3.将List以jobject形式返回
getNear()方法实现代码如下:
//getNear()native方法
jobject Java_com_manning_androidhacks_hack042_db_DatabaseHelper_getNear(JNIEnv *env,jobject thiz,jstring dbPath,jfloat lat,jfloat lon){
sqlite3 *db;
sqlite3_stmt *stmt;
const char *path = env->GetStringUTFChars(dbPath,0);
jclass arrayClass = env->FindClass("java/util/ArrayList");
jmethodID mid_init = env->GetMethodID(arrayClass,"<init>","()V");
//创建ArrayList
jobject objArr = env->NewObject(arrayClass,mid_init);
jmethodID mid_add = env->GetMethodID(arrayClass,"add","(Ljava/lang/Object;)Z");
jclass poiClass = env->FindClass("com.manning.androidhacks.hack042.model.Poi");
jmethodID poi_mid_init = env->GetMethodID(poiClass,"<init>","(Ljava/lang/String;FFF)V");
//根据指定路径打开数据库
sqlite3_open(path,&db);
env -> ReleaseStringUTFChars(dbPath,path);
//创建自定义功能
sqlite3_create_function(db,"distance",4,SQLITE_UTF8,NULL,&distanceFunc,NULL,NULL);
if(sqlite3_prepare(db,"SELECT title,latitude,longitude,distance(latitude,longitude,?,?) as kms FROM pois ORDER BY kms",-1,&stmt,NULL)==SQLITE_OK){
int err;
sqlite3_bind_double(stmt,1,lat);
sqlite3_bind_double(stmt,2,lon);
//遍历返回结果
while((err = sqlite3_step(stmt))==SQLITE_ROW){
const char *name = (char const *)
sqlite3_column_text(stmt,0);
jfloat latitude = sqlite3_column_double(stmt,1);
jfloat longitude = sqlite3_column_double(stmt,2);
jfloat distance = sqlite3_column_double(stmt,3);
//新建POI对象
jobject poiObj = env->NewObject(poiClass,poi_mid_init,env->NewStringUTF(name),latitude,longitude,distance);
env->CallBooleanMethod(objArr,mid_add,poiObj);
}
if(err != SQLITE_DONE){
LOGI("Query execution failed:%s\n",sqlite3_errmsg(db));
}
sqlite3_finalize(stmt);
}else{}
}
首先要关注的是Java和NDK方法签名的不同。既然需要返回List,我们就使用JNI新建一个ArrayList。然后可以通过指定的路径打开数据库,并且通过传入一个函数指针创建自定义功能。distance()函数定义在main.cpp文件中。自定义功能创建后,就可以使用distance()函数编写查询语句。最后一步是遍历返回结果,使用原始数据创建Poi对象,并把该对象添加到列表中。
到这里每当调用DatabaseHelper的getNear()方法,该方法就会用到我们实现的自定义功能。
外链地址1
外链地址2
外链地址3
外链地址4
外链地址5
外链地址6
Hack 43 数据库批处理
Android中有一个功能是:可以将数据保存在数据库中,然后使用CursorAdapter将其显示在列表中。如果使用ContentProvider处理数据库操作,可以返回一个Cursor,当数据改变时,该Cursor会随之更新。这意味着如果一切正常,开发者可以专注于在后台线程中提供修改数据库表中的信息逻辑,UI就会自动更新。
存在的问题:如果执行了大量数据库操作,Cursor会被频繁更新,UI会出现闪烁(flicker)。
我们在这里提供三种可能的实现:
1.不适用批处理
2.使用批处理
3.使用批处理,并且使用SQLiteContentProvider类
演示示例:显示从1到100的列表。当用户点击刷新按钮后,老数字被删除,新数字被添加。为了实现上述功能,我们将为下面四种控件编码:
1.一个Activity,用于显示数字
2.一个适配器,用于创建并填充ListView中的视图
3.一个ContentProvider用于处理数据库查询
4.一个Service,用于通过ContentProvider更新数据库表
在这里我们只分析每种方案中不同的部分的代码,这些代码主要位于Service和ContentProvider中。
43.1 不使用批处理操作
Service的代码如下:
public class NoBatchService extends IntentService{
...
@Override
protected void onHandleIntent(Intent intent){
ContentResolver contentResolver = getContentResolver();
//插入新数字前,先删除之前的数字
contentResolver.delete(NoBatchNumbersContentProvider.CONTENT_URI,null,null);
if(int i = 1;i <= 100;i++){
//在for循环中,创建ContentView,并通过ContentResolver插入数字
ContentValues cv = new ContentValues();
cv.put(NoBatchNumbersContentProvider.COLUMN_TEXT,""+i);
contentResolver.insert(NoBatchNumbersContentProvider.CONTENT_URI,cv);
}
}
}
这里出现了闪烁情况,原因是:因为每次通过NoBatchNumbersContentProvider执行插入或删除操作时,都执行了以下操作:
getContext().getContentResolver().notifyChange(uri,null);
这意味着,通过NoBatchNumbersContentProvider的query()方法检索出的Cursor会被更新,适配器会令ListView自身也发生刷新。
43.2 使用批处理操作
这种方法我们在ContentProvider中定义了如下方法:
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations);
这种方法的思路是创建一个ContentProviderOperation的列表,然后一起处理列表项。Service的代码如下:
public class BatchService extends IntentService{
private static final String TAG=BatchService.class.getCanonicalName();
...
@Override
protected void onHandleIntent(Intent intent){
Builder builder = null;
ContentResolver contentResolver = getContentResolver();
//创建ContentProviderOperations的列表
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
//使用ContentProviderOperations的构建器创建删除操作,并将该操作添加到操作列表中
builder = ContentProviderOperation.newDelete(BatchNumbersContentProvider.CONTENT_URI);
operations.add(builder.build());
for(int i = 1;i <= 100;i++){
ContentValues cv = new ContentValues();
cv.put(NoBatchNumbersContentProvider.COLUMN_TEXT,""+i);
//为每个数字创建要给插入操作
builder = ContentProviderOperation.newInsert(BatchNumbersContentProvider.CONTENT_URI);
builder.withValues(cv);
operations.add(builder.build());
}
try{
//以操作列表为参数调用applyBatch()方法
contentResolver.applyBatch(BatchNumbersContentProvider.AUTHORITY,operations);
}catch (RemoteException e){
Log.e(TAG,"Couldn't apply batch:"+e.getMessage());
}catch (OperationApplicationException e){
Log.e(TAG,"Couldn't apply batch:"+e.getMessage());
}
}
}
这种方法还是出现了闪烁现象,为什么呢?
如果分析ContentProvider的实现代码,会发现applyBatch()方法并没有什么特别的地方,该方法只是一个循环遍历每一个操作,并调用apply()方法,该方法最终会调用BatchNumbersContentProvider类中的insert()/delete()方法。
applyBatch方法的官方文档:
“重写该方法以处理批量操作请求。否则,默认实现会循环遍历各个操作并为每个操作调用apply()方法。如果所有apply()方法调用都成功,就会返回一个ContentProviderResult类型的数组,该数组会保存各个操作所返回的数据元素。如果任意一次方法调用失败,需要根据不同的实现来决定其他方法调用是否有效。”
43.3 使用SQLiteContentProvider执行批处理操作
到了这里我们应该知道解决闪烁问题需要在ContentProvider的实现代码中队applyBatch()方法做一些修改。
在Android开源项目中(Android Open Source Project,简称AOSP)有一个名为SQLiteContentProvider的类,这个类不属于SDK,而是位于com.android.providers.calendar中。
所以在本例中我们不需要继承ContentProvider,而是继承SQLiteContentProvider。
Service的代码与第二种方法基本相同。接下来分析SQLiteContentProvider的applyBatch()方法:
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws OperationApplicationException{
mDb = mOperationHelper.getWritableDatabase();
//
mDb.beginTransactionWithListener(this);
try{
mApplyingBatch.set(true);
final int numOperations = operations.size();
final ContentProviderResult[] results = new ContentProviderResult[numOperations];
for(int i = 0;i < numOperations;i++){
final ContentProviderOperation operation = operations.get(i);
//这种实现也是调用apply()方法
results[i] = operation.apply(this,results,i);
}
//结束数据库事务
mDb.setTransactionSuccessful();
return results;
}finally{
mApplyingBatch.set(false);
mDb.endTransaction();
//onEndTransaction负责在所有操作执行完毕后,通知数据发生变化
onEndTransaction();
}
}
这里每个操作都是在数据库事务中执行,但是这种实现方法仍需要为每个操作调用apply()方法。
为什么不会在每次insert()/delete()方法调用时得到通知呢?
要正确理解这种方式,需要分析SQLiteContentProvider的insert()方法的实现代码:
@Override
public Uri insert(Uri uri,ContentValues values){
Uri result = null;
//检查是否在进行批量处理
boolean applyingBatch = applyingBatch();
if(!applyingBatch){
mDb = mOpenHelper.getWritableDatabase();
mDb.beginTransactionWithListener(this);
try{
result = insertInTransaction(uri,values);
if(result != null){
mNotifyChange = true;
}
mDb.setTransactionSuccessful();
}finally{
mDb.endTransaction();
}
onEndTransaction();
}else{
//如果在批处理操作中,调用insertInTransaction()方法
result = insertInTransaction(uri,values);
if(result != null){
//如果插入了数据,就打开mNotifyChange标记,以使onEndTransaction()方法知道是否需要省略通知
mNotifyChange = true;
}
}
return result;
}
insertInTransaction()方法的逻辑在我们的实现方案中。该方法与其他方法是相同的,只是缺少了数据变化通知的逻辑。
SQLiteContentProvider类不属于SDK很遗憾,如果ContentProvider使用SQLite数据库存储数据,就试试SQLiteContentProvider吧,它会使UI响应更灵敏,在单独事务中执行操作也会让程序运行更快。
外链地址1
外链地址2