讀古今文學網 > Android程序設計:第2版 > SQL語言 >

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.db
SQLite version 3.6.22
Enter ".help" for instructions
Enter 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> .table
video
sqlite>
  

下一步,將介紹之前介紹的關於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 Bicycle
Epic 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=m0iGn2c47LA
Epic Fail Sidewalk|http://www.youtube.com/watch?v=m0iGn2cNcNo
Epic Fail Motorcycle|http://www.youtube.com/watch?v=7n7apBB8qkeGE
Epic Fail Car|http://www.youtube.com/watch?v=01ynapTnYVkeGE
Epic 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 Bicycle
3|Epic Fail Wagon
4|Epic Fail Sidewalk
5|Epic Fail Motorcycle