Postgresql 允许不存在的外键

Postgresql allows nonexistent foreign keys

我有一个使用 postgresql 12 的 rails 应用程序。最近,我编写了一些测试并看到了一些奇怪的行为。 我有一个 countries table。它的架构如下所示:

qq2_test=# \d countries;
                                               Table "public.countries"
        Column         |              Type              | Collation | Nullable |                Default                
-----------------------+--------------------------------+-----------+----------+---------------------------------------
 id                    | bigint                         |           | not null | nextval('countries_id_seq'::regclass)
 domain                | character varying              |           | not null | ''::character varying
 root_city_id          | bigint                         |           |          | 
 language_id           | bigint                         |           | not null | 
 currency_id           | bigint                         |           | not null | 
 google_tag_manager_id | character varying              |           | not null | ''::character varying
 created_at            | timestamp(6) without time zone |           | not null | 
 updated_at            | timestamp(6) without time zone |           | not null | 
Indexes:
    "countries_pkey" PRIMARY KEY, btree (id)
    "index_countries_on_domain" UNIQUE, btree (domain)
    "index_countries_on_currency_id" btree (currency_id)
    "index_countries_on_language_id" btree (language_id)
Foreign-key constraints:
    "fk_rails_6f479b409c" FOREIGN KEY (language_id) REFERENCES languages(id)
    "fk_rails_7cac1212c7" FOREIGN KEY (currency_id) REFERENCES currencies(id)
    "fk_rails_beac36a0bd" FOREIGN KEY (root_city_id) REFERENCES cities(id)
Referenced by:
    TABLE "country_translations" CONSTRAINT "fk_rails_0c4ee35f26" FOREIGN KEY (country_id) REFERENCES countries(id) ON DELETE CASCADE
    TABLE "countries_languages" CONSTRAINT "fk_rails_556e7398aa" FOREIGN KEY (country_id) REFERENCES countries(id) ON DELETE CASCADE
    TABLE "cities" CONSTRAINT "fk_rails_996e05be41" FOREIGN KEY (country_id) REFERENCES countries(id)

如您所见,我在 currency_idlanguage_id 字段上都有外键约束。 当我 运行 我的测试时,我看到 table:

中有记录
qq2_test=# select * from countries;
    id     | domain | root_city_id | language_id | currency_id | google_tag_manager_id |         created_at         |         updated_at         
-----------+--------+--------------+-------------+-------------+-----------------------+----------------------------+----------------------------
 665664142 | com    |              |  1019186233 |   432072940 |                       | 2020-10-23 06:20:49.288637 | 2020-10-23 06:20:49.288637
 169150333 | by     |              |  1019186233 |   432072940 |                       | 2020-10-23 06:20:49.288637 | 2020-10-23 06:20:49.288637
(2 rows)

我有两份测试记录,它们有语言和货币参考。但是他们的 table 是空的:

qq2_test=# select * from currencies;
 id | name | symbol | created_at | updated_at 
----+------+--------+------------+------------
(0 rows)

qq2_test=# select * from languages;
 id | name | locale | image | created_at | updated_at 
----+------+--------+-------+------------+------------
(0 rows)

为什么 postgresql 允许 countries table 中不存在的引用?

只有两个选项:

  1. 外键约束为NOT VALID.

    此类约束会针对新条目进行检查,但现有条目可能会违反它们。

    但这会出现在您的 \d 输出中,所以事实并非如此。

  2. 您有数据损坏。

    除了硬件问题或软件错误,可能的解释是:

    • 有人设置 session_replication_role = replica 这样触发器就不会触发。

    • 超级用户 运行

      ALTER TABLE countries DISABLE TRIGGER ALL;
      

假设您是唯一使用数据库的人(因为您谈论的是小型 1-2 行测试),我猜您的 Rails 应用程序(或相应的驱动程序)正在禁用触发器或外键检查。像这样绕过外键检查是完全可能的:

edb=# show session_replication_role ;
 session_replication_role 
--------------------------
 origin
(1 row)

edb=# create table city (id int primary key, name text);
CREATE TABLE
edb=# select * from city;
 id | name 
----+------
(0 rows)

edb=# create table person (id int, name text, city int references city(id));
CREATE TABLE
edb=# insert into person values (1,'foo',1);
ERROR:  insert or update on table "person" violates foreign key constraint "person_city_fkey"
DETAIL:  Key (city)=(1) is not present in table "city".
edb=# set session_replication_role to replica;
SET
edb=# insert into person values (1,'foo',1);
INSERT 0 1
edb=# select * from person;
 id | name | city 
----+------+------
  1 | foo  |    1
(1 row)

edb=# select * from city;
 id | name 
----+------
(0 rows)

我建议您再次临时设置 log_statement = all 和 运行 测试——然后在您的 Postgres 服务器日志中查看(对于 Ubuntu,默认应该是 /var/log/postgresql/postgresql-12-main.log ) 什么可能会禁用您的外键约束检查,然后相应地解决您的发现。