安卓-SQLite-基础知识-全-

安卓 SQLite 基础知识(全)

原文:zh.annas-archive.org/md5/C362B2CF2341EAB7AC3F3FDAF20E2012

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Android 可能是本十年的热词。在短短的时间内,它已经占据了大部分手机市场。Android 计划在今年秋天通过 Android L 版本接管可穿戴设备、我们的电视房间以及我们的汽车。随着 Android 的快速增长,开发人员也需要提升自己的技能。面向数据库的应用程序开发是每个开发人员都应该具备的关键技能之一。应用程序中的 SQLite 数据库是数据中心产品的核心,也是构建优秀产品的关键。理解 SQLite 并实现 Android 数据库对一些人来说可能是一个陡峭的学习曲线。诸如内容提供程序和加载程序之类的概念更加复杂,需要更多的理解和实现。Android SQLite Essentials以简单的方式为开发人员提供了构建基于数据库的 Android 应用程序的工具。它是根据当前行业的需求和最佳实践编写的。让我们开始我们的旅程。

本书涵盖内容

第一章, 进入 SQLite,提供了对 SQLite 架构、SQLite 基础知识及其与 Android 的连接的深入了解。

第二章, 连接点,介绍了如何将数据库连接到 Android 视图。它还涵盖了构建以数据库为中心/启用的应用程序应遵循的一些最佳实践。

第三章, 分享就是关怀,将反映如何通过内容提供程序访问和共享 Android 中的数据,以及如何构建内容提供程序。

第四章, 小心处理线程,将指导您如何使用加载程序并确保数据库和数据的安全。它还将为您提供探索在 Android 应用程序中构建和使用数据库的替代方法的提示。

本书所需内容

为了有效地使用本书,您需要一个预装有 Windows、Ubuntu 或 Mac OS 的工作系统。下载并设置 Java 环境;我们需要这个环境来运行我们选择的 IDE Eclipse。从 Android 开发者网站下载 Android SDK 和 Eclipse 的 Android ADT 插件。或者,您可以下载包含 Eclipse SDK 和 ADT 插件的 Eclipse ADT 捆绑包。您也可以尝试 Android Studio;这个 IDE 刚刚转入 beta 版,也可以在开发者网站上找到。确保您的操作系统、JDK 和 IDE 都是 32 位或 64 位中的一种。

本书适合对象

Android SQLite Essentials是一本面向 Android 程序员的指南,他们想要探索基于 SQLite 数据库的 Android 应用程序。读者应该具有一些 Android 基本构建块的实际经验,以及 IDE 和 Android 工具的知识。

约定

在本书中,您将找到一些文本样式,用于区分不同类型的信息。以下是这些样式的一些示例以及它们的含义解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“要关闭Cursor对象,将使用close()方法调用。”

代码块设置如下:

ContentValues cv = new ContentValues();
cv.put(COL_NAME, "john doe");
cv.put(COL_NUMBER, "12345000");
dataBase.insert(TABLE_CONTACTS, null, cv);

任何命令行输入或输出都以以下形式书写:

adb shell SQLite3 --version
SQLite 3.7.11: API 16 - 19
SQLite 3.7.4: API 11 - 15
SQLite 3.6.22: API 8 - 10
SQLite 3.5.9: API 3 - 7

新术语重要单词以粗体显示。例如,屏幕上看到的单词、菜单或对话框中的单词等,都会以这种方式出现在文本中:“从Windows菜单中转到Android 虚拟设备管理器以启动模拟器。”

注意

警告或重要提示以以下方式显示在一个框中。

提示

提示和技巧以这种方式出现。

第一章:进入 SQLite

SQLite 的架构师和主要作者 Richard Hipp 博士在他 2007 年 6 月接受《卫报》采访时解释了一切是如何开始的:

“我是在 2000 年 5 月 29 日开始的。现在已经七年多了,”他说。他当时正在做一个项目,使用了一个数据库服务器,但数据库不时会离线。“然后我的程序会出错,说数据库不工作了,我就因此受到指责。所以我说,这个数据库对我的应用来说并不是一个很苛刻的要求,为什么我不直接和磁盘对话,然后以这种方式构建一个 SQL 数据库引擎呢?就是这样开始的。”

在我们开始探索 Android 环境中的 SQLite 之旅之前,我们想要告诉您一些先决条件。以下是非常基本的要求,您需要付出很少的努力:

  • 您需要确保 Android 应用程序构建的环境已经就位。当我们说“环境”时,我们指的是 JDK 和 Eclipse 的组合,我们的 IDE 选择,ADT 插件和 Android SDK 工具。如果这些还没有就位,ADT 捆绑包中包含了 IDE、ADT 插件、Android SDK 工具和平台工具,可以从developer.android.com/sdk/index.html下载。链接中提到的步骤非常易懂。对于 JDK,您可以访问 Oracle 的网站下载最新版本并在www.oracle.com/technetwork/java/javase/downloads/index.html设置它。

  • 您需要对 Android 组件有基本的了解,并且在 Android 模拟器上运行过不止“Hello World”程序。如果没有,Android 开发者网站上有一个非常合适的指南来设置模拟器。我们建议您熟悉基本的 Android 组件:Intent、Service、Content Providers 和 Broadcast Receiver。Android 开发者网站上有很好的示例库和文档。其中一些如下:

  • 模拟器:developer.android.com/tools/devices/index.html

  • Android 基础:developer.android.com/training/basics/firstapp/index.html

有了这些准备,我们现在可以开始探索 SQLite 了。

在这一章中,我们将涵盖以下内容:

  • 为什么选择 SQLite?

  • SQLite 的架构

  • 数据库基础知识快速回顾

  • Android 中的 SQLite

为什么选择 SQLite?

SQLite 是一个嵌入式 SQL 数据库引擎。它被广泛应用于诸如 Adobe 集成运行时(AIR)中的 Adobe、空中客车公司的飞行软件、Python 等知名公司。在移动领域,由于其轻量级的特性,SQLite 是各种平台上非常受欢迎的选择。苹果在 iPhone 中使用它,谷歌在 Android 操作系统中使用它。

它被用作应用程序文件格式,电子设备的数据库,网站的数据库,以及企业关系数据库管理系统。是什么让 SQLite 成为这些以及许多其他公司的如此有趣的选择呢?让我们更仔细地看看 SQLite 的特点,看看它为什么如此受欢迎:

  • 零配置:SQLite 被设计成不需要配置文件。它不需要安装步骤或初始设置;它没有运行服务器进程,即使它崩溃也不需要恢复步骤。它没有服务器,直接嵌入在我们的应用程序中。此外,不需要管理员来创建或维护数据库实例,或者为用户设置权限。简而言之,这是一个真正无需 DBA 的数据库。

  • 无版权:SQLite 不是以许可证而是以祝福的形式提供。SQLite 的源代码是公有领域的;您可以自由修改、分发,甚至出售代码。甚至贡献者也被要求签署一份声明,以保护免受未来可能发生的任何版权纠纷的影响。

  • 跨平台: 一个系统的数据库文件可以轻松地移动到运行不同架构的系统上。这是可能的,因为数据库文件格式是二进制的,所有的机器都使用相同的格式。在接下来的章节中,我们将从 Android 模拟器中提取数据库到 Windows。

  • 紧凑: 一个 SQLite 数据库是一个普通的磁盘文件;它没有服务器,被设计成轻量级和简单。这些特性导致了一个非常轻量级的数据库引擎。与其他 SQL 数据库引擎相比,SQLite Version 3.7.8 的占用空间小于 350 KiB(kibibyte)。

  • 防错: 代码库有很好的注释,易于理解,而且是模块化的。SQLite 中的测试用例和测试脚本的代码量大约是 SQLite 库源代码的 1084 倍,他们声称测试覆盖了 100%的分支。这种级别的测试重新确立了开发者对 SQLite 的信心。

注意

有兴趣的读者可以在维基百科上阅读更多关于分支测试覆盖的信息,网址为en.wikipedia.org/wiki/Code_coverage

SQLite 架构

核心、SQL 编译器、后端和数据库构成了 SQLite 的架构:

SQLite 架构

SQLite 接口

根据文档,在 SQLite 库堆栈的顶部,大部分公共接口是由wen.clegacy.cvdbeapi.c源文件实现的。这是其他程序和脚本的通信点。

SQL 编译器

分词器将从接口传递的 SQL 字符串分解为标记,并逐个将标记传递给解析器。分词器是用 C 手工编码的。SQLite 的解析器是由 Lemon 解析器生成器生成的。它比 YACC 和 Bison 更快,同时是线程安全的,并防止内存泄漏。解析器从分词器传递的标记构建解析树,并将树传递给代码生成器。生成器从输入生成虚拟机代码,并将其作为可执行文件传递给虚拟机。有关 Lemon 解析器生成器的更多信息,请访问en.wikipedia.org/wiki/Lemon_Parser_Generator

虚拟机

虚拟机,也被称为虚拟数据库引擎VDBE),是 SQLite 的核心。它负责从数据库中获取和更改值。它执行代码生成器生成的程序来操作数据库文件。每个 SQL 语句首先被转换为 VDBE 的虚拟机语言。VDBE 的每个指令都包含一个操作码和最多三个附加操作数。

SQLite 后端

B 树,连同 Pager 和 OS 接口,形成了 SQLite 架构的后端。B 树用于组织数据。Pager 则通过缓存、修改和回滚数据来辅助 B 树。当需要时,B 树会从缓存中请求特定的页面;这个请求由 Pager 以高效可靠的方式处理。OS 接口提供了一个抽象层,可以移植到不同的操作系统。它隐藏了与不同操作系统通信的不必要细节,由 SQLite 调用代表 SQLite 处理。

这些是 SQLite 的内部,Android 应用程序开发者不需要担心 Android 的内部,因为 SQLite Android 库有效地使用了抽象的概念,所有的复杂性都被隐藏起来。只需要掌握提供的 API,就可以满足在 Android 应用程序中使用 SQLite 的所有可能的用例。

数据库基础知识的快速回顾

数据库,简单来说,是一种有序的持续存储数据的方式。数据保存在表中。表由不同数据类型的列组成。表中的每一行对应一个数据记录。您可以将表想象成 Excel 电子表格。从面向对象编程的角度来看,数据库中的每个表通常描述一个对象(由类表示)。每个表列举了一个类属性。表中的每条记录表示该对象的特定实例。

让我们看一个快速的例子。假设您有一个名为Shop的数据库,其中有一个名为Inventory的表。这个表可以用来存储商店中所有产品的信息。Inventory表可能包含这些列:产品名称(字符串)、产品 ID(数字)、成本(数字)、库存(0/1)和可用数量(数字)。然后,您可以向数据库中添加一个名为鞋子的产品记录:

ID 产品名称 产品 ID 成本 库存 可用数量
1 地毯 340023 2310 1 4
2 鞋子 231257 235 1 2

数据库中的数据应该经过检查和影响。表中的数据可以如下所示:

  • 使用INSERT命令添加

  • 使用UPDATE命令修改

  • 使用DELETE命令删除

您可以通过使用所谓的查询在数据库中搜索特定数据。查询(使用SELECT命令)可以涉及一个表,或多个表。要生成查询,必须使用 SQL 命令确定感兴趣的表、数据列和数据值。每个 SQL 命令都以分号(;)结尾。

什么是 SQLite 语句?

SQLite 语句是用 SQL 编写的,用于从数据库中检索数据或创建、插入、更新或删除数据库中的数据。

所有 SQLite 语句都以关键字之一开头:SELECTINSERTUPDATEDELETEALTERDROP等,所有语句都以分号(;)结尾。例如:

CREATE TABLE table_name (column_name INTEGER);

CREATE TABLE命令用于在 SQLite 数据库中创建新表。CREATE TABLE命令描述了正在创建的新表的以下属性:

  • 新表的名称。

  • 创建新表的数据库。表可以在主数据库、临时数据库或任何已连接的数据库中生成。

  • 表中每列的名称。

  • 表中每列的声明类型。

  • 表中每列的默认值或表达式。

  • 每列使用的默认关系序列。

  • 最好为表设置一个PRIMARY KEY。这将支持单列和复合(多列)主键。

  • 每个表的一组 SQL 约束。支持UNIQUENOT NULLCHECKFOREIGN KEY等约束。

  • 在某些情况下,表将是WITHOUT ROWID表。

以下是一个创建表的简单 SQLite 语句:

String databaseTable =   "CREATE TABLE " 
   + TABLE_CONTACTS +"(" 
   + KEY_ID  
   + " INTEGER PRIMARY KEY,"
   + KEY_NAME + " TEXT,"
   + KEY_NUMBER + " INTEGER"
   + ")";

在这里,CREATE TABLE是创建一个名为TABLE_CONTACTS的表的命令。KEY_IDKEY_NAMEKEY_NUMBER是列 ID。SQLite 要求为每个列提供唯一 ID。INTEGERTEXT是与相应列关联的数据类型。SQLite 要求在创建表时定义要存储在列中的数据类型。PRIMARY KEY是数据列的约束(对表中的数据列强制执行的规则)。

SQLite 支持更多的属性,可用于创建表,例如,让我们创建一个create table语句,为空列输入默认值。请注意,对于KEY_NAME,我们提供了一个默认值xyz,对于KEY_NUMBER列,我们提供了一个默认值100

String databaseTable = 
   "CREATE TABLE " 
   + TABLE_CONTACTS  + "(" 
   + KEY_ID    + " INTEGER PRIMARY KEY,"

   + KEY_NAME + " TEXT DEFAULT  xyz,"

   + KEY_NUMBER + " INTEGER DEFAULT 100" + ")";

在这里,当在数据库中插入一行时,这些列将以CREATE SQL 语句中定义的默认值进行预初始化。

还有更多的关键字,但我们不想让你因为一个庞大的列表而感到无聊。我们将在后续章节中介绍其他关键字。

SQLite 语法

SQLite 遵循一组称为语法的独特规则和指南。

需要注意的一点是,SQLite 是不区分大小写的,但有一些命令是区分大小写的,例如,在 SQLite 中,GLOBglob具有不同的含义。让我们以 SQLite DELETE语句的语法为例。尽管我们使用了大写字母,但用小写字母替换它们也可以正常工作:

DELETE FROM table WHERE {condition};

SQLite 中的数据类型

SQLite 使用动态和弱类型的 SQL 语法,而大多数 SQL 数据库使用静态、严格的类型。如果我们看其他语言,Java 是一种静态类型语言,Python 是一种动态类型语言。那么当我们说动态或静态时,我们是什么意思呢?让我们看一个例子:

a=5
a="android"

在静态类型的语言中,这将引发异常,而在动态类型的语言中,它将起作用。在 SQLite 中,值的数据类型与其容器无关,而与值本身相关。当处理静态类型系统时,这不是一个问题,其中值由容器确定。这是因为 SQLite 向后兼容更常见的静态类型系统。因此,我们用于静态系统的 SQL 语句可以在这里无缝使用。

存储类

在 SQLite 中,我们有比数据类型更一般的存储类。在内部,SQLite 以五种存储类存储数据,也可以称为原始数据类型

  • NULL:这代表数据库中的缺失值。

  • INTEGER:这支持带符号整数的范围,从 1、2、3、4、6 或 8 个字节,取决于值的大小。SQLite 会根据值的大小自动处理这一点。在内存中处理时,它们会被转换为最一般的 8 字节带符号整数形式。

  • REAL:这是一个浮点值,SQLite 使用它作为 8 字节 IEEE 浮点数来存储这些值。

  • TEXT:SQLite 支持各种字符编码,如 UTF-8、UTF-16BE 或 UTF-16LE。这个值是一个文本字符串。

  • BLOB:这种类型存储了一个大的二进制数据数组,就像输入时提供的那样。

SQLite 本身不验证写入列的类型是否实际上是定义的类型,例如,您可以将整数写入字符串列,反之亦然。我们甚至可以有一个单独的列具有不同的存储类:

 id                   col_t
------               ------
1                       23
2                     NULL
3                     test

布尔数据类型

SQLite 没有单独的布尔存储类,而是使用Integer类来实现这一目的。整数0表示假状态,而1表示真状态。这意味着 SQLite 间接支持布尔类型,我们只能创建布尔类型的列。问题是,它不包含熟悉的TRUE/FALSE值。

日期和时间数据类型

就像我们在布尔数据类型中看到的那样,在 SQLite 中没有日期和时间数据类型的存储类。SQLite 有五个内置的日期和时间函数来帮助我们处理它;我们可以将日期和时间用作整数、文本或实数值。此外,这些值是可互换的,取决于应用程序的需要。例如,要计算当前日期,请使用以下代码:

SELECT date('now');

Android 中的 SQLite

Android 软件堆栈由核心 Linux 内核、Android 运行时、支持 Android 框架的 Android 库以及最终运行在所有这些之上的 Android 应用程序组成。Android 运行时使用Dalvik 虚拟机DVM)来执行 dex 代码。在较新的 Android 版本中,即从 KitKat(4.4)开始,Android 启用了一个名为ART的实验性功能,它最终将取代 DVM。它基于Ahead of TimeAOT),而 DVM 基于Just in TimeJIT)。在下图中,我们可以看到 SQLite 提供了本地数据库支持,并且是支持应用程序框架的库的一部分,还有诸如 SSL、OpenGL ES、WebKit 等库。这些用 C/C++编写的库在 Linux 内核上运行,并与 Android 运行时一起构成了应用程序框架的支撑,如下图所示:

Android 中的 SQLite

在我们开始探索 Android 中的 SQLite 之前,让我们先看看 Android 中的其他持久存储替代方案:

  • 共享偏好:数据以键值对的形式存储在共享偏好中。文件本身是一个包含键值对的 XML 文件。该文件位于应用程序的内部存储中,可以根据需要进行公共或私有访问。Android 提供了 API 来写入和读取共享偏好。建议在需要保存少量此类数据时使用此功能。一个常见的例子是保存 PDF 中的最后阅读位置,或者保存用户的偏好以显示评分框。

  • 内部/外部存储:这个术语可能有点误导;Android 定义了两个存储空间来保存文件。在一些设备上,你可能会有一个外部存储设备,比如 SD 卡,而在其他设备上,你会发现系统将其内存分成两部分,分别标记为内部和外部。可以使用 Android API 获取外部和内部存储的路径。默认情况下,内部存储是有限的,只能被应用程序访问,而外部存储可能可用,也可能不可用,具体取决于是否已挂载。

提示

android:installLocation可以在清单中使用,指定应用程序的内部/外部安装位置。

SQLite 版本

从 API 级别 1 开始,Android 就内置了 SQLite。在撰写本书时,当前版本的 SQLite 是 3.8.4.1。根据文档,SQLite 的版本是 3.4.0,但已知不同的 Android 版本会内置不同版本的 SQLite。我们可以通过 Android SDK 安装文件夹内的platform-tools文件夹中的名为SQLite3的工具以及 Android 模拟器轻松验证这一点。

adb shell SQLite3 --version
SQLite 3.7.11: API 16 - 19
SQLite 3.7.4: API 11 - 15
SQLite 3.6.22: API 8 - 10
SQLite 3.5.9: API 3 - 7

我们不需要担心 SQLite 的不同版本,应该坚持使用 3.5.9 以确保兼容性,或者我们可以按照 API 14 是新的minSdkVersion的说法,并将其切换为 3.7.4。除非你有特定于某个版本的需求,否则这几乎不重要。

注意

一些额外方便的 SQLite3 命令如下:

  • .dump:打印表的内容

  • .schema:打印现有表的SQL CREATE语句

  • .help:获取指令

数据库包

android.database包含了所有与数据库操作相关的必要类。android.database.SQLite包含了特定于 SQLite 的类。

API

Android 提供了各种 API 来创建、访问、修改和删除数据库。完整的列表可能会让人感到不知所措;为了简洁起见,我们将介绍最重要和最常用的 API。

SQLiteOpenHelper 类

SQLiteOpenHelper类是 Android 中用于处理 SQLite 数据库的第一个和最重要的类;它位于android.database.SQLite命名空间中。SQLiteOpenHelper是一个辅助类,旨在进行扩展,并在创建、打开和使用数据库时实现您认为重要的任务和操作。这个辅助类由 Android 框架提供,用于处理 SQLite 数据库,并帮助管理数据库的创建和版本管理。操作方式是扩展该类,并根据我们的应用程序的要求实现任务和操作。SQLiteOpenHelper有以下定义的构造函数:

SQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version)

SQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler)

应用程序上下文允许访问应用程序的所有共享资源和资产。name参数包括 Android 存储中的数据库文件名。SQLiteDatabase.CursorFactory是一个工厂类,用于创建光标对象,充当针对 Android 下 SQLite 应用的所有查询的输出集。数据库的应用程序特定版本号将是版本参数(或更确切地说,它的模式)。

SQLiteOpenHelper的构造函数用于创建一个帮助对象来创建、打开或管理数据库。context是允许访问所有共享资源和资产的应用程序上下文。name参数要么包含数据库的名称,要么为内存中的数据库为 null。SQLiteDatabase.CursorFactory工厂创建一个光标对象,充当所有查询的结果集。version参数定义了数据库的版本号,并用于升级/降级数据库。第二个构造函数中的errorHandler参数在 SQLite 报告数据库损坏时使用。

如果我们的数据库版本号不是默认的1SQLiteOpenHelper将触发其onUpgrade()方法。SQLiteOpenHelper类的重要方法如下:

  • synchronized void close()

  • synchronized SQLiteDatabase getReadableDatabase()

  • synchronized SQLiteDatabase getWritableDatabase()

  • abstract void onCreate(SQLiteDatabase db)

  • void onOpen(SQLiteDatabase db)

  • abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)

同步的close()方法关闭任何打开的数据库对象。synchronized关键字可以防止线程和内存一致性错误。

接下来的两个方法getReadableDatabase()getWriteableDatabase()是实际创建或打开数据库的方法。两者都返回相同的SQLiteDatabase对象;不同之处在于getReadableDatabase()在无法返回可写数据库时将返回可读数据库,而getWriteableDatabase()返回可写数据库对象。如果无法打开数据库进行写操作,getWriteableDatabase()方法将抛出SQLiteException。对于getReadableDatabase(),如果无法打开数据库,它将抛出相同的异常。

我们可以使用SQLiteDatabase类的isReadOnly()方法来了解数据库的状态。对于只读数据库,它返回true

调用这两个方法中的任何一个将在数据库尚不存在时调用onCreate()方法。否则,它将调用onOpen()onUpgrade()方法,具体取决于版本号。onOpen()方法应在更新数据库之前检查isReadOnly()方法。一旦打开,数据库将被缓存以提高性能。最后,我们需要调用close()方法来关闭数据库对象。

onCreate()onOpen()onUpgrade()方法是为了子类实现预期行为。当数据库第一次创建时,将调用onCreate()方法。这是我们使用 SQLite 语句创建表的地方,这些语句在前面的示例中已经看到了。当数据库已经配置并且数据库模式已经根据需要创建、升级或降级时,将触发onOpen()方法。在这里应该使用isReadOnly()方法检查读/写状态。

当数据库需要根据提供的版本号进行升级时,将调用onUpgrade()方法。默认情况下,数据库版本是1,随着我们增加数据库版本号并发布新版本,将执行升级。

本章的代码包中包含了一个演示 SQLiteOpenHelper 类的简单示例;我们将用它进行解释:

class SQLiteHelperClass
    {
    ...
    ...
    public static final int VERSION_NUMBER = 1;

    sqlHelper =
       new SQLiteOpenHelper(context, "ContactDatabase", null,
      VERSION_NUMBER)
    {

      @Override
      public void onUpgrade(SQLiteDatabase db,   
            int oldVersion, int newVersion) 
      {

        //drop table on upgrade
        db.execSQL("DROP TABLE IF EXISTS " 
                + TABLE_CONTACTS);
        // Create tables again
        onCreate(db);

      }

   @Override
   public void onCreate(SQLiteDatabase db)
   {
      // creating table during onCreate
      String createContactsTable = 
 "CREATE TABLE "
 + TABLE_CONTACTS + "(" 
 + KEY_ID + " INTEGER PRIMARY KEY," 
 + KEY_NAME + " TEXT,"
 + KEY_NUMBER + " INTEGER" + ")";

        try {
       db.execSQL(createContactsTable);
        } catch(SQLException e) {
          e.printStackTrace();
        }
   }

   @Override
   public synchronized void close()
   {
      super.close();
      Log.d("TAG", "Database closed");
   }

   @Override
   public void onOpen(SQLiteDatabase db)
   {
         super.onOpen(db);
         Log.d("TAG", "Database opened");
   }

};

...
... 

//open the database in read-only mode
SQLiteDatabase db = SQLiteOpenHelper.getWritableDatabase();

...
...

//open the database in read/write mode
SQLiteDatabase db = SQLiteOpenHelper.getWritableDatabase();

提示

下载示例代码

您可以从您在www.packtpub.com的帐户中购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,直接将文件发送到您的电子邮件。

SQLiteDatabase 类

现在您已经熟悉了在 Android 中启动 SQLite 数据库使用的辅助类,是时候看看核心的SQLiteDatabase类了。SQLiteDatabase是在 Android 中使用 SQLite 数据库所需的基类,并提供了打开、查询、更新和关闭数据库的方法。

SQLiteDatabase类提供了 50 多种方法,每种方法都有其自己的细微差别和用例。我们将覆盖最重要的方法子集,而不是详尽的列表,并允许您在闲暇时探索一些重载方法。您可以随时参考developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html上的完整在线 Android 文档了解SQLiteDatabase类。

以下是SQLiteDatabase类的一些方法:

  • public long insert (String table, String nullColumnHack, ContentValues values)

  • public Cursor query (String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)

  • public Cursor rawQuery(String sql, String[] selectionArgs)

  • public int delete (String table, String whereClause, String[] whereArgs)

  • public int update (String table, ContentValues values, String whereClause, String[] whereArgs)

让我们通过一个示例来看看这些SQLiteDatabase类的实际应用。我们将在表中插入一个名称和数字。然后,我们将使用原始查询从表中获取数据。之后,我们将介绍delete()update()方法,这两种方法都将以id作为参数,以确定我们打算删除或更新数据库表中的哪一行数据:

public void insertToSimpleDataBase() 
{
   SQLiteDatabase db = sqlHelper.getWritableDatabase();

   ContentValues cv = new ContentValues();
   cv.put(KEY_NAME, "John");
   cv.put(KEY_NUMBER, "0000000000");
   // Inserting values in different columns of the table using
   // Content Values
   db.insert(TABLE_CONTACTS, null, cv);

   cv = new ContentValues();
   cv.put(KEY_NAME, "Tom");
   cv.put(KEY_NUMBER, "5555555");
   // Inserting values in different columns of the table using
   // Content Values
   db.insert(TABLE_CONTACTS, null, cv);
}
...
...

public void getDataFromDatabase()
{  
   int count;
   db = sqlHelper.getReadableDatabase();
   // Use of normal query to fetch data
   Cursor cr = db. query(TABLE_CONTACTS, null, null, 
                           null, null, null, null);

   if(cr != null) {
      count = cr.getCount();
      Log.d("DATABASE", "count is : " + count);
   }

   // Use of raw query to fetch data
   cr = db.rawQuery("select * from " + TABLE_CONTACTS, null);
   if(cr != null) {
      count = cr.getCount();
      Log.d("DATABASE", "count is : " + count);
   }

}
...
...

public void delete(String name)
 {
     String whereClause = KEY_NAME + "=?";
     String[] whereArgs = new String[]{name};
     db = sqlHelper.getWritableDatabase();
     int rowsDeleted = db.delete(TABLE_CONTACTS, whereClause, whereArgs);
 }
...
...

public void update(String name)
 {
     String whereClause = KEY_NAME + "=?";
     String[] whereArgs = new String[]{name};
     ContentValues cv = new ContentValues();
     cv.put(KEY_NAME, "Betty");
     cv.put(KEY_NUMBER, "999000");
     db = sqlHelper.getWritableDatabase();
     int rowsUpdated = db.update(TABLE_CONTACTS, cv, whereClause, whereArgs);
 }

ContentValues

ContentValues本质上是一组键值对,其中键表示表的列,值是要插入该列的值。因此,在values.put("COL_1", 1);的情况下,列是COL_1,要插入该列的值是1

以下是一个示例:

ContentValues cv = new ContentValues();
cv.put(COL_NAME, "john doe");
cv.put(COL_NUMBER, "12345000");
dataBase.insert(TABLE_CONTACTS, null, cv);

游标

查询会返回一个Cursor对象。Cursor对象描述了查询的结果,基本上指向查询结果的一行。通过这种方法,Android 可以以一种高效的方式缓冲查询结果;因为它不需要将所有数据加载到内存中。

您可以使用getCount()方法获取查询结果的元素。

要在各个数据行之间导航,可以利用moveToFirst()moveToNext()方法。isAfterLast()方法允许您分析输出是否已经结束。

Cursor对象提供了带类型的get*()方法,例如getLong(columnIndex)getString(columnIndex)方法,以便访问结果的当前位置的列数据。columnIndex是您将要访问的列的编号。

Cursor对象还提供了getColumnIndexOrThrow(String)方法,允许您获取表的列名的列索引。

要关闭Cursor对象,将使用close()方法调用。

数据库查询返回一个游标。这个接口提供了对结果集的随机读写访问。它指向查询结果的一行,使得 Android 能够有效地缓冲结果,因为现在不需要将所有数据加载到内存中。

返回的游标指针指向第 0 个位置,也就是游标的第一个位置。我们需要在Cursor对象上调用moveToFirst()方法;它将游标指针移动到第一个位置。现在我们可以访问第一条记录中的数据。

如果来自多个线程的游标实现,应在使用游标时执行自己的同步。通过调用close()方法关闭游标以释放对象持有的资源。

我们将遇到一些其他支持方法,如下所示:

  • getCount()方法:返回查询结果中元素的数量。

  • get*()方法:用于访问结果的当前位置的列数据,例如,getLong(columnIndex)getString(columnIndex)

  • moveToNext()方法:将游标移动到下一行。如果游标已经超过了结果集中的最后一个条目,它将返回false

总结

在本章中,我们介绍了 SQLite 的特性和内部架构。我们从讨论 SQLite 的显著特点开始,然后介绍了 SQLite 的基本架构,如语法和数据类型,最后转向了 Android 中的 SQLite。我们探索了在 Android 中使用 SQLite 的 Android API。

在下一章中,我们将专注于将本章学到的知识应用到构建 Android 应用程序中。我们将专注于 UI 元素和将 UI 连接到数据库组件。

第二章:连接点

"除非你以多种方式学习,否则你不会理解任何东西。"
---Marvin Minsky

在上一章中,我们学习了两个重要的 Android 类及其相应的方法,以便与 SQLite 数据库一起工作:

  • SQLiteOpenHelper

  • SQLiteDatabase

我们还看到了解释它们实现的代码片段。现在,我们准备在 Android 应用程序中使用所有这些概念。我们将利用上一章中学到的知识来制作一个功能性的应用程序。我们还将进一步研究插入、查询和删除数据库中的数据的 SQL 语句。

在本章中,我们将在 Android 模拟器上构建和运行 Android 应用程序。我们还将构建我们自己的完整的contacts数据库。在本章的过程中,我们将遇到 Android UI 组件,如ButtonsListView。如果需要重新访问 Android 中的 UI 组件,请访问链接developer.android.com/design/building-blocks/index.html

在我们开始之前,本章的代码旨在解释与 Android 中的 SQLite 数据库相关的概念,并不适用于生产;在许多地方,您会发现缺乏适当的异常处理或适当的空值检查以及类似的实践,以减少代码的冗长。您可以从 Packt 的网站下载当前和以下章节的完整代码。为了获得最佳结果,我们建议在阅读本章的过程中下载代码并参考它。

在本章中,我们将涵盖:

  • 构建模块

  • 数据库处理程序和查询

  • 连接 UI 和数据库

构建模块

Android 以在不同硬件和软件规格的各种设备上运行而闻名。在撰写本书时,激活标记已经突破了 10 亿。运行 Android 的设备数量惊人,为用户提供了不同形态和不同硬件基础的丰富选择。当在不同设备上测试应用程序时,这增加了障碍,因为人类不可能获得所有这些设备,更不用说需要投入其中的时间和资本。模拟器本身是一个很好的工具;它使我们能够通过模拟不同的硬件特性(如 CPU 架构、RAM 和相机)和从早期的 Cupcake 到 KitKat 的不同软件版本,来规避这个问题。我们还将尝试利用这一优势来运行我们的应用程序。使用模拟器的另一个好处是,我们将运行一个已 root 的设备,这将允许我们执行一些操作。在普通设备上,我们将无法执行这些操作。

让我们从在 Eclipse 中设置模拟器开始:

  1. 转到窗口菜单中的Android 虚拟设备管理器以启动模拟器。

我们可以设置不同的硬件属性,如 CPU 类型、前/后摄像头、RAM(最好在 Windows 机器上少于 768MB)、内部和外部存储大小。

  1. 启动应用程序时,启用保存快照;这将减少下次从快照启动模拟器实例时的启动时间:Building blocks

注意

有兴趣的读者可以尝试在www.genymotion.co上尝试更快的模拟器 Genymotion。

现在让我们开始构建我们的 Android 应用程序。

  1. 我们将从创建一个名为PersonalContactManager的新项目开始。转到文件 | 新建 | 项目。现在,导航到Android,然后选择Android 应用程序项目。这一步将为我们提供一个活动文件和一个相应的 XML 文件。

在我们放置所有需要的块之后,我们将回到这些组件。对于我们的应用程序,我们将创建一个名为contact的数据库,其中将包含一个名为ContactsTable的表。在上一章中,我们讨论了如何使用 SQL 语句创建数据库;让我们为我们的项目构建一个数据库架构。这是一个非常重要的步骤,它基于我们应用程序的要求;例如,在我们的情况下,我们正在构建一个个人联系人管理器,并且将需要诸如姓名、号码、电子邮件和显示图片等字段。

ContactsTable的数据库架构概述如下:

数据类型
Contact_ID 整数/主键/自动递增
姓名 文本
号码 文本
电子邮件 文本
照片 Blob

注意

一个 Android 应用可以有多个数据库,每个数据库可以有多个表。每个表以 2D(行和列)格式存储数据。

第一列是Contact_ID。它的数据类型是整数,其列约束是主键。此外,当在该行中插入数据时,该列是自动递增的,这意味着对于每一行,它将递增一次。

主键唯一标识每一行,不能为 null。数据库中的每个表最多可以有一个主键。一个表的主键可以作为另一个表的外键。外键作为两个相关表之间的连接;例如,我们当前的ContactsTable架构是:

ContactsTable (Contact_ID,Name, Number, Email, Photo)

假设我们有另一个具有以下架构的表ColleagueTable

ColleagueTable (Colleague_ID, Contact_ID, Position, Fax)

在这里,ContactTable的主键,即Contact_ID可以称为ColleagueTable的外键。它用于在关系数据库中连接两个表,因此允许我们对ColleagueTable执行操作。我们将在接下来的章节和示例中详细探讨这个概念。

注意

列约束

约束是对表中数据列强制执行的规则。这确保了数据库中数据的准确性和可靠性。

与大多数 SQL 数据库不同,SQLite 不会根据声明的列类型限制可以插入列的数据类型。相反,SQLite 使用动态类型。列的声明类型仅用于确定列的亲和性。当将一种类型的变量存储在另一种类型中时,也会进行类型转换(自动)。

约束可以是列级或表级。列级约束仅应用于一列,而表级约束应用于整个表。

以下是 SQLite 中常用的约束和关键字:

  • NOT NULL约束:这确保列没有NULL值。

  • DEFAULT约束:当未指定列的默认值时,这为列提供了默认值。

  • UNIQUE约束:这确保列中的所有值都不同。

  • 主键:这个唯一标识数据库表中所有行/记录的键。

  • CHECK约束:CHECK约束确保列中的所有值满足某些条件。

  • AUTO INCREMENT关键字:AUTOINCREMENT是一个用于自动递增表中字段值的关键字。我们可以使用AUTOINCREMENT关键字来自动递增一个字段值,当创建一个具有特定列名的表时,使用AUTOINCREMENT关键字。关键字AUTOINCREMENT只能与INTEGER字段一起使用。

下一步是准备我们的数据模型;我们将使用我们的模式来构建数据模型类。ContactModel类将具有Contact_IDNameNumberEmailPhoto作为字段,它们分别表示为idnamecontactNoemailbyteArray。该类将包括一个 getter/setter 方法,根据需要设置和获取属性值。数据模型的使用将有助于活动与数据库处理程序之间的通信,我们将在本章后面定义。我们将在其中创建一个新的包和一个新的类,称为ContactModel类。请注意,创建一个新的包不是必要的步骤;它用于以逻辑和易于访问的方式组织我们的类。这个类可以描述如下:

public class ContactModel {
  private int id;
  private String name, contactNo, email;
  private byte[] byteArray;

  public byte[] getPhoto() {
    return byteArray;
  }
  public void setPhoto(byte[] array) {
    byteArray = array;
  }
  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  ……………
}

提示

Eclipse 提供了很多有用的快捷方式,但不包括生成 getter 和 setter 方法。我们可以将生成 getter 和 setter 方法绑定到任何我们喜欢的键绑定上。在 Eclipse 中,转到窗口 | 首选项 | 常规 | ,搜索 getter,并添加你的绑定。我们使用Alt + Shift + G;你可以自由设置任何其他键组合。

数据库处理程序和查询

我们将构建一个支持类,该类将根据我们的数据库需求包含读取、更新和删除数据的方法。这个类将使我们能够创建和更新数据库,并充当我们的数据管理中心。我们将使用这个类来运行 SQLite 查询,并将数据发送到 UI;在我们的情况下,是一个 listview 来显示结果:

public class DatabaseManager {

  private SQLiteDatabase db; 
  private static final String DB_NAME = "contact";

  private static final int DB_VERSION = 1;
  private static final String TABLE_NAME = "contact_table";
  private static final String TABLE_ROW_ID = "_id";
  private static final String TABLE_ROW_NAME = "contact_name";
  private static final String TABLE_ROW_PHONENUM = "contact_number";
  private static final String TABLE_ROW_EMAIL = "contact_email";
  private static final String TABLE_ROW_PHOTOID = "photo_id";
  .........
}

我们将创建一个SQLiteDatabase类的对象,稍后我们将用getWritableDatabase()getReadableDatabase()来初始化它。我们将定义整个类中将要使用的常量。

注意

按照惯例,常量以大写字母定义,但在定义常量时使用static final比惯例更多一些。要了解更多,请参考goo.gl/t0PoQj

我们将把数据库的名称定义为contact,并将版本定义为 1。如果我们回顾前一章,我们会记得这个值的重要性。对这个值的快速回顾使我们能够将数据库从当前版本升级到新版本。通过这个例子,用例将变得清晰。假设将来有一个新的需求,即我们需要在我们的联系人详细信息中添加传真号码。我们将修改我们当前的模式以包含这个变化,我们的联系人数据库将相应地改变。如果我们在新设备上安装应用程序,就不会有问题;但在已经运行应用程序的设备上,我们将面临问题。在这种情况下,DB_VERSION将派上用场,并帮助我们用当前版本替换旧版本的数据库。另一种方法是卸载应用程序并重新安装,但这是不鼓励的。

现在将定义表名和重要字段,如表列。TABLE_ROW_ID是一个非常重要的列。这将作为表的主键;它还将自动递增,不能为 null。NOT NULL再次是一个列约束,它只能附加到列定义,并且不能作为表约束指定。毫不奇怪,NOT NULL约束规定相关列不能包含NULL值。在插入新行或更新现有行时,如果尝试将列值设置为NULL,将导致约束违规。这将用于在表中查找特定值。ID 的唯一性保证了我们在表中没有与表中数据冲突,因为每一行都是由这个键唯一标识的。表的其余列都相当容易理解。DatabaseManager类的构造函数如下:

public DatabaseManager(Context context) {
   this.context = context;
   CustomSQLiteOpenHelper helper = new CustomSQLiteOpenHelper(context);
   this.db = helper.getWritableDatabase();
  }

注意我们使用了一个名为CustomSQLiteOpenHelper的类。我们稍后会回到这个问题。我们将使用该类对象来获取我们的SQLitedatabase实例。

构建创建查询

为了创建一个具有所需列的表,我们将构建一个查询语句并执行它。该语句将包含表名、不同的表列和相应的数据类型。我们现在将看一下创建新数据库以及根据应用程序的需求升级现有数据库的方法:

private class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
  public CustomSQLiteOpenHelper(Context context) {
    super(context, DB_NAME, null, DB_VERSION);
  }
  @Override
  public void onCreate(SQLiteDatabase db) {
String newTableQueryString = "create table "
+ TABLE_NAME + " ("
+ TABLE_ROW_ID 
+ " integer primary key autoincrement not null,"
+ TABLE_ROW_NAME
+ " text not null," 
+ TABLE_ROW_PHONENUM 
+ " text not null,"
+ TABLE_ROW_EMAIL
+ " text not null,"
+ TABLE_ROW_PHOTOID 
+ " BLOB" + ");";
    db.execSQL(newTableQueryString);
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, 
int newVersion) {

    String DROP_TABLE = "DROP TABLE IF EXISTS " + 
TABLE_NAME;
    db.execSQL(DROP_TABLE);
    onCreate(db);
  }
}

CustomSQLiteOpenHelper扩展了SQLiteOpenHelper,并为我们提供了关键方法onCreate()onUpgrade()。我们已将此类定义为DatabaseManager类的内部类。这使我们能够从一个地方管理所有与数据库相关的功能,即 CRUD(创建、读取、更新和删除)。

在我们的CustomSQLiteOpenHelper构造函数中,负责创建我们类的实例,我们将传递一个上下文,然后将其传递给超级构造函数,参数如下:

  • Context context:这是我们传递给构造函数的上下文

  • String name:这是我们数据库的名称

  • CursorFactory factory:这是游标工厂对象,可以传递为null

  • int version:这是数据库的版本

下一个重要的方法是onCreate()。我们将构建我们的 SQLite 查询字符串,用于创建我们的数据库表:

"create table " + TABLE_NAME + " ("
+ TABLE_ROW_ID
+ " integer primary key autoincrement not null,"
….....
+ TABLE_ROW_PHOTOID + " BLOB" + ");";

前面的语句基于以下语法图:

构建创建查询

在这里,关键字create table用于创建表。接着是表名、列的声明和它们的数据类型。准备好我们的 SQL 语句后,我们将使用 SQLite 数据库的execSQL()方法来执行它。如果我们之前构建的查询语句有问题,我们将遇到异常android.database.sqlite.SQLiteException。默认情况下,数据库形成在应用程序分配的内部存储空间中。该文件夹可以在/data/data/<yourpackage>/databases/找到。

我们可以在模拟器或已获取 root 权限的手机上运行这段代码时轻松验证我们的数据库是否已创建。在 Eclipse 中,转到 DDMS 透视图,然后转到文件管理器。如果我们有足够的权限,即已获取 root 权限的设备,我们可以轻松导航到给定的文件夹。我们还可以借助文件资源管理器拉取我们的数据库,并借助独立的 SQLite 管理工具查看我们的数据库,并对其执行 CRUD 操作。是什么使得 Android 应用程序的数据库可以通过其他工具读取?还记得我们在上一章中讨论过 SQLite 特性中的跨平台吗?在下面的截图中,注意表名、用于构建它的 SQL 语句以及列名及其数据类型:

构建创建查询

注意

SQLite 管理工具可以在 Chrome 或 Firefox 浏览器中下载。以下是 Firefox 扩展的链接:goo.gl/NLu8JT

另一个方便的方法是使用adb pull命令来拉取我们的数据库或任何其他文件:

adb pull /data/data/your package name/databases  /file location

另一个有趣的要点是TABLE_ROW_PHOTOID的数据类型是BLOB。BLOB 代表二进制大对象。它与其他数据类型(如文本和整数)不同,因为它可以存储二进制数据。二进制数据可以是图像、音频或任何其他类型的多媒体对象。

不建议在数据库中存储大型图像;我们可以存储文件名或位置,但存储图像有点过度。想象一种情况,我们存储联系人图像。为了放大这种情况,让它不是几百个联系人,而是几千个联系人。数据库的大小将变得很大,访问时间也会增加。我们想通过存储联系人图像来演示 BLOB 的使用。

当数据库升级时,将调用onUpgrade()方法。通过更改数据库的版本号来升级数据库。在这里,实现取决于应用的需求。在某些情况下,可能需要删除整个表并创建一个新表,在某些应用程序中,可能只需要进行轻微修改。如何从一个版本迁移到另一个版本在第四章中有所涵盖,小心操作

构建插入查询

要在数据库表中插入新的数据行,我们需要使用insert()方法或者制作一个插入查询语句并使用execute()方法:

public void addRow(ContactModel contactObj) {
  ContentValues values = prepareData(contactObj);
  try {
    db.insert(TABLE_NAME, null, values);
  } catch (Exception e) {
    Log.e("DB ERROR", e.toString()); 
    e.printStackTrace();
  }
}

如果我们的表名错误,SQLite 将给出一个日志no such table消息和异常android.database.sqlite.SQLiteExceptionaddRow()方法用于在数据库行中插入联系人详细信息;请注意,该方法的参数是ContactModel的对象。我们创建了一个额外的方法prepareData(),从ContactModel对象的 getter 方法构造一个ContentValues对象。

.......................
values.put(TABLE_ROW_NAME, contactObj.getName());
values.put(TABLE_ROW_PHONENUM, contactObj.getContactNo());
....................

在准备好ContentValues对象之后,我们将使用SQLiteDatabase类的insert()方法:

public long insert (String table, String nullColumnHack, ContentValues values)

insert()方法的参数如下:

  • table:要将行插入的数据库表。

  • values:这个键值映射包含表行的初始列值。列名充当键。值作为列值。

  • nullColumnHack:这与其名称一样有趣。以下是来自 Android 文档网站的一句引用:

“可选;可能为空。SQL 不允许插入完全空的行,而不命名至少一个列名。如果您提供的值为空,那么不知道任何列名,也无法插入空行。如果未设置为 null,则 nullColumnHack 参数提供可为空列名的名称,以明确在值为空的情况下插入 NULL。”

简而言之,在我们试图传递一个空的ContentValues以进行插入的情况下,SQLite 需要一些安全的列来分配NULL

或者,我们可以准备 SQL 语句并执行它,如下所示:

public void addRowAlternative(ContactModel contactObj) {

  String insertStatment = "INSERT INTO " + TABLE_NAME 
      + " ("
      + TABLE_ROW_NAME + ","
      + TABLE_ROW_PHONENUM + ","
      + TABLE_ROW_EMAIL + ","
      + TABLE_ROW_PHOTOID
      + ") "
      + " VALUES "
      + "(?,?,?,?)";

  SQLiteStatement s = db.compileStatement(insertStatment);
  s.bindString(1, contactObj.getName());
  s.bindString(2, contactObj.getContactNo());
  s.bindString(3, contactObj.getEmail());
if (contactObj.getPhoto() != null)
   {s.bindBlob(4, contactObj.getPhoto());}
  s.execute();
}

我们将涵盖这里提到的许多方法的替代方案。其目的是使您熟悉构建和执行查询的其他可能方式。替代部分的解释留作练习给您。getRowAsObject()方法将以ContactModel对象的形式返回从数据库中获取的行,如下面的代码所示。它将需要rowID作为参数,以唯一标识我们想要访问的表中的哪一行:

public ContactModel getRowAsObject(int rowID) { 
  ContactModel rowContactObj = new ContactModel();
  Cursor cursor;
  try {
    cursor = db.query(TABLE_NAME, new String[] {
TABLE_ROW_ID, TABLE_ROW_NAME, TABLE_ROW_PHONENUM, TABLE_ROW_EMAIL, TABLE_ROW_PHOTOID },
    TABLE_ROW_ID + "=" + rowID, null,
    null, null, null, null);
    cursor.moveToFirst();
    if (!cursor.isAfterLast()) {
      prepareSendObject(rowContactObj, cursor);    }
  } catch (SQLException e) {
      Log.e("DB ERROR", e.toString());
    e.printStackTrace();
  }
  return rowContactObj;
}

这个方法将以ContactModel对象的形式返回从数据库中获取的行。我们正在使用SQLiteDatabase()查询方法从我们的联系人表中根据提供的rowID参数获取行。该方法返回结果集上的游标:

public Cursor query (String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)

以下是上述代码的参数:

  • table:这表示将对其运行查询的数据库表。

  • columns:这是返回的列的列表;如果我们传递null,它将返回所有列。

  • selection:这是我们定义要返回哪些行的地方,并作为 SQL WHERE 子句。传递null将返回所有行。

  • selectionArgs:我们可以为这个参数传递null,或者我们可以在选择中包含问号,这些问号将被selectionArgs中的值替换。

  • groupBy:这是一个作为 SQL GROUP BY 子句的过滤器,声明如何对行进行分组。传递null将导致行不被分组。

  • Having:这是一个过滤器,告诉哪些行组应该成为游标的一部分,作为 SQL HAVING 子句。传递null将导致所有行组被包括。

  • OrderBy:这告诉查询如何对行进行排序,作为 SQL ORDER BY子句。传递null将使用默认排序顺序。

  • limit:这将限制查询返回的行数,作为LIMIT子句。传递null表示没有LIMIT子句。

这里另一个重要的概念是移动游标以访问数据。注意以下方法:cursor.moveToFirst()cursor.isAfterLast()cursor.moveToNext()

当我们尝试检索数据构建 SQL 查询语句时,数据库将首先创建游标对象的对象并返回其引用。返回的引用指针指向第 0 个位置,也称为游标的“第一个位置”之前。当我们想要检索数据时,我们必须首先移动到第一条记录;因此,使用cursor.moveToFirst()。谈到其他两种方法,cursor.isAfterLast()返回游标是否指向最后一行之后的位置,cursor.moveToNext()将游标移动到下一行。

提示

建议读者查看 Android 开发者网站上更多的游标方法:goo.gl/fR75t8

或者,我们可以使用以下方法:

public ContactModel getRowAsObjectAlternative(int rowID) {

  ContactModel rowContactObj = new ContactModel();
  Cursor cursor;

  try {
    String queryStatement = "SELECT * FROM " 
       + TABLE_NAME  + " WHERE " + TABLE_ROW_ID + "=?";
    cursor = db.rawQuery(queryStatement,
      new String[]{String.valueOf(rowID)});
    cursor.moveToFirst();

    rowContactObj = new ContactModel();
    rowContactObj.setId(cursor.getInt(0));
    prepareSendObject(rowContactObj, cursor);

  } catch (SQLException e) {
    Log.e("DB ERROR", e.toString());
    e.printStackTrace();
  }

  return rowContactObj;
}

update语句基于以下语法图:

构建插入查询

在我们转到datamanager类中的其他方法之前,让我们看一下在prepareSendObject()方法中从游标对象中获取数据:

rowObj.setContactNo(cursor.getString(cursor.getColumnIndexOrThrow(TABLE_ROW_PHONENUM)));
rowObj.setEmail(cursor.getString(cursor.getColumnIndexOrThrow(TABLE_ROW_EMAIL)));

这里cursor.getstring()以列索引作为参数,并返回请求列的值,而cursor.getColumnIndexOrThrow()以列名作为参数,并返回给定列名的基于零的索引。除了这种链接方法,我们可以直接使用cursor.getstring()。如果我们知道要从中提取数据的所需列的列号,我们可以使用以下表示法:

cursor.getstring(2);

构建删除查询

从我们的数据库表中删除特定的数据行,我们需要提供主键来唯一标识要删除的数据集:

public void deleteRow(int rowID) {
  try {
    db.delete(TABLE_NAME, TABLE_ROW_ID 
    + "=" + rowID, null);
  } catch (Exception e) {
    Log.e("DB ERROR", e.toString());
    e.printStackTrace();
  }
}

此方法使用 SQLiteDatabase 的delete()方法来删除表中给定 ID 的行:

public int delete (String table, String whereClause, String[] whereArgs)

以下是上述代码片段的参数:

  • table:这是要针对其运行查询的数据库表。

  • whereClause:这是在删除行时要应用的子句;在此子句中传递null将删除所有行

  • whereArgs:我们可以在where子句中包含问号,这些问号将被绑定为字符串的值

或者,我们可以使用以下方法:

public void deleteRowAlternative(int rowId) {

  String deleteStatement = "DELETE FROM " 
    + TABLE_NAME + " WHERE " 
    + TABLE_ROW_ID + "=?";
  SQLiteStatement s = db.compileStatement(deleteStatement);
  s.bindLong(1, rowId);
  s.executeUpdateDelete();
}

delete语句基于以下语法图:

构建删除查询

构建更新查询

要更新现有值,我们需要使用update()方法和所需的参数:

public void updateRow(int rowId, ContactModel contactObj) {

  ContentValues values = prepareData(contactObj);

  String whereClause = TABLE_ROW_ID + "=?";
  String whereArgs[] = new String[] {String.valueOf(rowId)};

  db.update(TABLE_NAME, values, whereClause, whereArgs);

}

通常情况下,我们需要主键,即rowId参数,来标识要修改的行。使用 SQLiteDatabase 的update()方法来修改数据库表中零行或多行的现有数据:

public int update (String table, ContentValues values, String whereClause, String[] whereArgs) 

以下是上述代码片段的参数:

  • table:这是要更新的合格数据库表名称。

  • values:这是从列名称到新列值的映射。

  • whereClause:这是在更新值/行时要应用的可选WHERE子句。如果UPDATE语句没有WHERE子句,则将修改表中的所有行。

  • whereArgs:我们可以在where子句中包含问号,这些问号将被绑定为字符串的值替换。

或者,您可以使用以下代码:

public void updateRowAlternative(int rowId, ContactModel contactObj) {
  String updateStatement = "UPDATE " + TABLE_NAME + " SET "
      + TABLE_ROW_NAME     + "=?,"
      + TABLE_ROW_PHONENUM + "=?,"
      + TABLE_ROW_EMAIL    + "=?,"
      + TABLE_ROW_PHOTOID  + "=?"
      + " WHERE " + TABLE_ROW_ID + "=?";

  SQLiteStatement s = db.compileStatement(updateStatement);
  s.bindString(1, contactObj.getName());
  s.bindString(2, contactObj.getContactNo());
  s.bindString(3, contactObj.getEmail());
  if (contactObj.getPhoto() != null)
   {s.bindBlob(4, contactObj.getPhoto());}
  s.bindLong(5, rowId);

  s.executeUpdateDelete();
}

update语句基于以下语法图:

构建更新查询

连接 UI 和数据库

既然我们已经在数据库中设置了钩子,让我们将我们的 UI 与数据连接起来:

  1. 第一步是从用户那里获取数据。我们可以通过内容提供程序使用 Android 联系人应用程序中的现有联系人数据。

我们将在下一章中介绍这种方法。现在,我们将要求用户添加一个新联系人,我们将把它插入到数据库中:

连接 UI 和数据库

  1. 我们正在使用标准的 Android UI 小部件,如EditTextTextViewButtons来收集用户提供的数据:
private void prepareSendData() {
  if (TextUtils.isEmpty(contactName.getText().toString())
      || TextUtils.isEmpty(
      contactPhone.getText().toString())) {

  .............

   } else {
    ContactModel contact = new ContactModel();
    contact.setName(contactName.getText().toString());
    ............

    DatabaseManager dm = new DatabaseManager(this);
    if(reqType == ContactsMainActivity
.CONTACT_UPDATE_REQ_CODE) {
      dm.updateRowAlternative(rowId, contact);
    } else {
      dm.addRowAlternative(contact);
    }

    setResult(RESULT_OK);
    finish();
  }
}

prepareSendData()是负责将数据打包到我们的对象模型中,然后将其插入到我们的数据库中的方法。请注意,我们使用TextUtils.isEmpty()而不是对contactName进行空值检查和长度检查,这是一个非常方便的方法。如果字符串为 null 或长度为零,则返回true

  1. 我们从用户填写表单接收的数据准备我们的ContactModel对象。我们创建我们的DatabaseManager类的一个实例,并访问我们的addRow()方法,将我们的联系对象传递给数据库中插入,正如我们之前讨论的那样。

另一个重要的方法是getBlob(),它用于以 BLOB 格式获取图像数据:

private byte[] getBlob() {

  ByteArrayOutputStream blob = new ByteArrayOutputStream();
  imageBitmap.compress(Bitmap.CompressFormat.JPEG, 100, blob);
  byte[] byteArray = blob.toByteArray();

  return byteArray;
}
  1. 我们创建一个新的ByteArrayOutputStream对象blob。位图的compress()方法将用于将位图的压缩版本写入我们的outputstream对象:
public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)

以下是上述代码的参数:

  • format:这是压缩图像的格式,在我们的情况下是 JPEG。

  • quality:这是对压缩器的提示,范围从0100。值0表示压缩到较小的尺寸和低质量,而100是最高质量。

  • stream:这是用于写入压缩数据的输出流。

  1. 然后,我们创建我们的byte[]对象,它将从ByteArrayOutputStream toByteArray()方法构造。

注意

您会注意到我们并没有涵盖所有的方法;只有与数据操作相关的方法以及可能引起混淆的一些方法或调用。还有一些用于调用相机或画廊以选择要用作联系人图像的照片的方法。建议您探索随书提供的代码中的方法。

让我们继续到演示部分,在那里我们使用自定义 listview 以一种可呈现和可读的方式显示我们的联系人信息。我们将跳过与演示相关的大部分代码,集中在我们获取和提供数据给我们的 listview 的部分。我们还将实现上下文菜单,以便为用户提供删除特定联系人的功能。我们将涉及数据库管理器方法,如getAllData()来获取所有添加的联系人。我们将使用deleteRow()来从我们的联系人数据库中删除任何不需要的联系人。最终结果将类似于以下截图:

连接 UI 和数据库

  1. 为了创建一个类似于前面截图中显示的自定义 listview,我们创建CustomListAdapter扩展BaseAdapter并使用自定义布局来设置 listview 行。请注意,在以下构造函数中,我们已经初始化了一个新的数组列表,并将使用我们的数据库管理器通过使用getAllData()方法来获取所有数据库条目的值:
public CustomListAdapter(Context context) {

   contactModelList = new ArrayList<ContactModel>();
   _context = context;
   inflater = (LayoutInflater)context.getSystemService( 
Context.LAYOUT_INFLATER_SERVICE);
      dm = new DatabaseManager(_context);
   contactModelList = dm.getAllData();
}

另一个非常重要的方法是getView()方法。这是我们在视图中填充自定义布局的地方:

convertView = inflater.inflate(R.layout.contact_list_row, null);

我们将使用视图持有者模式来提高 listview 的滚动流畅性:

vHolder = (ViewHolder) convertView.getTag();
  1. 最后,将数据设置到相应的视图中:
vHolder.contact_email.setText(contactObj.getEmail());

注意

在视图持有者中持有视图对象可以通过减少对findViewById()的调用来提高性能。您可以在developer.android.com/training/improving-layouts/smooth-scrolling.html上阅读更多关于此的信息以及如何使 listview 滚动流畅。

  1. 我们还将实现一种删除 listview 条目的方法。我们将使用上下文菜单来实现这一目的。我们将首先在应用程序结构的res文件夹下的menu文件夹中创建一个菜单项:
<?xml version="1.0" encoding="utf-8"?>
<menu  >

    <item
        android:id="@+id/delete_item"
        android:title="Delete"/>
<item
        android:id="@+id/update_item"
     android:title="Update"/>
</menu>
  1. 现在,在我们将显示 listview 的主要活动中,我们将使用以下调用来注册我们的 listview 到上下文菜单。为了启动上下文菜单,我们需要在 listview 项上执行长按操作:
registerForContextMenu(listReminder) 
  1. 还有一些方法我们需要实现以实现删除功能:
@Override
  public void onCreateContextMenu(ContextMenu menu, View v,
      ContextMenuInfo menuInfo) {
    super.onCreateContextMenu(menu, v, menuInfo);
    MenuInflater m = getMenuInflater();
    m.inflate(R.menu.del_menu, menu);
  }

这种方法用于用我们之前在 XML 中定义的菜单填充上下文菜单。MenuInfater类从菜单 XML 文件生成菜单对象。菜单膨胀在很大程度上依赖于在构建时对 XML 文件的预处理;这是为了提高性能而做的。

  1. 现在,我们将实现一种捕获上下文菜单点击的方法:
  @Override
  public boolean onContextItemSelected(MenuItem item) {
..............
    case R.id.delete_item:

      cAdapter.delRow(info.position);
      cAdapter.notifyDataSetChanged();
      return true;
    case R.id.update_item:

      Intent intent = new Intent( 
ContactsMainActivity.this, AddNewContactActivity.class);
      ......................
  }
  1. 在这里,我们将找到点击的 listview 项的位置 ID,并调用 CustomListAdapter 的delRow()方法,最后,我们将通知适配器数据集已更改:
public void delRow(int delPosition) {
                  dm.deleteRowAlternative(contactModelList.get(delPosition).getId());
       contactModelList.remove(delPosition);

delRow()方法负责将我们数据库的deleteRowAlternative()方法连接到我们上下文菜单的delete()方法。在这里,我们获取设置在特定 listview 项上的对象的 ID,并将其传递给databaseManagerdeleteRowAlternative()方法,以从数据库中删除数据。在从数据库中删除数据后,我们将指示我们的 listview 从我们的联系人列表中删除相应的条目。

onContextItemSelected()方法中,我们还可以看到update_item,以防用户点击了update按钮。我们将启动添加新联系人的活动,并在用户想要编辑某些字段时添加我们已经拥有的数据。关键是要知道调用是从哪里发起的。是要添加新条目还是更新现有条目?我们借助以下代码告诉活动,此操作用于更新而不是添加新条目:

intent.putExtra(REQ_TYPE, CONTACT_UPDATE_REQ_CODE);

摘要

在本章中,我们涵盖了构建基于数据库的应用程序的步骤,从头开始,然后从模式到对象模型,然后从对象模型到构建实际数据库。我们经历了构建数据库管理器的过程,最终实现了 UI 数据库连接,实现了一个完全功能的应用程序。涵盖的主题包括模型类的构建块,数据库模式到数据库处理程序和 CRUD 方法。我们还涵盖了将数据库连接到 Android 视图的重要概念,并在适当的位置设置钩子以获取用户数据,将数据添加到数据库,并在从数据库中获取数据后显示相关信息。

在下一章中,我们将专注于在这里所做的基础上构建。我们将探索ContentProviders。我们还将学习如何从ContentProviders获取数据,如何制作我们自己的内容提供程序,以及在构建它们时涉及的最佳实践等等。

第三章:分享就是关怀

"数据真的驱动着我们所做的一切。"
--– Jeff Weiner, LinkedIn

在上一章中,我们开始编写我们自己的联系人管理器。我们遇到了数据库中心应用程序的各种构建模块;我们涵盖了数据库处理程序和构建查询,以便从我们的数据库中获取有意义的数据。我们还探讨了如何在我们的 UI 和数据库之间建立连接,并以一种可消费的方式呈现给最终用户。

在这一章中,我们将学习如何通过内容提供程序访问其他应用程序的数据。我们还将学习如何构建自己的内容提供程序,以便与其他应用程序共享我们的数据。我们将研究 Android 的提供者,如contactprovider。最后,我们将构建一个测试应用程序来使用我们新构建的内容提供程序。

在本章中,我们将涵盖以下主题:

  • 什么是内容提供程序?

  • 创建内容提供程序

  • 实现核心方法

  • 使用内容提供程序

什么是内容提供程序?

内容提供程序是 Android 应用程序的第四个组件。它用于管理对结构化数据集的访问。内容提供程序封装数据,并提供抽象和定义数据安全性的机制。然而,内容提供程序主要用于被其他应用程序使用,这些应用程序使用提供程序的客户端对象访问提供程序。提供程序和提供程序客户端一起为数据提供了一致的标准接口,还处理了进程间通信和安全数据访问。

内容提供程序允许一个应用程序与其他应用程序共享数据。按设计,由应用程序创建的 Android SQLite 数据库对应用程序是私有的;从安全的角度来看,这是很好的,但当你想要在不同的应用程序之间共享数据时会很麻烦。这就是内容提供程序发挥作用的地方;通过构建自己的内容提供程序,您可以轻松地共享数据。重要的是要注意,尽管我们的讨论将集中在数据库上,但内容提供程序并不局限于此。它也可以用来提供通常存储在文件中的文件数据,如照片、音频或视频:

什么是内容提供程序?

在上图中,注意应用程序 A 和 B 之间交换数据的交互方式。在这里,我们有一个应用程序 A,其活动需要访问应用程序 B的数据库。正如我们已经看到的,应用程序 B的数据库存储在内部存储器中,无法直接被应用程序 A访问。这就是内容提供程序出现的地方;它允许我们共享数据并修改对其他应用程序的访问。内容提供程序实现了查询、插入、更新和删除数据库中的数据的方法。应用程序 A现在请求内容提供程序代表它执行一些所需的操作。我们将探索这个问题的两面,但我们将首先使用内容提供程序从手机的联系人数据库中获取联系人,然后我们将构建我们自己的内容提供程序,供其他人从我们的数据库中获取数据。

使用现有内容提供程序

Android 列出了许多标准内容提供程序,我们可以使用。其中一些是BrowserCalendarContractCallLogContactsContactsContractMediaStoreuserDictionary等。

在我们当前的联系人管理应用程序中,我们将添加一个新功能。在AddNewContactActivity类的 UI 中,我们将添加一个小按钮,以帮助系统的现有ContentProviderContentResolver提供程序从手机的联系人列表中获取联系人。我们将使用ContactsContract提供程序来实现这个目的。

什么是内容解析器?

应用程序上下文中的ContentResolver对象用于作为客户端与提供程序进行通信。ContentResolver对象与提供程序对象通信——提供程序对象是实现ContentProvider的类的实例。提供程序对象接收来自客户端的数据请求,执行请求的操作,并返回结果。

ContentResolver是我们应用程序中的一个单一的全局实例,它提供了对其他应用程序的内容提供程序的访问;我们不需要担心处理进程间通信。ContentResolver方法提供了持久存储的基本 CRUD(创建、检索、更新和删除)功能;它有调用提供程序对象中同名方法的方法,但不知道实现。随着我们在本章中的进展,我们将更详细地介绍ContentResolver

什么是内容解析器?

在前面的屏幕截图中,注意右侧的新图标,可以直接从手机联系人中添加联系人;我们修改了现有的 XML 以添加这个图标。相应的类AddNewContactActivity也将被修改:

public void pickContact() {
   try {
       Intent cIntent = new Intent(Intent.ACTION_PICK,
            ContactsContract.Contacts.CONTENT_URI);
      startActivityForResult(cIntent, PICK_CONTACT);
    } catch (Exception e) {
      e.printStackTrace();
      Log.i(TAG, "Exception while picking contact");
    }
   }

我们添加了一个新的方法pickContact()来准备一个意图以选择联系人。Intent.ACTION_PICK允许我们从数据源中选择一个项目;此外,我们只需要知道提供程序的统一资源标识符URI),在我们的情况下是ContactsContract.Contacts.CONTENT_URI。这个功能也由消息、画廊和联系人提供。如果您查看第二章的代码,连接点,您会发现我们已经使用了相同的代码从画廊中选择图像。联系人屏幕将弹出,允许我们浏览或搜索我们需要迁移到我们的新应用程序的联系人。注意onActivityResult,也就是说,我们的下一站我们将修改这个方法来处理我们对联系人的相应请求。让我们看看我们需要添加的代码,以从 Android 的联系人提供程序中选择联系人:

{
.
.
.

else if (requestCode == PICK_CONTACT) {
      if (resultCode == Activity.RESULT_OK)

       {
          Uri contactData = data.getData();
          Cursor c = getContentResolver().query(contactData, null, null, null, null);
         if (c.moveToFirst()) {
             String id = c
                   .getString(c
                         .getColumnIndexOrThrow(ContactsContract.Contacts._ID));

             String hasPhone = c
                   .getString(c
                         .getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER));

            if (hasPhone.equalsIgnoreCase("1")) {
                Cursor phones = getContentResolver()
                      .query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                           null,
                           ContactsContract.CommonDataKinds.Phone.CONTACT_ID
                                  + " = " + id, null, null);
               phones.moveToFirst();
               contactPhone.setText(phones.getString(phones
                      .getColumnIndex("data1")));

               contactName
                      .setText(phones.getString(phones
                            .getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)));

 }
…..

提示

为了为您的应用程序增添一些特色,可以从 Android 开发者网站goo.gl/4Msuct下载整套模板、源代码、操作栏图标包、颜色样本和 Roboto 字体系列。设计一个功能性应用程序是不完整的,如果没有遵循 Android 指南的一致 UI。

我们首先检查请求代码是否与我们的匹配。然后,我们交叉检查resultcode。我们通过在Context对象上调用getcontentresolver来获取ContentResolver对象;这是android.content.Context类的一个方法。由于我们在一个继承自Context的活动中,我们不需要显式地调用它。服务也是一样。现在我们将验证我们选择的联系人是否有电话号码。在验证必要的细节之后,我们提取我们需要的数据,比如联系人姓名和电话号码,并将它们设置在相关字段中。

创建内容提供程序

内容提供程序以两种方式提供数据访问:一种是以数据库的形式进行结构化数据,就像我们目前正在处理的例子一样,或者以文件数据的形式,也就是说,以图片、音频、视频等形式存储在应用程序的私有空间中。在我们开始深入研究如何创建内容提供程序之前,我们还应该回顾一下我们是否需要一个。如果我们想要向其他应用程序提供数据,允许用户从我们的应用程序复制数据到另一个应用程序,或者在我们的应用程序中使用搜索框架,那么答案就是肯定的。

就像其他 Android 组件(ActivityServiceBroadcastReceiver)一样,内容提供程序是通过扩展ContentProvider类来创建的。由于ContentProvider是一个抽象类,我们必须实现这六个抽象方法。这些方法如下:

方法 用法
void onCreate() 初始化提供程序
String getType(Uri) 返回内容提供程序中数据的 MIME 类型
int delete(Uri uri, String selection, String[] selectionArgs) 从内容提供程序中删除数据
Uri insert(Uri uri, ContentValues values) 将新数据插入内容提供程序
Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 返回数据给调用者
int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) 更新内容提供程序中的现有数据

随着我们在本章中的进展和应用程序的构建,这些方法将在以后更详细地讨论。

理解内容 URI

ContentProvider的每个数据访问方法都有一个内容 URI 作为参数,允许它确定要访问的表、行或文件。它通常遵循以下结构:

content://authority/Path/Id

让我们分析content:// URI 组件的分解。内容提供程序的方案始终是content。冒号和双斜杠(://)充当与权限部分的分隔符。然后,我们有authority部分。根据规则,每个内容提供程序的权限都必须是唯一的。Android 文档推荐使用的命名约定是内容提供程序子类的完全限定类名。通常,它是一个包名称加上我们发布的每个内容提供程序的限定符。

剩下的部分是可选的,也称为path,用于区分内容提供程序可以提供的不同类型的数据。一个很好的例子是MediaStore提供程序,它需要区分音频、视频和图像文件。

另一个可选部分是id,它指向特定记录;根据id是否存在,URI 分别成为基于 ID 或基于目录。另一种理解方式是,基于 ID 的 URI 使我们能够在行级别单独与数据交互,而基于目录的 URI 使我们能够与数据库的多行交互。

例如,考虑content://com.personalcontactmanager.provider/contacts;随着我们继续本章的进展,我们很快就会遇到这个。

注意

顺便说一句,应用程序的包名称应始终是唯一的;这是因为 Play 商店上的所有应用程序都是通过其包名称进行识别的。Play 商店上的应用程序的所有更新都需要具有相同的包名称,并且必须使用最初使用的相同密钥库进行签名。例如,以下是 Gmail 应用程序的 Play 商店链接;请注意,在 URL 的末尾,我们将找到应用程序的包名称:

play.google.com/store/apps/details?id=com.google.android.gm

声明我们的合同类

声明合同是构建内容提供程序的非常重要的部分。这个类,正如其名称所示,将充当我们的内容提供程序和将要访问我们的内容提供程序的应用程序之间的合同。它是一个public final类,其中包含 URI、列名和其他元数据的常量定义。它也可以包含 Javadoc,但最大的优势是使用它的开发人员不需要担心表的名称、列和常量的名称,从而减少了容易出错的代码。

合同类为我们提供了必要的抽象;我们可以根据需要更改底层操作,也可以更改影响其他依赖应用程序的相应数据操作。需要注意的一点是,我们在未来更改合同时需要小心;如果我们不小心,可能会破坏使用我们合同类的其他应用程序。

我们的合同类看起来像下面这样:

public final class PersonalContactContract {

   /**
    * The authority of the PersonalContactProvider
    */
   public static final String AUTHORITY = "com.personalcontactmanager.provider";

   public static final String BASE_PATH = "contacts";

   /**
    * The Uri for the top-level PersonalContactProvider
    * authority
    */
   public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY 
         + "/" + BASE_PATH);

   /**
    * The mime type of a directory of items.
    */
   public static final String CONTENT_TYPE =                  
ContentResolver.CURSOR_DIR_BASE_TYPE + 
                  "/vnd.com.personalcontactmanager.provider.table";
   /**
    * The mime type of a single item.
    */
   public static final String CONTENT_ITEM_TYPE = 
ContentResolver.CURSOR_ITEM_BASE_TYPE + 
                 "/vnd.com.personalcontactmanager.provider.table_item";

   /**
    * A projection of all columns 
    * in the items table.
    */
   public static final String[] PROJECTION_ALL = { "_id", 
      "contact_name", "contact_number", 
      "contact_email", "photo_id" };

   /**
    * The default sort order for 
    * queries containing NAME fields.
    */
   //public static final String SORT_ORDER_DEFAULT = NAME + " ASC";

   public static final class Columns {
      public static String TABLE_ROW_ID = "_id";
      public static String TABLE_ROW_NAME  = "contact_name";
      public static String TABLE_ROW_PHONENUM = "contact_number";
      public static String TABLE_ROW_EMAIL = "contact_email";
      public static String TABLE_ROW_PHOTOID = "photo_id";
   }
}

AUTHORITY是在 Android 系统中注册的许多其他提供程序中标识提供程序的符号名称。BASE_PATH是表的路径。CONTENT_URI是提供程序封装的表的 URI。CONTENT_TYPE是包含零个或多个项目的游标的内容 URI 的 Android 平台的基本 MIME 类型。CONTENT_ITEM_TYPE是包含单个项目的游标的内容 URI 的 Android 平台的基本 MIME 类型。PROJECTION_ALLColumns包含表的列 ID。

没有这些信息,其他开发人员将无法访问您的提供程序,即使它是开放访问的。

注意

提供程序内部可能有许多表,每个表都应该有一个唯一的路径;路径不是真正的物理路径,而是一个标识符。

创建 UriMatcher 定义

UriMatcher是一个实用类,它帮助匹配内容提供程序中的 URI。addURI()方法接受提供程序应该识别的内容 URI 模式。我们添加一个要匹配的 URI,以及在匹配此 URI 时返回的代码:

addURI(String authority, String path, int code)

我们将authoritypath模式和整数值传递给UriMatcheraddURI()方法;当我们尝试匹配模式时,它返回我们定义的常量作为int值。

我们的UriMatcher看起来像下面这样:

private static final int CONTACTS_TABLE = 1;
private static final int CONTACTS_TABLE_ITEM = 2;

private static final UriMatcher mmURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
   static {
      mmURIMatcher.addURI(PersonalContactContract.AUTHORITY, 
            PersonalContactContract.BASE_PATH, CONTACTS_TABLE);
      mmURIMatcher.addURI(PersonalContactContract.AUTHORITY, 
            PersonalContactContract.BASE_PATH+  "/#",  
                       CONTACTS_TABLE_ITEM);
   }

请注意,它还支持使用通配符;我们在前面的代码片段中使用了井号(#),我们也可以使用通配符,比如*。在我们的情况下,使用井号,"content://com.personalcontactmanager.provider/contacts/2"这个表达式匹配,但使用* "content://com.personalcontactmanager.provider/contacts就不匹配了。

实现核心方法

为了构建我们的内容提供程序,下一步将是准备我们的核心数据库访问和数据修改方法,也就是 CRUD 方法。这是我们希望根据接收到的插入、查询或删除调用与数据交互的核心逻辑所在。我们还将实现 Android 架构的生命周期方法,比如onCreate()

通过 onCreate()方法初始化提供程序

我们在onCreate()中创建我们的数据库管理器类的对象。oncreate()中应该有最少的操作,因为它在主 UI 线程上运行,可能会导致某些用户的延迟。最好避免在oncreate()中进行长时间运行的任务,因为这会增加提供程序的启动时间。甚至建议将数据库创建和数据加载推迟到我们的提供程序实际收到对数据的请求时,也就是将持续时间长的操作移到 CRUD 方法中:

@Override
Public Boolean onCreate() {
   dbm = new DatabaseManager(getContext());
   return false;
}   

通过 query()方法查询记录

query()方法将返回结果集上的游标。将 URI 传递给我们的UriMatcher,以查看它是否与我们之前定义的任何模式匹配。在我们的 switch case 语句中,如果是与表项相关的情况,我们检查selection语句是否为空;如果是,我们将选择语句构建到lastpathsegment,否则我们将选择附加到lastpathsegment语句。我们使用DatabaseManager对象在数据库上运行查询,并得到一个游标作为结果。query()方法预期会抛出IllegalArgumentException来通知未知的 URI;在查询过程中遇到内部错误时,抛出nullPointerException也是一个良好的做法:

@Override
public Cursor query(Uri uri, String[] projection, String selection,
      String[] selectionArgs, String sortOrder) {

   int uriType = mmURIMatcher.match(uri);
   switch(uriType) {

   case CONTACTS_TABLE:
      break;
   case CONTACTS_TABLE_ITEM:
      if (TextUtils.isEmpty(selection)) {
         selection = PersonalContactContract.Columns.TABLE_ROW_ID 
                  + "=" + uri.getLastPathSegment();
      } else {
         selection = PersonalContactContract.Columns.TABLE_ROW_ID 
                  + "=" + uri.getLastPathSegment() + 
               " and " + selection;
      }
      break;
   default:
      throw new IllegalArgumentException("Unknown URI: " + uri);
   }

   Cursor cr = dbm.getRowAsCursor(projection, selection, 
               selectionArgs, sortOrder);

   return cr;
}

注意

请记住,Android 系统必须能够跨进程边界通信异常。Android 可以为以下异常执行此操作,这些异常在处理查询错误时可能有用:

  • IllegalArgumentException:如果您的提供程序收到无效的内容 URI,您可以选择抛出此异常

  • NullPointerException:当对象为空且我们尝试访问其字段或方法时抛出

通过 insert()方法添加记录

正如其名称所示,insert()方法用于在我们的数据库中插入一个值。它返回插入行的 URI,并且在检查 URI 时,我们需要记住插入可以发生在表级别,因此方法中的操作在与表匹配的 URI 上进行处理。匹配后,我们使用标准的DatabaseManager对象将新值插入到数据库中。新行的内容 URI 是通过将新行的_ID值附加到表的内容 URI 构造的:

@Override
public Uri insert(Uri uri, ContentValues values) {

   int uriType = mmURIMatcher.match(uri);
   long id;

   switch(uriType) {
   case CONTACTS_TABLE:
      id = dbm.addRow(values);
      break;
   default:
      throw new IllegalArgumentException("Unknown URI: " + uri);
   }

   Uri ur = ContentUris.withAppendedId(uri, id);
   return ur;
}

通过 update()方法更新记录

update()方法更新适当表中的现有行,使用ContentValues参数中的值。首先,我们确定 URI,无论是基于目录还是基于 ID,然后我们构建选择语句,就像我们在query()方法中所做的那样。现在,我们将执行我们在第二章中构建此应用程序时定义的标准DatabaseManagerupdateRow()方法,该方法返回受影响的行数。

update()方法返回更新的行数。根据选择条件,可以更新一行或多行:

@Override
public int update(Uri uri, ContentValues values, String selection,
      String[] selectionArgs) {
   int uriType = mmURIMatcher.match(uri);

   switch(uriType) {
   case CONTACTS_TABLE:
      break;
   case CONTACTS_TABLE_ITEM:
      if (TextUtils.isEmpty(selection)) {
         selection = PersonalContactContract.Columns.TABLE_ROW_ID
 + "=" + uri.getLastPathSegment();
      } else {
         selection = PersonalContactContract.Columns.TABLE_ROW_ID 
+ "=" + uri.getLastPathSegment() 
+ " and " + selection;
      }
      break;
   default:
      throw new IllegalArgumentException("Unknown URI: " + uri);
   }

   int count = dbm.updateRow(values, selection, selectionArgs);

   return count;
}

通过 delete()方法删除记录

delete()方法与update()方法非常相似,使用它的过程类似;在这里,调用是用来删除一行而不是更新它。delete()方法返回删除的行数。根据选择条件,可以删除一行或多行:

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {

   int uriType = mmURIMatcher.match(uri);

   switch(uriType) {
   case CONTACTS_TABLE:
      break;
   case CONTACTS_TABLE_ITEM:
      if (TextUtils.isEmpty(selection)) {
         selection = PersonalContactContract.Columns.TABLE_ROW_ID
 + "=" + uri.getLastPathSegment();
      } else {
         selection = PersonalContactContract.Columns.TABLE_ROW_ID 
 + "=" + uri.getLastPathSegment() 
 + " and " + selection;
      }
      break;
   default:
      throw new IllegalArgumentException("Unknown URI: " + uri);
   }

   int count = dbm.deleteRow(selection, selectionArgs);

   return count;
}

通过 getType()方法获取数据的返回类型

这个简单方法的签名接受一个 URI 并返回一个字符串值;每个内容提供者必须为其支持的 URI 返回内容类型。一个非常有趣的事实是,应用程序访问这些信息时不需要任何权限;如果我们的内容提供者需要权限,或者没有被导出,所有的应用程序仍然可以调用这个方法,而不管它们对检索 MIME 类型的访问权限如何。

所有这些 MIME 类型都应在合同类中声明:

@Override
public String getType(Uri uri) {

   int uriType = mmURIMatcher.match(uri);
   switch(uriType) {
   case CONTACTS_TABLE:
      return PersonalContactContract.CONTENT_TYPE;
   case CONTACTS_TABLE_ITEM:
      return PersonalContactContract.CONTENT_ITEM_TYPE;
   default:
      throw new IllegalArgumentException("Unknown URI: " + uri);   
   }

}

将提供者添加到清单中

另一个重要的步骤是将我们的内容提供者添加到清单中,就像我们对其他 Android 组件所做的那样。我们可以在这里注册多个提供者。这里的重要部分,除了android:authorities之外,还有android:exported;它定义了内容提供者是否可供其他应用程序使用。如果为true,则提供者可供其他应用程序使用;如果为false,则提供者不可供其他应用程序使用。如果应用程序具有与提供者相同的用户 ID(UID),它们将可以访问它:

<provider
   android:name="com.personalcontactmanager.provider.PersonalContactProvider"
   android:authorities="com.personalcontactmanager.provider"
   android:exported="true"
   android:grantUriPermissions="true" >
   </provider>

另一个重要的概念是权限。我们可以通过添加读取和写入权限来增加额外的安全性,其他应用程序必须在其清单 XML 文件中添加这些权限,并自动通知用户他们将要使用特定应用程序的内容提供者来读取、写入或两者兼而有之。我们可以通过以下方式添加权限:

android:readPermission="com.personalcontactmanager.provider.READ"

使用内容提供者

我们构建内容提供者的主要原因是允许其他应用程序访问我们数据库中的复杂数据存储并执行 CRUD 操作。现在我们将构建另一个应用程序来测试我们新构建的内容提供者。测试应用程序非常简单,只包括一个活动类和一个布局文件。它有标准按钮来执行操作。没有花哨的东西,只是用来测试我们刚刚实现的功能的工具。现在我们将深入研究TestMainActivity类并查看其实现:

public class TestMainActivity extends Activity {

public final String AUTHORITY = "com.personalcontactmanager.provider";
public final String BASE_PATH = "contacts";
private TextViewqueryT, insertT;

public class Columns {
   public final static String TABLE_ROW_ID = "_id";
   public final static String TABLE_ROW_NAME = "contact_name";
   public final static String TABLE_ROW_PHONENUM =

"contact_number";
   public final static String TABLE_ROW_EMAIL = "contact_email";
   public final static String TABLE_ROW_PHOTOID = "photo_id";
   }

要访问内容提供程序,我们需要诸如AUTHORITYBASE_PATH的详细信息,以及数据库表的列名称;我们需要访问公共类Columns。为此目的。我们有更多的表,我们将看到更多这些类。通常,所有这些必要的信息将从内容提供程序的已发布合同类中获取。一些内容提供程序还需要在清单中实现读取或写入权限:

<uses-permissionandroid:name="AUTHORITY.permission.WRITE_TASKS"/>

在某些情况下,我们需要访问的内容提供程序可能会要求我们在清单中添加权限。当用户安装应用程序时,他们将在其权限列表中看到一个添加的权限:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_test_main);
   queryT = (TextView) findViewById(R.id.textQuery);
   insertT = (TextView) findViewById(R.id.textInsert);
   }

注意

要尝试其他应用程序的内容提供程序,请参阅goo.gl/NEX2hN

它列出了如何使用 Any.do 的内容提供程序-一个非常著名的任务应用程序。

我们将在活动的onCreate()中设置我们的布局并初始化我们需要的视图。要查询,我们首先需要准备与表匹配的 URI 对象。

现在内容解析器开始发挥作用;它充当我们准备的内容 URI 的解析器。在这种情况下,我们的getContentResolver.query()方法将获取所有列和行。现在,我们将游标移动到第一个位置,以便读取结果。出于测试目的,它被读取为一个字符串:

public void query(View v) {
  Uri contentUri = Uri.parse("content://" + AUTHORITY 
               + "/" + BASE_PATH);

  Cursor cr = getContentResolver().query(contentUri, null, 
            null, null, null);     

  if (cr != null) {
      if (cr.getCount() > 0) {
         cr.moveToFirst();
         String name = cr.getString(cr.getColumnIndexOrThrow( 
Columns.TABLE_ROW_NAME));
         queryT.setText(name);
      }
  }

  ....
  ....
}

现在,我们构建一个 URI 来读取特定行,而不是完整的表。我们已经提到,为了使 URI 基于 ID,我们需要将 ID 部分添加到我们现有的contenturi中。现在,我们构建我们的投影字符串数组,以作为我们query()方法中的参数传递:

public void query(View v) {

 ...
 ...

  Uri rowUri = contentUri = ContentUris.withAppendedId
            (contentUri, getFirstRowId());

  String[] projection = new String[] {
      Columns.TABLE_ROW_NAME, Columns.TABLE_ROW_PHONENUM,
      Columns.TABLE_ROW_EMAIL, Columns.TABLE_ROW_PHOTOID };

  cr = getContentResolver().query(contentUri, projection,
      null, null, null);

  if (cr != null) {
      if (cr.getCount() > 0) {
         cr.moveToFirst();
         String name = cr.getString(cr.getColumnIndexOrThrow(
                  Columns.TABLE_ROW_NAME));

         queryT.setText(name);

      }
  }

}   

getFirstRowId()方法获取表中第一行的 ID。这是因为第一行的 ID 并不总是1。当行被删除时,它会发生变化。如果具有行 ID1的表中的第一项被删除,那么具有行 ID1的第二项将成为第一项:

private int getFirstRowId() {

  int id = 1;
  Uri contentUri = Uri.parse("content://" + AUTHORITY + "/"
               + "contacts");
  Cursor cr = getContentResolver().query(contentUri, null,
            null, null, null);
  if (cr != null) {
      if (cr.getCount() > 0) {
         cr.moveToFirst();
         id = cr.getInt(cr.getColumnIndexOrThrow(
            Columns.TABLE_ROW_ID));
      }
  }
return id;

}

让我们更仔细地看一下query()方法:

public final Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

在 API 级别 1 中,query()方法根据我们提供的参数返回结果集上的游标。以下是前面代码的参数:

  • uri:这是我们的情况下的contentURI,使用content://方案来检索内容。它可以基于 ID 或基于目录。

  • projection:这是要返回的列的列表,我们已经使用列名准备好了。传递null将返回所有列。

  • selection:格式化为 SQL WHERE子句,不包括WHERE本身,这充当一个过滤器,声明要返回哪些行。

  • selectionArgs:我们可以在selection中包含?参数标记。Android SQL 查询构建器将使用从selectionArgs绑定为字符串的值替换?参数标记,按照它们在selection中出现的顺序。

  • sortOrder:这告诉我们如何对行进行排序,格式化为 SQL ORDER BY子句。null值将使用默认排序顺序。

注意

根据官方文档,我们应该遵循一些指导方针以获得最佳性能:

  • 提供明确的投影,以防止从存储中读取不会使用的数据。

  • 在选择参数中使用问号参数标记,例如phone=?,而不是显式值,以便仅由这些值不同的查询将被识别为相同以进行缓存。

我们之前使用的相同过程用于检查null值和空游标,并最终从游标中提取所需的值。

现在,让我们看一下我们测试应用程序的insert方法。

我们首先构建我们的内容值对象和相关的键值对,例如,在相关的Columns.TABLE_ROW_PHONENUM字段中放入电话号码。请注意,因为诸如列名之类的细节以类的形式与我们共享,所以我们不需要担心实际的列名等细节。我们只需要通过Columns类的方式访问它。这确保我们只需要更新相关的值。如果将来内容提供程序发生某些更改并更改表名,其余功能和实现仍然保持不变。我们像之前在查询内容提供程序数据的情况下一样,构建我们所需的列名的投影字符串数组。

我们还构建我们的内容 URI;请注意,它与表匹配,而不是单独的行。insert()方法也返回一个 URI,不像query()方法返回结果集上的游标:

public void insert(View v) {

  String name = getRandomName();
  String number = getRandomNumber();

  ContentValues values = new ContentValues();
  values.put(Columns.TABLE_ROW_NAME, name);
  values.put(Columns.TABLE_ROW_PHONENUM, number);
  values.put(Columns.TABLE_ROW_EMAIL, name + "@gmail.com");
  values.put(Columns.TABLE_ROW_PHOTOID, "abc");

  String[] projection = new String[] {
      Columns.TABLE_ROW_NAME, Columns.TABLE_ROW_PHONENUM,
      Columns.TABLE_ROW_EMAIL, Columns.TABLE_ROW_PHOTOID };

  Uri contentUri = Uri.parse("content://" + AUTHORITY + "/"
            + BASE_PATH);

  Uri insertedRowUri = getContentResolver().insert(
            contentUri, values);

  //checking the added row
  Cursor cr = getContentResolver().query(insertedRowUri,
         projection, null, null, null);

  if (cr != null) {
      if (cr.getCount() > 0) {
           cr.moveToFirst();
           name = cr.getString(cr.getColumnIndexOrThrow(
               Columns.TABLE_ROW_NAME));
           insertT.setText(name);
      }
  }

}

getRandomName()getRandomNumber()方法生成要插入表中的随机名称和数字:

private String getRandomName() {

      Random rand = new Random();
      String name = "" + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26))
         + (char) (122-rand.nextInt(26)) ;

      return name;
}

public String getRandomNumber() {
  Random rand = new Random();
  String number = rand.nextInt(98989)*rand.nextInt(59595)+"";

  return number;
}

让我们更仔细地看看insert()方法:

public final Uri insert (Uri url, ContentValues values)

以下是上一行代码的参数:

  • url:要插入数据的表的 URL

  • values:以ContentValues对象的形式为新插入的行的值,键是字段的列名

请注意,在插入后,我们再次运行了query()方法,使用了insert()方法返回的 URI。我们运行这个查询是为了看到我们打算插入的值是否已经插入;这个查询将根据附加了 ID 的行的投影返回列。

到目前为止,我们已经涵盖了query()insert()方法;现在,我们将涵盖update()方法。

我们在insert()方法中通过准备ContentValues对象来进行了进展。类似地,我们将准备一个对象,我们将在ContentResolverupdate()方法中使用来更新现有行。在这种情况下,我们将构建我们的 URI 直到 ID,因为这个操作是基于 ID 的。更新由rowUri对象指向的行,它将返回更新的行数,这将与 URI 相同;在这种情况下,它是指向单个行的rowUri。另一种方法可能是使用指向表的contentUriselection/selectionArgs的组合。在这种情况下,根据selection子句,更新的行可能多于一个:

public void update(View v) {

  String name = getRandomName();
  String number = getRandomNumber();

  ContentValues values = new ContentValues();
  values.put(Columns.TABLE_ROW_NAME, name);
  values.put(Columns.TABLE_ROW_PHONENUM, number);
  values.put(Columns.TABLE_ROW_EMAIL, name + "@gmail.com");
  values.put(Columns.TABLE_ROW_PHOTOID, " ");

  Uri contentUri = Uri.parse("content://" + AUTHORITY
                    + "/" + BASE_PATH);
  Uri rowUri = ContentUris.withAppendedId(
                    contentUri, getFirstRowId());
  int count = getContentResolver().update(rowUri, values, null, null);

}

让我们更仔细地看看update()方法:

public final int update (Uri uri, ContentValues values, String where, String[] selectionArgs)

以下是上一行代码的参数:

  • uri:这是我们希望修改的内容 URI

  • values:这类似于我们之前在其他方法中使用的值;传递null值将删除现有字段值

  • where:作为过滤器对行进行更新之前的 SQL WHERE子句

我们可以再次运行query()方法来查看更改是否反映出来;这个活动留给你作为练习。

最后一个方法是delete(),我们需要它来完成我们的 CRUD 方法。delete()方法的开始方式与其他方法类似;首先,准备我们的内容 URI 在目录级别,然后在 ID 级别构建它,也就是在单个行级别。之后,我们将其传递给ContentResolverdelete()方法。与query()insert()方法返回整数值不同,delete()方法删除由我们基于 ID 的内容 URI 对象rowUri指向的行,并返回删除的行数。在我们的情况下,这将是1,因为我们的 URI 只指向一行。另一种方法可能是使用指向表的contentUriselection/selectionArgs的组合。在这种情况下,根据selection子句,删除的行可能多于 1:

public void delete(View v) {

      Uri contentUri = Uri.parse("content://" + AUTHORITY
                              + "/" + BASE_PATH);
      Uri rowUri = contentUri = ContentUris.withAppendedId(
                              contentUri, getFirstRowId());
      int count = getContentResolver().delete(rowUri, null,
               null);
}

UI 和输出如下:

使用内容提供程序

注意

如果你想更深入地了解 Android 内容提供程序是如何在各个表之间管理各种写入和读取调用的(提示:它使用CountDownLatch),你可以查看 Coursera 上 Douglas C. Schmidt 博士的视频以获取更多信息。视频可以在class.coursera.org/posa-002/lecture/49找到。

总结

在本章中,我们介绍了内容提供程序的基础知识。我们学习了如何访问系统提供的内容提供程序,甚至我们自己的内容提供程序版本。我们从创建一个基本的联系人管理器,逐渐发展成为 Android 生态系统中的一个完整的成员,通过实现ContentProvider来在其他应用程序之间共享数据。

在接下来的章节中,我们将介绍LoadersCursorAdapters、巧妙的技巧和提示,以及一些开源库,以使我们在使用 SQLite 数据库时更轻松。

第四章:小心操作

"过早优化是万恶之源。"
---Donald Knuth

在上一章中,我们涵盖了一个非常重要的概念:内容提供程序。我们以一步一步的方式进行了进展,详细介绍了如何创建内容提供程序以及如何使用现有系统与内容提供程序。我们还介绍了如何通过创建一个测试应用程序来访问我们创建的内容提供程序。

在本章中,我们将探讨如何使用加载器,特别是一种名为游标加载器的加载器。我们将通过一个示例来了解如何异步与内容提供程序进行交互。我们将讨论安卓数据库中的重要安全主题,以及如何确保数据在安卓模型中得到保护。最后但并非最不重要的是,我们还将看到一些代码片段,涵盖了如何升级数据库以及如何在应用程序中预装数据库。

在本章中,我们将涵盖以下主题:

  • 使用 CursorLoader 加载数据

  • 数据安全

  • 一般提示和库

使用 CursorLoader 加载数据

CursorLoader是加载器家族的一部分。在我们深入探讨如何使用CursorLoader的示例之前,我们将稍微探讨一下加载器以及为什么它在当前情况下很重要。

加载器

在 HoneyComb(API 级别 11)中引入,加载器的作用是在活动或片段中异步提供数据。需要加载器的原因有很多:在主 UI 线程上调用各种耗时方法以获取数据导致界面笨重,甚至在某些情况下出现可怕的 ANR 对话框。这在以下截图中有所展示:

加载器

例如,managedQuery()方法在 API 11 中已被弃用,它是ContentResolver'squery()方法的包装器。

在上一章中,我们在强调如何在查询方法中从内容提供程序中获取数据时,使用了getContentResolver.query()而不是managedQuery()。使用弃用的方法可能会导致未来版本出现问题,应该避免使用。

加载器为活动或片段在非 UI 线程上异步加载数据。加载器或加载器的子类在单独的线程中执行其工作,并将结果传递到主线程。在单独的线程中工作时,从主线程中调用和在主线程上发布结果的分离确保我们拥有一个响应迅速的应用程序。

提示

在加载器时代之后,我们面临着诸如当活动由于配置更改而需要重新创建时的问题,例如,设备方向的旋转。我们必须在创建新实例时担心数据和重新获取数据。但是有了加载器,我们不必担心所有这些,因为加载器在设备配置更改后重新创建时会自动重新连接到上一个加载器的游标并重新获取数据。作为额外的奖励,加载器监视数据源,并在内容更改时提供新的结果。换句话说,加载器会自动更新,因此无需重新查询游标。在安卓开发者网站上阅读更多关于保持您的安卓应用程序响应迅速并避免应用程序无响应ANR)消息的内容,网址为developer.android.com/training/articles/perf-anr.html

加载器 API 摘要

让我们来看看由各种类和接口组成的加载器 API。在本节中,我们将看一下加载器 API 类/接口的实现方面:

类/接口 描述
LoaderManager 这是与活动或片段关联的抽象类,用于管理加载器。虽然可以有一个或多个加载器实例,但每个活动或片段只允许一个LoaderManager实例。它负责处理活动或片段的生命周期,特别是在运行长时间任务时非常有帮助。
LoaderManager.LoaderCallbacks 这是一个回调接口,我们必须实现以与LoaderManager交互。
Loader 这是加载器的基类。它是一个执行数据异步加载的抽象类。我们可以实现自己的子类,而不是使用诸如CursorLoader之类的子类。
AsyncTaskLoader 这是一个抽象的加载器,提供AsyncTask在后台执行工作,也就是在单独的线程上;然而,结果是在主线程上传递的。根据文档,建议子类化AsyncTaskLoader而不是直接子类化Loader类。
CursorLoader 这是AsyncTaskLoader的子类,它在后台线程上以非阻塞方式查询ContentResolver并返回游标。

使用 CursorLoader

加载器为我们提供了许多方便的功能;其中之一是一旦我们的活动或片段实现了加载器,就不需要担心刷新数据。加载器为我们监视数据源,反映任何更改,甚至执行新的加载;所有这些都是异步完成的。因此,我们不需要关心实现和管理线程,将查询卸载到后台线程,并在查询完成后检索结果。

加载器可以处于以下三种不同状态之一:

  • 启动状态:一旦启动,加载器将保持在此状态,直到停止或重置。它执行加载,监视任何更改,并将其反映给监听器。

  • 停止状态:在这里,加载器继续监视更改,但不将结果传递给客户端。

  • 重置状态:在此状态下,加载器释放其持有的任何资源,并不执行执行、加载或监视数据的过程。

现在,我们将重新查看我们的个人联系人管理应用程序,并对我们的应用程序实现CursorLoader进行相应的更改。CursorLoader,顾名思义,是一个查询ContentResolver并返回游标的加载器。这是AsyncTaskLoader的子类,并在后台线程上执行游标查询,以便不阻塞应用程序的 UI。在图表中,您可以看到加载器回调的各种方法以及它们如何与CursorLoaderCursorAdapter进行通信。

使用 CursorLoader

要实现游标加载器,我们需要执行以下步骤:

  1. 首先,我们需要实现LoaderManager.LoaderCallbacks<Cursor>接口:
public class ContactsMainActivity extends Activity implements OnClickListener, LoaderManager.LoaderCallbacks<Cursor> {…}

然后,实现反映加载器不同状态的方法:onCreateLoader()onLoadFinished()onLoaderReset()

  1. 发起查询,我们将调用LoaderManager.initLoader()方法;这将初始化后台框架。
getLoaderManager().initLoader(CUR_LOADER, null, this);

CUR_LOADER值传递给onCreateLoader()方法,它充当加载器的 ID。对initloader()的调用会调用onCreateLoader(),传递我们用于调用initloader()的 ID:

@Override
public Loader<Cursor> onCreateLoader(int loaderID, 
Bundle bundle)
{
  switch (loaderID) {
  case CUR_LOADER:
    return new CursorLoader(this, PersonalContactContract.CONTENT_URI,
      PersonalContactContract.PROJECTION_ALL, null, null, null );
    default: return null;
   }
}
  1. 我们使用 switch case 根据其 ID 获取加载器,并对于无效的 ID 返回null。我们创建一个 URI 对象contentUri并将其作为参数传递给CursorLoader构造函数。需要注意的是,我们可以使用此构造函数或空的未指定的游标加载器CursorLoader(Context context)来实现游标加载器。此外,我们可以通过方法设置值,例如setUri(Uri)setSelection(String)setSelectionArgs(String[])setSortOrder(String)setProjection(String[])
public CursorLoader (Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

以下是上述代码的参数:

  • context:这是父活动的上下文。

  • uri:我们使用contentURI,采用content://方案,来检索内容。它可以基于 ID 或目录。

  • projection:这是要返回的列的列表,因为我们已经准备好了列名。传递null将返回所有列。

  • selection:这被格式化为 SQL 的WHERE子句,不包括WHERE本身,作为一个过滤器声明要返回哪些行。

  • selectionArgs:我们可以在选择中包含问号,这些问号将被selectionArgs中绑定的字符串值替换,并按照它们的选择顺序出现。

  • sortOrder:这告诉我们如何对行进行排序,格式为 SQL 的ORDER BY子句。空值将使用默认排序顺序。

  1. onCreateLoader在后台启动查询,当查询完成时,游标加载器对象被传递给后台的框架,框架调用onLoadFinished(),我们在这里提供游标对象数据给我们的适配器实例:
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data)
{
  this.mAdapter.changeCursor(data);
}
  1. 适配器是CursorAdapter的子类。我们不再使用传统的通过扩展BaseAdapter获得的getView()方法,而是使用bindView()newView()方法。我们在newView中填充我们的列表视图行布局,而在 bind view 中,我们执行类似于getView()方法的操作。我们定义我们的布局元素,并将其与相关数据关联起来:
public class CustomCursorAdapter extends CursorAdapter
{
   ...
  public void bindView(View view, Context arg1, Cursor cursor)
  {
    finalImageView contact_photo = (ImageView) view
      .findViewById(R.id.contact_photo);
  ...
  ...
  contact_email.setText(cursor.getString(cursor
       .getColumnIndexOrThrow(DatabaseConstants.TABLE_ROW_EMAIL)));
  setImage(cursor.getBlob(cursor
       .getColumnIndex(DatabaseConstants.TABLE_ROW_PHOTOID)),
      contact_photo);
   }

   @Override
  public View newView(Context arg0, Cursor arg1, ViewGroup arg2)
  {
    final View view = LayoutInflater.from(context).inflate(
      R.layout.contact_list_row, null, false);
    return view;
   }
...
}
  1. 当游标加载器被重置时,将调用此方法。我们通过向changeCursor()方法传递null来清除对游标的任何引用。每当与游标相关的数据发生更改时,游标加载器在重新运行查询之前调用此方法,以清除任何过去的引用,从而防止内存泄漏。一旦设置了onLoaderReset(),游标加载器将重新运行其查询。
@Override
public void onLoaderReset(Loader<Cursor> loader) 
{
  this.mAdapter.changeCursor(null);

    }
  1. 现在我们转向我们的内容提供程序,在那里我们必须进行一些小的更改,以确保我们对数据库所做的任何更改都反映在我们应用程序的列表视图中:
cr.setNotificationUri(getContext().getContentResolver(),uri);
  1. 我们需要在ContentProvider的查询方法中通过游标在ContentResolver中注册observer。我们这样做是为了监视内容 URI 的任何更改,这可以是特定数据行或表的 URI:
getContext().getContentResolver().notifyChange(ur,null);
  1. insert()方法中,我们使用notifyChange()方法通知已注册的观察者行已更新。默认情况下,CursorAdapter对象将收到此通知。因此,现在当我们通过在我们的应用程序中插入新联系人来添加新的数据行时,将通过调用contentProviderinsert()方法:
resolver.insert(PersonalContactContract.CONTENT_URI, prepareData(contact));
  1. 对于delete()update()方法,需要执行类似的操作,这两种方法都留给读者作为练习,因为大部分样板代码都已经存在。实现加载器是简单的,可以节省我们很多线程方面的麻烦,强烈建议在执行此任务时避免令人不悦的 UI。

注意

loadInBackground()是另一个重要的方法;这返回一个用于加载操作的游标实例,并在工作线程上调用。理想情况下,loadInBackground()不应直接返回加载操作的结果,但我们可以通过重写deliverResult(D)方法来实现这一点。要取消,我们需要检查isLoadInBackgroundCanceled()的值,就像在AsyncTask中检查isCancelled()一样,定期检查。

数据安全

安全是当今的热门词。Android 生态系统确保我们的数据库不会暴露给窥探的眼睛;然而,一个 rooted 设备可能会暴露我们的数据库,就像我们在第二章连接点中看到的那样。借助 rooted 设备,模拟器和adb pull命令,在我们的情况下,我们拉取了我们的数据库以便使用 SQLite 管理工具进行检查。另一个重要的方面是内容提供程序;在设置权限时,我们需要小心。我们应该强制执行适当权限的申请过程,以便告知用户应用程序对数据的控制,使用contract类。

ContentProvider 和权限

在第三章分享是关心中,我们简要介绍了将提供者添加到清单部分中的权限主题。让我们再详细介绍一下:

  1. 如前所述,在将内容提供程序添加到清单时,我们还将添加我们的自定义权限。这将确保两件事,即阻止应用程序中的未经授权的操作,并告知用户权限:
<provider
android:name="com.personalcontactmanager.provider.PersonalContactProvider"
android:authorities="com.personalcontactmanager.provider"
android:readPermission="com.personalcontactmanager.provider.read"
android:exported="true"
android:grantUriPermissions="true"
>
  1. 此外,我们将在清单中添加permissions标签,以指示其他应用程序将需要的权限集:
<permission
android:name="com.personalcontactmanager.provider.read"
android:icon="@drawable/ic_launcher"
android:label="Contact Manager"
android:protectionLevel="normal" >
</permission>  
  1. 现在,在我们想要访问内容提供程序的应用程序中,我们使用permission标签,在我们的情况下,在代码包中使用Ch4-TestApp
<uses-permission android:name="com.personalcontactmanager.provider.read" />

当用户安装此应用程序时,他们将收到我们的自定义权限消息以及应用程序所需的其他权限。在这一步中,不要直接从 Eclipse 运行应用程序,而是导出一个 apk 并安装它:

ContentProvider 和权限

如果您没有在应用程序中定义权限,并且应用程序尝试访问内容提供程序,它将收到SecurityException: Permission Denial消息。

如果我们创建的内容提供程序不打算共享,我们需要将android:exported="true"属性更改为false。这将使我们的内容提供程序更安全,如果有人试图对其运行恶意查询,他们将遇到安全异常。

如果我们只想在我们的应用程序之间共享数据,Android 提供了一个解决方案;我们可以使用android:protectionLevel并将权限设置为signature而不是normal。为此,实现内容提供程序和想要访问它的应用程序都必须在导出时由相同的密钥签名。这是因为奖励签名权限不需要用户确认。这不会让用户感到困惑,因为它是在内部完成的,也不会影响用户体验。

加密关键数据

我们已经讨论了其他应用程序对我们的数据库有什么样的访问权限,以及如何有效地共享我们的内容提供程序,我们也简要讨论了为什么我们不应该相信系统是绝对安全的。在最安全的方法中,敏感数据不会保存在设备上,而是保存在服务器上,并且它将使用令牌来授予访问权限。如果必须将数据存储在设备的数据库中,请使用加密。使用用户定义的密钥来加密和解密敏感数据。

我们将探讨一种使用加密数据库的方法,如果有人能够通过 root 或利用备份的手段提取它,那么它将是不可读的。如果有人试图使用 SQLite Manager 或其他工具来读取它,他们将收到友好的消息,就像下面截图中显示的那样;这是我们将用一个名为 SQLCipher 的库在一会儿创建的数据库文件。

加密关键数据

SQLCipher 是 SQLite 的一个开源扩展,提供了数据库文件的透明 256 位 AES 加密,正如他们的网站上所提到的。部署 SQLCipher 非常容易。现在我们将看一下构建一个示例应用程序的步骤:

  1. 首先,我们将从sqlcipher.net/open-source下载所需的文件。在这里,他们列出了基于 Android 的 SQLCipher 的社区版;下载它。

  2. 现在我们将在我们的 eclipse 环境中创建一个新的 Android 项目。

  3. 在下载的文件夹中,我们会找到libs文件夹;里面是一组我们需要与 SQLCipher 一起使用的 jar 文件。我们还会注意到文件夹被命名为armeabiarmeabi-v7ax86,所有这些文件夹都包含.so文件。如果您熟悉 Android NDK,这不会是新鲜事。.so文件是共享对象文件,是动态库的组成部分。对于不同的架构,我们需要不同的.so文件,因此有三个文件夹。如果您正在运行 x86 模拟器,则需要在libs文件夹中使用x86文件夹。为简单起见,我们将所有文件夹复制到libs文件夹中。将asset文件夹的内容复制到我们项目的asset文件夹中,并导航到项目的属性。它看起来像以下截图。您还可以在项目的类路径中看到这些 JAR 文件。此项目的初始设置现在已经完成。加密关键数据

完成必要的设置后,让我们开始编写代码来制作一个小型测试应用程序:

public class MainActivity extends Activity
{
  TextView showResult;

   @Override
   protected void onCreate(Bundle savedInstanceState)
   {  
super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  showResult = (TextView) findViewById(R.id.showResult);
  InitializeSQLCipher();
   }

   private void InitializeSQLCipher()
   {
SQLiteDatabase.loadLibs(this);
  File databaseFile = getDatabasePath("test.db");
  databaseFile.mkdirs();
  databaseFile.delete();
  SQLiteDatabase database = SQLiteDatabase
      .openOrCreateDatabase(databaseFile, "test123", null);
  database.execSQL("create table t1(a, b)");
  database.execSQL("insert into t1(a, b) values(?, ?)",
        new Object[] {"I am ", "Encrypted" });
   }

   public void runQuery(View v)
   {
  File databaseFile = getDatabasePath("test.db");
  SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(
      databaseFile, "test123", null);
  String selection = "select * from t1";
  Cursor c = database.rawQuery(selection, null);
  c.moveToFirst();
  showResult.setText(c.getString(c.getColumnIndex("a")) + 
c.getString(c.getColumnIndex("b")));
    }
}

上述代码有两个主要方法:InitializeSQLCipher()runQuery()。在InitializeSQLCipher()中,我们通过调用loadLibs()方法加载我们的.so库文件。

  1. 现在我们找到数据库的绝对路径,并创建缺少的父文件夹。通过openOrCreateDatabase(),我们将调用打开现有数据库或者如果数据库不存在则创建一个。我们将执行标准的数据库调用来创建一个具有列ab的表,并在一行中插入值。

现在我们将执行一个简单的查询,将值取回到runQuery()方法。您会注意到,除了加载库之外,我们使用的所有核心方法基本上都是标准的,那么主要的变化在哪里呢?转到代码包中的Ch4-PersonalContactManager示例,注意我们使用的包:

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

我们有 SQLCipher 包:

import net.sqlcipher.Cursor;
import net.sqlcipher.database.SQLiteDatabase;

实现简单,熟悉且易于实现。如果您将数据库取出并尝试读取它,您将会发现错误消息,就像我们之前在截图中显示的那样。用户将不会发现任何变化,甚至我们应用程序的逻辑仍然保持不变。在截图中,您可以看到我们刚刚构建的应用程序屏幕,它加密了数据库:

加密关键数据

注意

OAuth是授权的开放标准。它为客户端应用程序提供了安全的委托访问,以代表资源所有者访问服务器资源。它规定了资源所有者授权第三方访问其服务器资源而不共享其凭据的过程,如维基百科所述;在oauth.net/2/了解更多关于 OAuth 的信息。

一般提示和库

我们将涵盖一些一般和不那么一般的解决方法和实践,这取决于情况。例如,在某些情况下,我们需要拥有一个预填充的值数据库,我们将在我们的 Android 应用程序中使用,或者升级一个数据库,这似乎微不足道,但可能会破坏我们的应用程序。

升级数据库

在第二章中,连接点,我们使用onUpgrade()来展示数据库如何更新。如果我们回到这个例子,您会注意到它执行了Drop Table命令。这里将发生的是原始表将被删除,并且通过onCreate()调用将创建一个新表。这将导致现有数据的丢失,因此不适合我们需要修改数据库的情况。onUpgrade()函数可以定义如下:

public void onUpgrade(SQLiteDatabase db, int oldVersion,int newVersion)
{
  String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME;
  db.execSQL(DROP_TABLE);
  onCreate(db);
}

另一个挑战是确定我们在这里使用的版本。用户可能正在运行应用程序的旧版本,因此我们必须记住应用程序具有的不同版本以及这些版本是否会对数据库带来任何更改。对于新用户,我们不需要担心,因为如果数据库不存在,将调用onCreate()

为了确保我们有一个适当的升级,我们将在我们的CustomSQLiteOpenHelper类中使用DB_VERSION常量,告诉我们的onUpgrade()方法要采取的操作:

private static final int DB_VERSION = 1;

我们将把DB_VERSION常量更改为3以反映升级:

private static final int DB_VERSION = 3;

构造函数会处理其余的事情:


public CustomSQLiteOpenHelper(Context context) 
{
  super(context, DB_NAME, null, DB_VERSION);
}  

当运行超类构造函数时,它会将存储的 SQLite.db文件的DB_VERSION常量与我们作为参数传递的DB_VERSION进行比较,并在需要时调用onUpgrade()方法:

public void onUpgrade(SQLiteDatabase db, int oldVersion,int newVersion)
{
switch(oldVersion) {
   case 1: db.execSQL(DATABASE_CREATE_MAIN_TABLE);
   case 2: db.execSQL(DATABASE_CREATE_MAIN_TABLE);
   case 3: db.execSQL(DATABASE_CREATE_DEL_TABLE);
   }
}

在我们的onUpgrade()方法中,我们有一个 switch case 来进行更改。请注意,我们不使用break语句,因为用户可能使用旧版本,并且可能没有更新应用程序,如前面所述。例如,假设用户正在运行DB_VERSION =1的应用程序的特定版本,并且跳过了包含DB_VERSION =2的下一个更新,最终发布了一个具有DB_VERSION =3的新版本的应用程序。现在,我们有一个情况,用户仍在使用旧版本的应用程序,并且尚未安装我们发布的新更新。因此,在这种情况下,当用户安装应用程序时,onUpgrade()方法将首先执行case 1,然后转到case 2以安装用户错过的更新;最后,用户将安装第三个版本的更新,确保所有数据库更改都得到反映。请注意,这里没有break语句。这是因为我们希望运行switch语句获得值1的所有情况以及switch语句获得值2的最后两个语句。

或者,我们也可以使用if语句。这也会按照我们的意图进行,因为我们的测试DB_VERSION常量是1,这将满足两个条件并反映出更改:

if (oldVersion<2) {db.execSQL(DATABASE_CREATE_MORE_TABLE); } 
if (oldVersion<3) {db.execSQL(DATABASE_CREATE_DEL_TABLE); }

无需 SQL 语句的数据库

在本书的大部分内容中,我们寻找了 Android 和 SQLite 的各个角落。对于一些人来说,编写 SQL 语句可能只是在办公室的另一天,而对于一些人来说,这可能是一次过山车之旅。本节将介绍一个库,它使我们能够在不编写任何 SQL 语句的情况下保存和检索 SQLite 数据库记录。ActiveAndroid是用于 Android 的活动记录风格的 SQLite 持久化。根据文档,每个数据库记录都被整洁地包装到一个具有save()delete()等方法的类中。我们将使用 ActiveAndroid 文档中的示例,并基于此构建一个可工作的示例。让我们看看启动和运行所需的步骤。

查看官方网站www.activeandroid.com/,了解概述并从goo.gl/oW2kod下载文件。

下载文件后,在根文件夹上运行ant来构建 JAR 文件。运行ant后,您将在dist文件夹中找到您的 JAR 文件。在 Eclipse 中,创建一个新项目,将 JAR 文件添加到项目的libs文件夹中,然后将 JAR 文件添加到项目属性中的Java Build Path中。

ActiveAndroid 会查找通过执行以下步骤配置的一些全局设置:

  1. 我们将首先创建一个类,扩展应用程序类:
public class MyApplication extends com.activeandroid.app.Application 
{
   @Override
public void onCreate()
{
     super.onCreate();
     ActiveAndroid.initialize(this);
    }

   @Override
public void onTerminate()
{
     super.onTerminate();
     ActiveAndroid.dispose();
   }

}
  1. 现在我们将把这个应用程序类添加到我们的清单文件中,并添加与我们的应用程序对应的元数据:
<application
  android:name="com.active.android.MyApplication">
  <meta-data
     android:name="AA_DB_NAME"
     android:value="test.db" />
  <meta-data
     android:name="AA_DB_VERSION"
     android:value="1" />
………..
</application>
  1. 完成了这个基本设置后,我们现在将继续创建我们的数据模型。ActiveAndroid 库支持注释,我们将在以下模型类中使用它:
// Category class

@Table(name = "Categories")
public class Category extends Model
{
@Column(name = "Name")
public String name;
}

// Item class

@Table(name = "Items")
public class Item extends Model 
{
   // If name is omitted, then the field name is used.
@Column(name = "Name")
public String name;

@Column(name = "Category")
public Category category;

public Item() 
{
     super();
   }

   public Item(String name, Category category)
   {
     super();
     this.name = name;
     this.category = category;   
   }
   }

注意

如果您想探索注释并在项目中使用它们并减少样板代码,您可以查看以下 Android 库:Android Annotations,Square's Dagger 和 ButterKnife。

  1. 要添加新的类别或项目,我们需要调用save()。在代码段中,我们可以看到创建了一个项目对象并与特定类别关联,并且最后调用了save()
public void insert(View v) 
{
  Item testItem = new Item();
  testItem.category = testCategory;
  testItem.name = editTextItem.getText().toString();
  testItem.save();
}

要删除项目,我们可以调用item.delete()。同样,要获取值,我们也有相关的方法。以下是调用特定类别的所有数据的调用:

  List<Item>getall = new Select().from(Item.class)
       .where("Category = ?", testCategory.getId())
       .orderBy("Name ASC").execute();

在 ActiveAndroid 中还有很多可以探索的内容。他们有模式迁移和类型序列化;除此之外,您还可以通过将数据库放在asset文件夹中来提供预填充数据库,并且还可以使用内容提供程序。简而言之,这是一个为寻求间接与数据库通信并执行数据库操作的人构建的良好库。它有助于以熟悉的 Java 方法形式访问数据库,而不是准备 SQL 语句来执行相同的操作。完整的示例代码捆绑在第四章的代码包中。

使用预填充数据库

我们将构建一个数据库并将其放入我们的asset文件夹中,这是一个只读目录。在运行时,我们将检查数据库是否存在。如果不存在,我们将从asset文件夹复制我们的数据库到/data/data/yourpackage/databases。在第二章中,我们使用了一个名为 SQLite Manager 的工具;看一下该章的第三个屏幕截图。我们现在将使用相同的工具来构建我们的数据库。如果您按照该部分中解释的方式提取数据库或查看该屏幕截图,您将注意到除了您的数据库表之外还有一些其他表:

使用预填充数据库

创建预填充数据库的步骤如下:

  1. 要创建一个预填充的数据库,我们需要创建一个名为android_metadata的表,除了我们需要的表。使用 SQLite Manager 工具,我们将创建一个名为contact的新数据库,然后我们将创建android_metdata表:
CREATE TABLE "android_metadata"("locale" TEXT DEFAULT 'en_US')
  1. 我们将在表中插入一行:
INSERT INTO "android_metadata" VALUES ('en_US')
  1. 现在我们将使用我们在第二章中使用的 SQL 查询来创建我们需要的表,即contact_table。在DatabaseManager类中,我们将只需用实际值替换常量:
CREATE TABLE "contact_table" ("_id" integer primary key autoincrement not null,"contact_name" text not null,"contact_number" text not null,"contact_email" text not null,"photo_id" BLOB )

如果尚未定义,有必要将我们表的主 ID 字段重命名为_id。这有助于 Android 识别我们表的 ID 字段绑定位置。

  1. 让我们填写一些数据行。我们可以通过运行Insert查询或使用该工具手动输入值来完成。现在,将数据库文件复制到asset文件夹中。

  2. 现在,在我们原始的个人联系人管理器中,我们将修改我们的DatabaseManager类。好处是这是我们唯一需要修改的类,系统的其余部分将按预期工作。

  3. 当应用程序运行并通过传递上下文创建一个新的DatabaseManager类时,我们将调用createDatabase(),首先我们将检查数据库是否已经存在:

Private Boolean checkDataBase()
{
  SQLiteDatabase checkDB = null;
  try {
     String myPath = DB_PATH + DB_NAME;
     checkDB = SQLiteDatabase.openDatabase(myPath, null,
         SQLiteDatabase.OPEN_READONLY);
  } catch (SQLiteException e) {
     // database doesn't exist yet.
  }
  if (checkDB != null) {
     checkDB.close();
  }
  return checkDB != null ? true : false;
}
  1. 如果没有,我们将创建一个空数据库,然后将其替换为我们从asset文件夹中复制的数据库。从asset文件夹复制数据库后,我们将创建一个新的SQLiteDatabase对象:
private void copyDataBase() throws IOException
{
  InputStream myInput = myContext.getAssets().open(DB_NAME);
  String outFileName = DB_PATH + DB_NAME;
  OutputStream myOutput = new FileOutputStream(outFileName);
  byte[] buffer = new byte[1024];
  int length;
  while ((length = myInput.read(buffer)) > 0) {
     myOutput.write(buffer, 0, length);
  }

  myOutput.flush();
  myOutput.close();
  myInput.close();
}

另一个要注意的是,我们的CustomSQLiteOpenHelper类的onCreate()方法将是空的,因为我们不是在创建数据库和表,而是在复制一个。示例代码捆绑在第四章的代码包中。如果这个过程看起来很繁琐,不用担心;Android 开发者社区为您提供了解决方案。SQLiteAssetHelper 是一个 Android 库,将帮助您管理数据库的创建和版本管理,使用应用程序的原始资产文件。

要实现这一点,我们必须遵循一些简单的步骤:

  1. 将 JAR 文件复制到我们项目的libs文件夹中。

  2. 将库添加到 Java 构建路径中。

  3. 将我们的压缩数据库文件复制到projectassets/databases/your_database.db.zipasset文件夹中。

  4. ZIP 文件应该只包含一个db文件。

  5. 不要扩展框架的SQLiteOpenHelper类,而是扩展SQLiteAssetHelper类。

  6. 他们还为您提供升级数据库文件的帮助,该文件需要放在assets/databases/<database_name>_upgrade_<from_version>-<to_version>.sql中。

  7. 该库、文档及其相应的示例可在goo.gl/8XSSmR找到。

总结

在本章中,我们涵盖了许多高级主题,从加载程序到数据安全性。我们实现了我们的游标加载程序,以了解加载程序如何为我们的应用程序带来魔力,并深入了解了如何保护我们的数据库以及在向其他应用程序公开内容提供程序时理解权限的概念。我们还介绍了一些技巧,比如使用预填充数据库进行发货,升级数据库而不破坏系统,以及在不使用 SQL 命令的情况下使用数据库查询。这绝不是我们可以通过数据库和 Android 实现的唯一一组事物。本章只是对广阔的编程可能性的一个推动。

posted @ 2024-05-22 15:10  绝不原创的飞龙  阅读(4)  评论(0编辑  收藏  举报