首页 » Android程序设计:第2版 » Android程序设计:第2版全文在线阅读

《Android程序设计:第2版》SQL语言

关灯直达底部

编写Android应用通常需要掌握基本的SQL语言知识,虽然更高级别的类通常是为了最常见的、数据相关的操作而提供的。本章提供了初学者所需的有关SQLite的介绍。虽然本书不是介绍SQL的,但我们将为你提供Android开发所需的SQL的足够详细的细节,使你能够实现Android应用中广泛存在的数据持久性。关于SQLite语言的更全面的信息,可参看http://www.sqlite.org/lang.html。我们将使用简单的SQL命令来解释SQLite语言,并且顺便说明如何使用sqlite3命令来查看这些查询对表的修改所带来的影响。你可能会发现W3Schools的教程也很有用:http://www.w3schools.com/sql/sql_intro.asp。

有了SQLite,数据库在Android文件系统中就是简单的文件,它可以存储在flash或外部存储卡上,但是你会发现大多数的数据库保存在目录/data/data/com.example.yourAppPackage/databases中。可以在adb shell窗口中执行ls命令列出Android在该目录下所创建的所有数据库。

数据库会处理好数据的持久性问题——它会通过应用程序发出的每条SQL语句更新SQLite文件。在后面,我们将描述SQLite命令,这些命令是在sqlite3命令行实用工具内使用的。后面,我们将说明使用Android API实现这些功能的方式。虽然命令行SQL不是要提交的应用的一部分,但它肯定有助于你调试应用。你会发现在Android中编写数据库代码通常是反复地编写Java代码对表执行操作的过程,然后使用命令行SQL语句查看所创建的数据。

SQL数据定义命令

SQL语句可以分成两类:一是创建和修改表,即数据所存储的地方,二是添加、读取、更新和删除这些表中的数据。本节要介绍的是第一类,即数据定义命令。

CREATE TABLE(http://www.sqlite.org/lang-createtable.html)

开发人员在使用SQL时,都是先创建一张表来存储数据。CREATE TABLE命令会在SQLite数据库中创建一张新表。它指定了表的名称及各个列(column),数据库表的名称必须是唯一的。每个列的名称在该表内必须是唯一的,每个列对应一种数据类型(SQL的数据类型,如date或text string)。列还可以指定一些其他属性,比如数据值是否需要唯一,当插入一条记录而没有指定值时,是否会有默认值,以及该列是否允许为NULL。

数据库表类似于电子表格。回到之前我们提到的联系方式数据库,表中的每一条记录包含一个联系方式的信息。列描述了你所收集的个人联系方式的各种信息:姓名、生日等。本章给出了一些例子,它们将有助于你开始使用我们的工作数据库。

注意:通过CREATE TABLE这条SQL语句创建的表及其属性统称为数据库模式(database schema)。

DROP TABLE(http://www.sqlite.org/lang-droptable.html)

DROP TABLE语句会删除由CREATE TABLE语句创建的数据库表。它的参数是待删除的表的名称。执行完DROP TABLE后,表中所存储的所有数据都将无法被检索到。

以下SQL代码将创建并删除一个简单的存储联系信息的表:


CREATE TABLE contacts (    first_name TEXT,    last_name TEXT,    phone_number TEXT,    height_in_meters REAL);DROP TABLE contacts;  

在sqlite3中输入命令时,必须以分号结束每条命令。

创建表之后,可以使用ALTER TABLE命令来修改数据库的模式(可能是添加一列或修改某列的默认值)。

SQLite类型

必须为表的每个列指定类型,正如P251“SQL数据定义命令”一节给出的。SQLite支持以下数据类型。

TEXT

文本字符串,使用数据库编码存储(UTF-8、UTF-16BE或UTF-16LE)。TEXT类型是最常见的。

REAL

浮点值,以8字节的IEEE浮点数存储。

BLOB

任意的二进制数据,以原始的输入形式保存。可以使用BLOB数据类型保存任何可变长度的数据,比如可执行文件,或者下载的图像。一般而言,blob数据会给移动数据库的性能带来很大的开销,应该尽量避免使用它们。在第13章,给出了另一种保存从Internet下载的图像的机制。

INTEGER

有符号整数,根据值的大小以1字节、2字节、3字节、4字节、6字节或8字节存储。

关于SQLite数据类型的信息可以访问这里:http://www.sqlite.org/datatype3.html。

数据库约束

数据库约束是指为某些列设置特定的修饰属性。有些约束是对数据做限制的,比如要求一个列的所有值都必须是唯一的(如保存社保号的列)。还有些约束和功能使用更相关。关系约束、主键(PRIMARY KEY)和外键(FOREIGN KEY)是构成表之间关系的基础。

大多数表包含唯一标识每条给定的记录的列。该列在SQL中使用PRIMARY KEY修饰,它往往只是作为每条记录的唯一标识符,而没有实际意义。因此,不需要为这个字段指定值,可以让SQLite在添加新的记录时,为它分配自增的整数值。其他的数据库通常需要特别指定该列为自动递增的来实现这个效果。SQLite也提供显式的AUTOINCREMENT约束,但是可以按默认方式对主键自动递增。该列中的值递增的功能和高级语言(如Java或C)中的对象指针类似。高级语言中的其他数据库表和代码可以使用该列来指向特定的行。

当数据库记录有唯一主键时,表之间就可以建立依赖关系。例如,数据库的员工表中可以定义一个名为employer_id的整数类型的列,其取值是employers表的主键值。执行SQL查询,从employers表中选出一条或多条记录之后,可以在employees表中使用employer_id查询。可以找到某个employer的所有employees。employees表和employers表可能看起来如下(只给出和这个例子相关的几个字段)所示:


CREATE TABLE employers (    _id INTEGER PRIMARY KEY,    company_name TEXT);CREATE TABLE employees (    name TEXT,    annual_salary REAL NOT NULL CHECK (annual_salary > 0),    employer_id REFERENCES employers(_id));  

一张表指向另一张表主键的思想在SQL中是通过外键(FOREIGN KEY)约束来实现的,它可以确保表之间的引用的有效性。该约束告诉数据库包含外键约束的整数列必须指向另一张表的有效记录的主键。因此,如果在employees表中插入一条记录,其employer_id在employers表中不存在,SQL就会抛出影响约束的错误提示信息。这可以帮助你避免孤立引用(orphaned references)。但是,SQLite中的外键约束是可选的,该选项在Android中是关掉的。不能依赖外键约束来捕捉错误的外键引用,因此,当创建使用外键的数据库模式时,应该注意。

数据库中还有一些其他约束,它们的影响没有外键那么深远。

UNIQUE

当插入或更新一条记录时,强制使给定列的值和该列中所有已有记录的值不同。任何尝试生成重复值的插入或更新操作将会造成违反SQLite约束。

NOT NULL

要求该列的值不能为NULL。注意主键必须同时满足UNIQUE和NOT NULL。

CHECK

接受布尔值表达式,要求对于任何插入列的值,该表达式都不为空。例如CHECK(annual_salary>0),属性在前面的employees表中给出了。

SQL数据操作命令

使用数据定义命令定义了表之后,就可以在数据库中插入数据并执行一些查询。以下数据操作命令是最常见的SQL语句。

SELECT

SELECT是查询数据库的主要工具。SELECT语句的执行结果是0条或多条(含1条)数据记录,每条数据记录包含固定的列数。可以把SELECT语句想成生成一张新的表,该表只包含在语句中选定的行和列。SELECT语句在SQL语言中是最复杂的命令,支持很多方式来构建一张数据库表或多张数据库表之间的关系。SELECT命令包含以下几个SQL条件子句,Android API支持所有这些子句:

·FROM:指定要查询的数据所在的表。

·WHERE:指定查询语句返回的选定记录要满足的条件。

·GROUP BY:根据列名对结果进行聚集。

·HAVING:根据表达语句进一步对结果进行限制,例如可以从查询中删除元素项少于最小值的组。

·ORDER BY:通过指定列名和函数(如ASC是升序,DSC是降序)设置查询结果的排序方式。

·LIMIT:限制查询结果中记录的数量为指定的值(如5条记录)。

以下是SELECT语句的一些例子:


SELECT * FROM contacts;SELECT first_name, height_in_meters    FROM contacts    WHERE last_name = "Smith";SELECT employees.name, employers.name    FROM employees, employers    WHERE employee.employer_id = employer._id    ORDER BY employer.company_name ASC;  

第一条语句返回contacts表中的所有记录,因为没有WHERE条件子句对结果执行过滤。记录的所有列(通过星号*表示)都会被查询出来。第二条SQL语句返回Smith家庭成员的名字和身高。最后一条语句输出员工及其雇主的名单,按公司名称排序。

关于SELECT语句的更多信息,请参看http://www.sqlite.org/lang_select.html。

INSERT

这条语句的功能是在数据库表中添加一行数据,在该语句中可以为每个列指定一个正确的SQLite类型的取值(例如,一个整数5)。在插入语句中可以指定一次插入操作会影响到的列的清单。如果没有指定所有的列并且在CREATE TABLE语句中为这个列指定了默认值,那么SQLite将为未指定列填充默认值。如果创建表时没有指定默认值,则SQLite会使用NULL进行填充。

下面是几个INSERT语句的例子:


INSERT INTO contacts(first_name)    VALUES("Thomas");INSERT INTO employers VALUES(1, "Acme Balloons");INSERT INTO employees VALUES("Wile E. Coyote", 100000.000, 1);  

第一条语句在contacts表中添加了一条记录,该记录的first_name的值是Thomas,剩余的last_name、phone_number和height_in_meters的取值都是空(NULL)。第二条记录插入了新的雇用者Acme Balloons,第三条记录插入了新员工Wile E.Coyote。

关于INSERT的更多信息,可以查看http://www.sqlite.org/lang_insert.html。

UPDATE

该语句把给定表的某些记录修改成新的值。每个赋值指定表名和函数,它们应该为列生成新值。和SELECT类似,可以指定WHERE子句,确定在调用UPDATE命令时要更新的记录。和INSERT类似,可以指定在执行命令时要更新的列。字段列表和INSERT相同。WHERE子句是至关重要的;如果都不匹配,UPDATE命令将不会更新任何记录;但是如果省略WHERE子句,该语句会影响表中的每条记录。以下是UPDATE语句的一些例子:


    UPDATE contacts        SET height_in_meters = 10, last_name = "Jones"    UPDATE employees        SET annual_salary = 200000.00        WHERE employer_id = (            SELECT _id                FROM employers                WHERE company_name = "Acme Balloons");  

第一条语句把所有朋友都设置为了身高10米的巨人,同时他们的姓氏也被统一设置成了Jones(琼斯)。第二条语句稍复杂些,它给Acme Balloons公司的所有员工大幅加薪。

关于UPDATE的更多信息,可查看http://www.sqlite.org/lang_update.html。

其他数据库概念

现在我们对简单的SQL语句已经有了足够的了解,可以开始使用Android数据库了。随着编写的应用变得愈加复杂,你可能还会用到下面的这些SQL结构,我们在本书中不会详细描述它。

内部连接(inner join)

内部连接在两张或多张表中通过外键关联来选取数据。这种查询对于收集在一张或多张表上分布的对象是有用的。前面的employee/employer示例中就用到了内部连接。正如我们前面指出的,因为Android并没有外键约束,如果用来执行连接的键不存在(外键本应该指向另一个表的某条记录的主键),那就会有麻烦了。

复合查询(compound query)

SQLite支持通过多条语句组合完成复杂的数据库操作。前面更新语句的例子中就有一个复合查询,在UPDATE语句中嵌套了SELECT语句。

触发器(triggers)

数据库触发器支持开发人员编写SQL语句,当特定的数据库条件被满足时会接收回调。

关于这些操作的更详细的信息,建议查询SQL相关的书籍:Alan Beaulieu的《Learning SQL》和Jonathan Gennick的《SQL Pocket Guide》,都是O’Reilly出版社出版的。

数据库事务

数据库事务使得SQL语句序列具有原子性,即要么所有的语句都成功,要么对数据库都没有影响。这种方式很重要,例如,你的应用遇到不幸的事情,如系统崩溃,事务会保证如果设备在某个操作序列中间出现故障,则所有操作都不会影响数据库。以数据库的术语而言,SQLite事务支持广泛的ACID事务属性,详情参见http://en.wikipedia.org/wiki/ACID。

在SQLite中,每个修改数据库的数据库操作在自己的数据库事务中执行数据库修改操作,它说明开发人员可以确保如果SQL语句成功,会写入所有的插入操作的。也可以显式启动和结束包含多个SQL语句的事务。对于给定事务,只有当事务中的所有语句都成功执行时,SQLite才会修改数据库。

对于Android移动环境的不断变化,建议不但要满足应用的一致性需求,还应该善于利用事务来支持应用程序的容错性。

使用sqlite3的数据库操作示例

现在,已经了解了和SQLite相关的SQL基础,我们一起来查看使用sqlite3命令行工具和Android调试shell来存储视频元数据的简单的数据库,可以通过adb命令来执行。使用命令行方式可以马上看到数据库的变化,并提供一些如何使用有用的数据库调试工具的简单示例。关于SQLite的sqlite3的更多说明,可在http://www.sqlite.org/sqlite.html上查找。注意,在最开始使用Android模拟器运行这个例子可能是最简单的,因为要在设备上运行该应用,需要root权限。

下面通过对数据库初始化来开始说明这个例子:


$ adb shell# cd /data/data/# mkdir com.oreilly.demo.pa.ch10.sql# cd com.oreilly.demo.pa.ch10.sql# mkdir databases# cd databases## sqlite3 simple_video.dbSQLite version 3.6.22Enter ".help" for instructionsEnter SQL statements terminated with a ";"sqlite>  

注意:开发人员不应该像这个例子一样手工创建这些目录,因为Android在安装应用时会创建它们。创建目录只对于这个特例有意义,因为我们还没有会自动创建这些目录的应用。

sqlite3命令行接受两种命令:合法的SQL语句,以点(.)开头的单个命令。可以在介绍信息中看到这些命令(可能也是最重要的):.help。尝试执行一下,看可以使用哪些选项。


sqlite> .help.bail ON|OFF             Stop after hitting an error. Default OFF.databases             List names and files of attached databases.dump ?TABLE? ...       Dump the database in a SQL text format.echo ON|OFF             Turn command echo on or off.exit                   Exit this program.explain ON|OFF       Turn output mode suitable for EXPLAIN on or off..header(s) ON|OFF       Turn display of headers on or off.help                   Show this message.import FILE TABLE       Import data from FILE into TABLE.indices TABLE       Show names of all indices on TABLE.load FILE ?ENTRY?       Load an extension library.mode MODE ?TABLE?       Set output mode where MODE is one of:                           csv          Comma-separated values                           column   Left-aligned columns. (See .width)                           html     HTML <table> code                           insert   SQL insert statements for TABLE                           line          One value per line                           list          Values delimited by .separator string                           tabs          Tab-separated values                           tcl          TCL list elements.nullvalue STRING       Print STRING in place of NULL values.output FILENAME       Send output to FILENAME.output stdout           Send output to the screen.prompt MAIN CONTINUE      Replace the standard prompts.quit                   Exit this program.read FILENAME       Execute SQL in FILENAME.schema ?TABLE?       Show the CREATE statements.separator STRING       Change separator used by output mode and .import.show                   Show the current values for various settings.tables ?PATTERN?       List names of tables matching a LIKE pattern.timeout MS             Try opening locked tables for MS milliseconds.timer ON|OFF       Turn the CPU timer measurement on or off.width NUM NUM ...       Set column widths for "column" mode  

在这个列表中,还有一个重要的命令:.exit。要记住该命令!需要使用该命令退出sqlite。此外,也可以使用Ctrl+D键退出。

要记住的另外一点是,每个SQL命令都需要以分号结束。如果看到这样的命令:


sqlite> ls...>  

它表示SQLite认为你开始输入SQL,在等待分号;结束符。注意,.命令不需要以分号作为结束符。

注意:当用户忘记自己正在使用sqlite3时,可能会无意识地输入ls命令。ls不是真正的sqlite3命令。如果输入完ls后,输入分号,sqlite会给出错误信息,然后可以重新开始输入点号或SQL语句。

目前,执行大多数的“点号”命令还没什么意义,因为数据库是空的。因此,我们先来插入一些数据:


sqlite> create table video (   ...> _id integer primary key,   ...> title text,   ...> description text,   ...> url text);  

这些命令会创建一张名为video的新数据库表。列的类型是integer和text。该数据库表包含主键_id,该列名不是随机选定的。Android要求使用_id这个名称作为主键,从而能够使用其游标。

使用“点号”命令.table,可以查看新创建的表:


sqlite> .tablevideosqlite>  

下一步,将介绍之前介绍的关于SQL概念的一些不同的查询,以及基于这些表所创建的应用。首先,先向新的数据库表中插入一些数据,这样在执行查询时可以返回一些结果:


INSERT INTO video (_id, title, url)    VALUES(1, "Epic Fail Car", "http://www.youtube.com/watch?v=01ynapTnYVkeGE");INSERT INTO video (_id, title, url)    VALUES(2, "Epic Fail Bicycle", "http://www.youtube.com/watch?v=7n7apTnYVkeGE");INSERT INTO video (_id, title, url)    VALUES(3, "Epic Fail Wagon", "http://www.youtube.com/watch?v=m0iGn2c47LA");INSERT INTO video (_id, title, url)    VALUES(4, "Epic Fail Sidewalk", "http://www.youtube.com/watch?v=m0iGn2cNcNo");INSERT INTO video (_id, title, url)    VALUES(5, "Epic Fail Motorcycle",        "http://www.youtube.com/watch?v=7n7apBB8qkeGE");  

要注意引号的对称。如果输入一个单引号,sqlite3会一直等待输入另一个单引号和它匹配。在这个例子中,没有给表中的所有列输入值。在SQL语句的INTO后面列出了语句会为其输入数据的字段,VALUES后面给出这些字段的值,顺序是一一对应的。

现在,假设想找到包含单词片段cycle的所有video名称。通过以下SELECT查询可以获取:


sqlite> SELECT title FROM video WHERE title LIKE "%cycle%";Epic Fail BicycleEpic Fail Motorcycle 

sqlite3把每条记录输出一行。在这个例子中,为了语义清晰,SQL保留字大写,但这不是必须的。SQL保留字可以大写,也可以小写,还可以大小写混合。

这个例子也说明了SQL中可用的基础模式匹配。关键字LIKE和通配符(%)结合,可以用于字符串匹配。

假设希望所有视频的URL以标题的反向字母顺序排序:


sqlite> SELECT title, url FROM video ORDER BY title DESC;Epic Fail Wagon|http://www.youtube.com/watch?v=m0iGn2c47LAEpic Fail Sidewalk|http://www.youtube.com/watch?v=m0iGn2cNcNoEpic Fail Motorcycle|http://www.youtube.com/watch?v=7n7apBB8qkeGEEpic Fail Car|http://www.youtube.com/watch?v=01ynapTnYVkeGEEpic Fail Bicycle|http://www.youtube.com/watch?v=7n7apTnYVkeGE  

可以发现sqlite3使用管道符(|)来分隔不同列的值。

这里没有给视频添加描述。下面来添加一下:


sqlite> UPDATE video SET description="Crash!" WHERE title LIKE "%Car";sqlite> UPDATE video SET description="Trip!" WHERE title LIKE '%Sidewalk%';sqlite> SELECT title, description FROM video WHERE NOT description IS NULL;Epic Fail Car|Crash!Epic Fail Sidewalk|Trip!  

最后,通过ID删除一条记录:


sqlite> DELETE FROM video WHERE _id = 1;sqlite> SELECT _id, title FROM videos;2|Epic Fail Bicycle3|Epic Fail Wagon4|Epic Fail Sidewalk5|Epic Fail Motorcycle