SQLite 外键支持

概述

本文档描述了对 SQLite版本 3.6.19 (2009-10-14) 中引入的 SQL 外键约束的支持。

第一部分通过示例介绍了 SQL 外键的概念,并定义了文档其余部分使用的术语。第 2 节描述了应用程序必须采取的步骤才能在 SQLite 中启用外键约束(默认情况下禁用)。下一节,即第 3 节,描述了用户为使用外键约束而必须创建的索引,以及为使外键约束有效运行而应该创建的索引。第 4 节描述了 SQLite 支持的高级外键相关功能,第 5 节描述了ALTERDROP TABLE的方式命令得到增强以支持外键约束。最后,第 6 节列举了当前实现的缺失特性和限制。

本文档不包含用于在 SQLite 中创建外键约束的语法的完整描述。这可以在CREATE TABLE语句的文档中找到。

、外键约束介绍

SQL 外键约束用于强制表之间的“存在”关系。例如,考虑使用以下 SQL 命令创建的数据库模式:

CREATE TABLE artist(
  artistid    INTEGER PRIMARY KEY, 
  artistname  TEXT
);
CREATE TABLE track(
  trackid     INTEGER,
  trackname   TEXT, 
  trackartist INTEGER     -- Must map to an artist.artistid!
);

使用此数据库的应用程序有权假定曲目表中的每一行在艺术家表中都存在对应的行 毕竟,声明中的评论是这么说的。不幸的是,如果用户使用外部工具编辑数据库,或者如果应用程序中存在错误,则可能会向 曲目表中插入与艺术家中的任何行都不对应的行。或者,行可能会从艺术家表中删除,从而在曲目表中留下孤立的行,这些行与艺术家中的任何剩余行都不对应. 这可能会导致应用程序或多个应用程序以后出现故障,或者至少会使应用程序的编码变得更加困难。

一种解决方案是向数据库模式添加 SQL 外键约束,以加强艺术家曲目表之间的关系。为此,可以通过将跟踪表的声明修改为以下内容来添加外键定义:

CREATE TABLE track(
  trackid     INTEGER, 
  trackname   TEXT, 
  trackartist INTEGER,
  FOREIGN KEY(trackartist) REFERENCES artist(artistid)
);

这样,约束由 SQLite 强制执行。尝试向track表中插入一行与artist表中的任何行都不对应的行将失败,当track表中存在依赖行时尝试从artist表中删除一行也会失败。有一个例外:如果track表外键列为NULL,则不需要artist表对应项。用 SQL 表示,这意味着对于跟踪表中的每一行,以下表达式的计算结果为真:

trackartist IS NULL OR EXISTS(SELECT 1 FROM artist WHERE artistid=trackartist)

提示:如果应用程序需要artisttrack之间更严格的关系 ,其中不允许在trackartist列中使用 NULL 值,只需将适当的“NOT NULL”约束添加到架构中。

还有其他几种方法可以将等效的外键声明添加到CREATE TABLE语句中。有关详细信息,请参阅 创建表文档

以下 SQLite 命令行会话说明了添加到跟踪表的外键约束的效果:

sqlite> SELECT * FROM artist;
artistid  artistname       
--------  -----------------
1         Dean Martin      
2         Frank Sinatra    

sqlite> SELECT * FROM track;
trackid  trackname          trackartist
-------  -----------------  -----------
11       That's Amore       1  
12       Christmas Blues    1  
13       My Way             2  

sqlite> -- This fails because the value inserted into the trackartist column (3)
sqlite> -- does not correspond to row in the artist table.
sqlite> INSERT INTO track VALUES(14, 'Mr. Bojangles', 3);
SQL error: foreign key constraint failed

sqlite> -- This succeeds because a NULL is inserted into trackartist. A
sqlite> -- corresponding row in the artist table is not required in this case.
sqlite> INSERT INTO track VALUES(14, 'Mr. Bojangles', NULL);

sqlite> -- Trying to modify the trackartist field of the record after it has 
sqlite> -- been inserted does not work either, since the new value of trackartist (3)
sqlite> -- Still does not correspond to any row in the artist table.
sqlite> UPDATE track SET trackartist = 3 WHERE trackname = 'Mr. Bojangles';
SQL error: foreign key constraint failed

sqlite> -- Insert the required row into the artist table. It is then possible to
sqlite> -- update the inserted row to set trackartist to 3 (since a corresponding
sqlite> -- row in the artist table now exists).
sqlite> INSERT INTO artist VALUES(3, 'Sammy Davis Jr.');
sqlite> UPDATE track SET trackartist = 3 WHERE trackname = 'Mr. Bojangles';

sqlite> -- Now that "Sammy Davis Jr." (artistid = 3) has been added to the database,
sqlite> -- it is possible to INSERT new tracks using this artist without violating
sqlite> -- the foreign key constraint:
sqlite> INSERT INTO track VALUES(15, 'Boogie Woogie', 3);

如您所料,不可能通过删除或更新 艺术家表中的行来将数据库操作到违反外键约束的状态:

sqlite> -- Attempting to delete the artist record for "Frank Sinatra" fails, since
sqlite> -- the track table contains a row that refer to it.
sqlite> DELETE FROM artist WHERE artistname = 'Frank Sinatra';
SQL error: foreign key constraint failed

sqlite> -- Delete all the records from the track table that refer to the artist
sqlite> -- "Frank Sinatra". Only then is it possible to delete the artist.
sqlite> DELETE FROM track WHERE trackname = 'My Way';
sqlite> DELETE FROM artist WHERE artistname = 'Frank Sinatra';

sqlite> -- Try to update the artistid of a row in the artist table while there
sqlite> -- exists records in the track table that refer to it. 
sqlite> UPDATE artist SET artistid=4 WHERE artistname = 'Dean Martin';
SQL error: foreign key constraint failed

sqlite> -- Once all the records that refer to a row in the artist table have
sqlite> -- been deleted, it is possible to modify the artistid of the row.
sqlite> DELETE FROM track WHERE trackname IN('That''s Amore', 'Christmas Blues');
sqlite> UPDATE artist SET artistid=4 WHERE artistname = 'Dean Martin';

SQLite 使用以下术语:

  • 父表是外键约束引用的表本节示例中的父表是 艺术家表。一些书籍和文章将此称为 参考表,这可以说更正确,但往往会导致混淆。

  • 子表是应用了外键约束的表和包含 REFERENCES 子句的表本节示例使用轨道表作为子表。其他书籍和文章将此称为 引用表

  • 父键是外键约束引用的父表中的列或列集。这通常是父表的主键,但并非总是如此。父键必须是父表中的一个或多个命名列,而不是rowid

  • 子键是子表中受外键约束并包含 REFERENCES 子句的列或列集

如果对于子表中的每一行,一个或多个子键列为 NULL,或者父表中存在一行,其每个父键列包含的值等于其中的值关联的子键列。

在上一段中,术语“相等”表示使用此处指定的规则比较值时相等。以下说明适用:

  • 比较文本值时, 始终使用与父键列关联 的整理顺序。

  • 比较值时,如果父键列具有亲和力,则在执行比较之前将该亲和力应用于子键值。

2. 启用外键支持

为了在 SQLite 中使用外键约束,必须在编译库时既不定义SQLITE_OMIT_FOREIGN_KEY也不 定义SQLITE_OMIT_TRIGGER如果定义了 SQLITE_OMIT_TRIGGER 但未定义 SQLITE_OMIT_FOREIGN_KEY,则 SQLite 的行为与版本 3.6.19 (2009-10-14) 之前的行为相同 - 外键定义被解析并且可以使用PRAGMA foreign_key_list进行查询,但外键约束并未强制执行。PRAGMA foreign_keys命令在此配置中是空操作。如果定义了 OMIT_FOREIGN_KEY,则甚至无法解析外键定义(尝试指定外键定义是语法错误)。

假设库是在启用外键约束的情况下编译的,它仍然必须在运行时由应用程序使用 PRAGMA foreign_keys命令启用。例如:

sqlite> PRAGMA foreign_keys = ON;

默认情况下禁用外键约束(为了向后兼容),因此必须为每个数据库连接 单独启用(但是请注意,SQLite 的未来版本可能会更改,以便默认启用外键约束。细心的开发人员不会对默认情况下是否启用外键做出任何假设,而是根据需要启用或禁用它们。)应用程序还可以使用PRAGMA foreign_keys语句来确定当前是否启用了外键。以下命令行会话演示了这一点:

sqlite> PRAGMA foreign_keys;
0
sqlite> PRAGMA foreign_keys = ON;
sqlite> PRAGMA foreign_keys;
1
sqlite> PRAGMA foreign_keys = OFF;
sqlite> PRAGMA foreign_keys;
0

提示:如果命令“PRAGMA foreign_keys”没有返回任何数据而不是包含“0”或“1”的单行,那么您使用的 SQLite 版本不支持外键(因为它早于 3.6.19 或因为它是使用SQLITE_OMIT_FOREIGN_KEYSQLITE_OMIT_TRIGGER定义编译的)。

无法在多语句事务中间启用或禁用外键约束(当 SQLite 未处于自动提交模式时)。尝试这样做不会返回错误;它根本没有效果。

3.要求和建议的数据库索引

通常,外键约束的父键是父表的主键。如果它们不是主键,那么父键列必须共同受 UNIQUE 约束或具有 UNIQUE 索引。如果父键列具有 UNIQUE 索引,则该索引必须使用在 CREATE TABLE 语句中为父表指定的排序规则序列。例如,

CREATE TABLE parent(a PRIMARY KEY, b UNIQUE, c, d, e, f);
CREATE UNIQUE INDEX i1 ON parent(c, d);
CREATE INDEX i2 ON parent(e);
CREATE UNIQUE INDEX i3 ON parent(f COLLATE nocase);

CREATE TABLE child1(f, g REFERENCES parent(a));                        -- Ok
CREATE TABLE child2(h, i REFERENCES parent(b));                        -- Ok
CREATE TABLE child3(j, k, FOREIGN KEY(j, k) REFERENCES parent(c, d));  -- Ok
CREATE TABLE child4(l, m REFERENCES parent(e));                        -- Error!
CREATE TABLE child5(n, o REFERENCES parent(f));                        -- Error!
CREATE TABLE child6(p, q, FOREIGN KEY(p, q) REFERENCES parent(b, c));  -- Error!
CREATE TABLE child7(r REFERENCES parent(c));                           -- Error!

作为表child1 、 child2 和 child3一部分创建外键约束都很好。声明为表child4的一部分的外键是错误的,因为即使父键列被索引,索引也不是唯一的。child5的外键 是一个错误,因为即使父键列具有唯一索引,该索引使用不同的整理顺序。child6child7不正确,因为虽然它们的父键上都有 UNIQUE 索引,但这些键与单个 UNIQUE 索引的列不完全匹配。

如果数据库模式包含需要查看多个表定义才能识别的外键错误,那么在创建表时不会检测到这些错误。相反,此类错误会阻止应用程序准备以使用外键的方式修改子表或父表内容的 SQL 语句。内容更改时报告的错误是“DML 错误”,模式更改时报告的错误是“DDL​​ 错误”。因此,换句话说,需要同时查看子项和父项的配置错误的外键约束是 DML 错误。外键 DML 错误的英文错误消息通常是“外键不匹配”,但如果父表不存在,也可以是“没有这样的表”。

  • 父表不存在,或者
  • 外键约束中指定的父键列不存在,或者
  • 外键约束中命名的父键列不是父表的主键,并且不受使用 CREATE TABLE 中指定的整理顺序的唯一约束,或者
  • 子表引用了父表的主键,没有指定主键列,父表的主键列数与子键列数不匹配。

上面的最后一个项目符号如下所示:

CREATE TABLE parent2(a, b, PRIMARY KEY(a,b));

CREATE TABLE child8(x, y, FOREIGN KEY(x,y) REFERENCES parent2);        -- Ok
CREATE TABLE child9(x REFERENCES parent2);                             -- Error!
CREATE TABLE child10(x,y,z, FOREIGN KEY(x,y,z) REFERENCES parent2);    -- Error!

相反,如果只需查看子表的定义就可以识别外键错误,而无需查阅父表定义,则子表的 CREATE TABLE语句将失败。因为错误发生在架构更改期间,所以这是一个 DDL 错误。无论建表时是否启用外键约束,都会报外键DDL错误。

子键列不需要索引,但它们几乎总是有益的。回到第 1 节中的示例,每次应用程序从artist表(父表)中删除一行时,它都会执行与以下 SELECT 语句等效的语句来搜索track表(子表)中的引用行。

SELECT rowid FROM track WHERE trackartist = ?

在哪里 ?上面的替换为从艺术家表 中删除的记录的artistid列的值(回想一下,trackartist列是子键,artistid 列是父键)。或者,更一般地说:

SELECT rowid FROM <child-table> WHERE <child-key> = :parent_key_value

如果此 SELECT 返回任何行,则 SQLite 断定从父表中删除该行将违反外键约束并返回错误。如果修改了父键的内容或向父表中插入了新行,则可能会运行类似的查询。如果这些查询不能使用索引,它们将被迫对整个子表进行线性扫描。在非平凡的数据库中,这可能非常昂贵。

因此,在大多数实际系统中,应该在每个外键约束的子键列上创建一个索引。子键索引不必(通常也不会)是 UNIQUE 索引。再次回到第 1 节中的示例,高效实现外键约束的完整数据库模式可能是:

CREATE TABLE artist(
  artistid    INTEGER PRIMARY KEY, 
  artistname  TEXT
);
CREATE TABLE track(
  trackid     INTEGER,
  trackname   TEXT, 
  trackartist INTEGER REFERENCES artist
);
CREATE INDEX trackindex ON track(trackartist);

上面的块使用简写形式来创建外键约束。将“REFERENCES <parent-table> ”子句附加到列定义会创建一个外键约束,将列映射到<parent-table>的主键。有关详细信息,请参阅CREATE TABLE 文档。

4.高级外键约束特性

4.1. 复合外键约束

复合外键约束是子键和父键都是复合键的约束。例如,考虑以下数据库模式:

CREATE TABLE album(
  albumartist TEXT,
  albumname TEXT,
  albumcover BINARY,
  PRIMARY KEY(albumartist, albumname)
);

CREATE TABLE song(
  songid     INTEGER,
  songartist TEXT,
  songalbum TEXT,
  songname   TEXT,
  FOREIGN KEY(songartist, songalbum) REFERENCES album(albumartist, albumname)
);

在这个系统中,歌曲表中的每个条目都需要映射到专辑表中具有相同艺术家和专辑组合的条目。

父键和子键必须具有相同的基数。在 SQLite 中,如果任何子键列(在本例中为 songartist 和 songalbum)为 NULL,则不需要在父表中有相应的行。

4.2. 延迟外键约束

SQLite 中的每个外键约束都分为立即约束或延迟约束。默认情况下,外键约束是立即的。到目前为止介绍的所有外键示例都是直接外键约束。

如果一条语句修改了数据库的内容,以至于在该语句结束时违反了直接外键约束,则会抛出异常并恢复该语句的效果。相比之下,如果一条语句修改了数据库的内容,从而违反了延迟外键约束,则不会立即报告违规情况。在事务尝试提交之前,不会检查延迟的外键约束只要用户有一个打开的事务,就允许数据库以违反任意数量的延迟外键约束的状态存在。但是, 只要违反外键约束 , COMMIT就会失败。

如果当前语句不在显式事务( BEGIN / COMMIT / ROLLBACK块)内,则在语句执行完毕后立即提交隐式事务。在这种情况下,延迟约束的行为与立即约束相同。

要将外键约束标记为延迟,其声明必须包含以下子句:

DEFERRABLE INITIALLY DEFERRED                -- A deferred foreign key constraint

指定外键约束的完整语法作为CREATE TABLE文档的一部分提供。用以下任何一个替换上面的短语会创建一个直接的外键约束。

NOT DEFERRABLE INITIALLY DEFERRED            -- An immediate foreign key constraint
NOT DEFERRABLE INITIALLY IMMEDIATE           -- An immediate foreign key constraint
NOT DEFERRABLE                               -- An immediate foreign key constraint
DEFERRABLE INITIALLY IMMEDIATE               -- An immediate foreign key constraint
DEFERRABLE                                   -- An immediate foreign key constraint

defer_foreign_keys pragma可用于临时将所有外键约束更改为延迟,而不管它们是如何声明的。

以下示例说明了使用延迟外键约束的效果。

-- Database schema. Both tables are initially empty. 
CREATE TABLE artist(
  artistid    INTEGER PRIMARY KEY, 
  artistname  TEXT
);
CREATE TABLE track(
  trackid     INTEGER,
  trackname   TEXT, 
  trackartist INTEGER REFERENCES artist(artistid) DEFERRABLE INITIALLY DEFERRED
);

sqlite3> -- If the foreign key constraint were immediate, this INSERT would
sqlite3> -- cause an error (since as there is no row in table artist with
sqlite3> -- artistid=5). But as the constraint is deferred and there is an
sqlite3> -- open transaction, no error occurs.
sqlite3> BEGIN;
sqlite3>   INSERT INTO track VALUES(1, 'White Christmas', 5);

sqlite3> -- The following COMMIT fails, as the database is in a state that
sqlite3> -- does not satisfy the deferred foreign key constraint. The
sqlite3> -- transaction remains open.
sqlite3> COMMIT;
SQL error: foreign key constraint failed

sqlite3> -- After inserting a row into the artist table with artistid=5, the
sqlite3> -- deferred foreign key constraint is satisfied. It is then possible
sqlite3> -- to commit the transaction without error.
sqlite3>   INSERT INTO artist VALUES(5, 'Bing Crosby');
sqlite3> COMMIT;

数据库处于不满足延迟外键约束的状态时,可以释放嵌套保存点事务。另一方面,事务保存点(在当前没有打开的事务时打开的非嵌套保存点)受到与 COMMIT 相同的限制 - 在数据库处于这种状态时尝试释放它将会失败。

如果 COMMIT 语句(或事务 SAVEPOINT 的 RELEASE)失败,因为数据库当前处于违反延迟外键约束的状态并且当前存在 嵌套保存点,则嵌套保存点保持打开状态。

4.3. 删除和更新操作

外键 ON DELETE 和 ON UPDATE 子句用于配置从父表中删除行 (ON DELETE) 或修改现有行的父键值 (ON UPDATE) 时发生的操作。单个外键约束可以为 ON DELETE 和 ON UPDATE 配置不同的操作。外键操作在很多方面类似于触发器。

与 SQLite 数据库中每个外键关联的删除和更新操作是“无操作”、“限制”、“设置空”、“设置默认值”或“级联”之一。如果未明确指定操作,则默认为“无操作”。

  • NO ACTION:配置“NO ACTION”意味着:当父键被修改或从数据库中删除时,不采取任何特殊操作。

  • 严格:“RESTRICT”操作意味着当存在一个或多个子键映射到父键时,禁止应用程序删除(对于删除限制)或修改(对于更新限制)父键。RESTRICT 操作和普通外键约束强制执行的效果之间的区别在于,RESTRICT 操作处理在字段更新后立即发生 - 而不是在当前语句的末尾,因为它会使用立即约束,或者在末尾当前事务的处理方式与延迟约束一样。即使它所附加的外键约束被延迟,如果删除或修改具有依赖子键的父键,配置 RESTRICT 操作也会导致 SQLite 立即返回错误。

  • SET NULL:如果配置的操作是“SET NULL”,那么当父键被删除(对于 ON DELETE SET NULL)或修改(对于 ON UPDATE SET NULL)时,映射的子表中所有行的子键列到父键被设置为包含 SQL NULL 值。

  • SET DEFAULT:“SET DEFAULT”操作类似于“SET NULL”,不同之处在于每个子键列都设置为包含该列的默认值而不是 NULL。有关如何将默认值分配给表列的详细信息,请参阅CREATE TABLE 文档。

  • CASCADE:“CASCADE”操作将对父键的删除或更新操作传播到每个从属子键。对于“ON DELETE CASCADE”操作,这意味着子表中与已删除父行关联的每一行也将被删除。对于“ON UPDATE CASCADE”操作,这意味着存储在每个依赖子键中的值被修改以匹配新的父键值。

例如,如下所示向外键添加“ON UPDATE CASCADE”子句可增强第 1 节中的示例架构,以允许用户更新 artistid(外键约束的父键)列而不会破坏参照完整性:

-- Database schema
CREATE TABLE artist(
  artistid    INTEGER PRIMARY KEY, 
  artistname  TEXT
);
CREATE TABLE track(
  trackid     INTEGER,
  trackname   TEXT, 
  trackartist INTEGER REFERENCES artist(artistid) ON UPDATE CASCADE
);

sqlite> SELECT * FROM artist;
artistid  artistname       
--------  -----------------
1         Dean Martin      
2         Frank Sinatra    

sqlite> SELECT * FROM track;
trackid  trackname          trackartist
-------  -----------------  -----------
11       That's Amore       1
12       Christmas Blues    1
13       My Way             2  

sqlite> -- Update the artistid column of the artist record for "Dean Martin".
sqlite> -- Normally, this would raise a constraint, as it would orphan the two
sqlite> -- dependent records in the track table. However, the ON UPDATE CASCADE clause
sqlite> -- attached to the foreign key definition causes the update to "cascade"
sqlite> -- to the child table, preventing the foreign key constraint violation.
sqlite> UPDATE artist SET artistid = 100 WHERE artistname = 'Dean Martin';

sqlite> SELECT * FROM artist;
artistid  artistname       
--------  -----------------
2         Frank Sinatra    
100       Dean Martin      

sqlite> SELECT * FROM track;
trackid  trackname          trackartist
-------  -----------------  -----------
11       That's Amore       100
12       Christmas Blues    100  
13       My Way             2  

配置 ON UPDATE 或 ON DELETE 操作并不意味着不需要满足外键约束。例如,如果配置了“ON DELETE SET DEFAULT”操作,但父表中没有对应子键列默认值的行,删除父键,而依赖子键存在,仍然会导致外键违反。例如:

-- Database schema
CREATE TABLE artist(
  artistid    INTEGER PRIMARY KEY, 
  artistname  TEXT
);
CREATE TABLE track(
  trackid     INTEGER,
  trackname   TEXT, 
  trackartist INTEGER DEFAULT 0 REFERENCES artist(artistid) ON DELETE SET DEFAULT
);

sqlite> SELECT * FROM artist;
artistid  artistname       
--------  -----------------
3         Sammy Davis Jr.

sqlite> SELECT * FROM track;
trackid  trackname          trackartist
-------  -----------------  -----------
14       Mr. Bojangles      3

sqlite> -- Deleting the row from the parent table causes the child key
sqlite> -- value of the dependent row to be set to integer value 0. However, this
sqlite> -- value does not correspond to any row in the parent table. Therefore
sqlite> -- the foreign key constraint is violated and an is exception thrown.
sqlite> DELETE FROM artist WHERE artistname = 'Sammy Davis Jr.';
SQL error: foreign key constraint failed

sqlite> -- This time, the value 0 does correspond to a parent table row. And
sqlite> -- so the DELETE statement does not violate the foreign key constraint
sqlite> -- and no exception is thrown.
sqlite> INSERT INTO artist VALUES(0, 'Unknown Artist');
sqlite> DELETE FROM artist WHERE artistname = 'Sammy Davis Jr.';

sqlite> SELECT * FROM artist;
artistid  artistname       
--------  -----------------
0         Unknown Artist

sqlite> SELECT * FROM track;
trackid  trackname          trackartist
-------  -----------------  -----------
14       Mr. Bojangles      0

熟悉SQLite 触发器 的人会注意到上面示例中演示的“ON DELETE SET DEFAULT”操作在效果上与以下 AFTER DELETE 触发器类似:

CREATE TRIGGER on_delete_set_default AFTER DELETE ON artist BEGIN
  UPDATE child SET trackartist = 0 WHERE trackartist = old.artistid;
END;

每当外键约束的父表中的一行被删除,或者存储在父键列中的值被修改时,事件的逻辑顺序是:

  1. 执行适用的 BEFORE 触发程序,
  2. 检查本地(非外键)约束,
  3. 更新或删除父表中的行,
  4. 执行任何所需的外键操作,
  5. 执行适用的 AFTER 触发器程序。

ON UPDATE 外键操作和 SQL 触发器之间有一个重要区别。只有当父键的值被修改使得新的父键值不等于旧的时,才会执行 ON UPDATE 操作。例如:

-- Database schema
CREATE TABLE parent(x PRIMARY KEY);
CREATE TABLE child(y REFERENCES parent ON UPDATE SET NULL);

sqlite> SELECT * FROM parent;
x
----
key

sqlite> SELECT * FROM child;
y
----
key

sqlite> -- Since the following UPDATE statement does not actually modify
sqlite> -- the parent key value, the ON UPDATE action is not performed and
sqlite> -- the child key value is not set to NULL.
sqlite> UPDATE parent SET x = 'key';
sqlite> SELECT IFNULL(y, 'null') FROM child;
y
----
key

sqlite> -- This time, since the UPDATE statement does modify the parent key
sqlite> -- value, the ON UPDATE action is performed and the child key is set
sqlite> -- to NULL.
sqlite> UPDATE parent SET x = 'key2';
sqlite> SELECT IFNULL(y, 'null') FROM child;
y
----
null

5. CREATE、ALTER 和 DROP TABLE 命令

本节介绍CREATE TABLEALTER TABLEDROP TABLE命令与 SQLite 的外键交互的方式。

无论是否 启用外键约束, CREATE TABLE命令的操作都是一样的。创建表时不检查外键约束的父键定义。没有什么可以阻止用户创建引用不存在的父表的外键定义,或者引用不存在或未由 PRIMARY KEY 或 UNIQUE 约束共同绑定的父键列。

启用外键约束时,ALTER TABLE命令在两个方面的工作方式不同:

  • 不可能使用“ALTER TABLE ... ADD COLUMN”语法来添加包含 REFERENCES 子句的列,除非新列的默认值为 NULL。尝试这样做会返回错误。

  • 如果使用“ALTER TABLE ... RENAME TO”命令重命名作为一个或多个外键约束的父表的表,则修改外键约束的定义以通过新名称引用父表. 存储在sqlite_schema 表中的子 CREATE TABLE 语句或语句的文本被修改以反映新的父表名称。

如果在准备时启用外键约束,则 DROP TABLE命令执行隐式DELETE以在删除表之前从表中删除所有行。隐式 DELETE 不会导致触发任何 SQL 触发器,但可能会调用外键操作或违反约束。如果直接外键约束被违反,则 DROP TABLE 语句失败并且表不会被删除。如果违反延迟的外键约束,那么当用户尝试提交事务时,如果外键约束违规仍然存在,则会报告错误。作为隐式 DELETE 的一部分遇到的任何“外键不匹配”错误都将被忽略。

对ALTER TABLEDROP TABLE命令 进行这些增强的目的 是确保它们不能用于创建包含外键违规的数据库,至少在启用外键约束时是这样。不过,这一规则有一个例外。如果父键不受作为父表定义的一部分创建的 PRIMARY KEY 或 UNIQUE 约束的约束,但由于使用CREATE INDEX创建的索引而受到 UNIQUE 约束的约束命令,则可能会填充子表而不会导致“外键不匹配”错误。如果 UNIQUE 索引从数据库模式中删除,那么父表本身也被删除,不会报告错误。但是,数据库可能处于外键约束的子表包含不引用任何父表行的行的状态。如果数据库模式中的所有父键都受到作为父表定义的一部分添加的 PRIMARY KEY 或 UNIQUE 约束的约束,而不是外部 UNIQUE 索引,则可以避免这种情况。

上述DROP TABLEALTER TABLE命令 的属性仅在启用外键时适用。如果用户认为它们不受欢迎,那么解决方法是在执行 DROP 或 ALTER TABLE 命令之前使用PRAGMA foreign_keys禁用外键约束。当然,虽然禁用了外键约束,但没有什么可以阻止用户违反外键约束并因此创建内部不一致的数据库。

6.限制和不支持的功能

本节列出了一些其他地方未提及的限制和省略的功能。

  1. 不支持 MATCH 子句。根据 SQL92,可以将 MATCH 子句附加到复合外键定义以修改处理子键中出现的 NULL 值的方式。如果指定了“MATCH SIMPLE”,则如果一个或多个子键值为 NULL,则子键不需要对应于父表的任何行。如果指定了“MATCH FULL”,那么如果任何一个子键值为NULL,则不需要父表中的对应行,但所有子键值必须为NULL。最后,如果外键约束声明为“MATCH PARTIAL”并且子键值之一为 NULL,则父表中必须至少存在一行,其非 NULL 子键值与父键值匹配。

    SQLite 解析 MATCH 子句(即,如果您指定一个子句,则不会报告语法错误),但不会强制执行它们。SQLite 中的所有外键约束都像指定了 MATCH SIMPLE 一样处理。

  2. 不支持在延迟模式和立即模式之间切换约束。许多系统允许用户在运行时在延迟模式和立即模式之间切换单个外键约束(例如使用 Oracle“SET CONSTRAINT”命令)。SQLite 不支持这个。在 SQLite 中,外键约束在创建时被永久标记为延迟或立即。

  3. 外键操作的递归限制。SQLITE_MAX_TRIGGER_DEPTHSQLITE_LIMIT_TRIGGER_DEPTH设置决定了触发器程序递归的 最大允许深度。出于这些限制的目的, 外键操作被视为触发程序。PRAGMA recursive_triggers设置不影响外键操作的操作无法禁用递归外键操作。