将 NULL 插入具有默认值的 NOT NULL 列

Posted

技术标签:

【中文标题】将 NULL 插入具有默认值的 NOT NULL 列【英文标题】:Inserting NULL into NOT NULL columns with Default Value 【发布时间】:2014-11-19 07:42:42 【问题描述】:

作为背景知识,我们在工作中使用 Zend Framework 2Doctrine。 Doctrine 将始终插入NULL 用于我们自己不填充的值。通常这没关系,就好像该字段有一个默认值一样,那么它应该用这个默认值填充该字段。

对于我们其中一台运行 MySQL 5.6.16 的服务器,如下所示的查询运行良好。尽管NULL 被插入到一个不可为空的字段中,但 mysql 在插入时使用其默认值填充该字段。

在我们另一台运行 MySQL 5.6.20 的服务器上,我们运行下面的查询,但它失败了,因为它抱怨“field_with_default_value”不能为空。

INSERT INTO table_name(id, field, field_with_default_value) 
VALUES(id_value, field_value, NULL);

Doctrine 本身不支持将“DEFAULT”传递到它构建的查询中,因此这不是一个选项。我认为这一定是某种 MySQL 服务器的东西,好像它在一个版本中可以正常工作,但在另一个版本中不行,但不幸的是我不知道这可能是什么。我们的 SQL 模式在两台服务器上也是相同的 ('NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION')。

我应该提一下,如果我真的在 Workbench 中运行上述 SQL,它仍然不能以相同的方式工作。所以这不是一个真正的 Doctrine 问题,而绝对是某种 MySQL 问题。

对此的任何帮助将不胜感激。

【问题讨论】:

你确定SQL模式是一样的吗?见dev.mysql.com/doc/refman/5.0/en/data-type-defaults.html 是的,我什至将 SQL 模式从一台服务器复制到另一台服务器,以仔细检查它们是否相同。 你有 STRICT_TRANS_TABLES sql 模式,所以如果你在命令行运行这两个查询,你应该有相同的效果。也许教义改变了这一点? (不使用 Doctrine 的另一个原因) 教义没有改变这一点。在命令行上运行查询也不起作用。这不是 Doctrine 问题,它特定于我们为 MySQL 设置的服务器。 【参考方案1】:

如果您从语句中省略列(名称和值),则将使用默认值。

一些相关建议:

您的表中没有任何“非空”默认值,并且可空列没有非空默认值。让所有值都从应用程序中设置。 不要将业务逻辑放在数据库端。

仅在真正需要时定义默认值,并且仅用于不可为空的列。并在不再需要时删除默认值。 (它们在 alter table 运行中派上用场,设置新列的值,但随后立即运行新的(便宜的!)alter 以删除默认值)

上面所说的“空”,与类型有关: - 0 表示数字列, - '' 用于 varchar/varbinary 列, - '1970-01-01 12:34:56' 用于时间戳, - 等等。

这为应用程序节省了许多往返数据库的次数。如果创建的行是完全可预测的,那么应用程序不需要在创建后读取它,就可以知道它变成了什么。 (假设:没有触发器,没有级联)

对于 MySQL,我们只对这些严格规则做了一些特定的例外:

名为 mysql_row_foo 的列仅由数据库设置。例子:

mysql_row_created_at timestamp(6) not null default '1970-01-01 12:34:56.000000', mysql_row_updated_at timestamp(6) null 默认 null 更新 current_timestamp,

欢迎使用非空列的唯一索引,以防止重复数据。例如,在看起来像 (id++, name) 的表中的 lookup.brand.name。

mysql_row_foo 列类似于列属性。例如,它们被数据同步工具使用。一般应用程序不会读取它们,它们会将应用程序端的时间戳存储为纪元值。例子:

valid_until_epoch int unsigned not null default 0, last_seen_epoch_ms bigint 不为 null 默认 0,

【讨论】:

【参考方案2】:

我在升级 MySQL 后遇到了同样的问题。原来有一个设置允许对 NOT NULL 时间戳字段进行 NULL 插入并获取默认值。

explicit_defaults_for_timestamp=0

这记录在https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_explicit_defaults_for_timestamp

【讨论】:

【参考方案3】:

根据我的研究,我会说它既可以是“你”的东西,也可以是“MySQL”的东西。使用SHOW CREATE TABLE table_name; 检查您的表定义。记下使用NOT NULL 定义的所有字段。

MySQL 5.6 Reference Manual: 13.2.5 INSERT syntax 声明:

将 NULL 插入已声明为 NOT NULL 的列中。为了 多行 INSERT 语句或 INSERT INTO ... SELECT 语句, 该列设置为列数据的隐式默认值 类型。数字类型为 0,字符串为空字符串 ('') 类型,以及日期和时间类型的“零”值。插入 ... SELECT 语句的处理方式与多行插入相同 因为服务器不检查从 SELECT 到的结果集 看看它是否返回单行。 (对于单行 INSERT,否 将 NULL 插入 NOT NULL 列时会出现警告。反而, 该语句因错误而失败。)

这意味着您使用哪种 SQL 模式并不重要。如果您正在执行单行INSERT(根据您的示例代码)并将NULL 值插入到使用NOT NULL 定义的列中,则它不应该工作。

同时,具有讽刺意味的是,如果您只是从值列表中省略值,MySQL 手册会这样说,SQL 模式在这种情况下确实很重要

如果您不是在严格的 SQL 模式下运行,则任何列都没有显式 给定值设置为其默认值(explicitimplicit)。为了 例如,如果您指定的列列表未命名所有 表中的列,未命名的列设置为其默认值。 默认值分配在第 11.6 节“数据类型”中描述 默认值”。另请参见第 1.7.3.3 节,“对 Invalid 的约束 数据”。

因此,你赢不了! ;-) 开玩笑。要做的事情是接受 MySQL 表字段上的 NOT NULL 确实意味着 在执行单行时,我不会接受字段的 NULL 值INSERT,无论 SQL 模式如何

话虽如此,手册中的以下内容也是正确的:

对于没有显式 DEFAULT 的 NOT NULL 列的数据输入 子句,如果 INSERT 或 REPLACE 语句不包含 列,或 UPDATE 语句将列设置为 NULL,MySQL 处理 根据当时有效的SQL模式的列:

如果启用了严格的SQL模式,事务处理会出错 表和语句被回滚。对于非事务表, 发生错误,但如果第二行或后续行发生这种情况 在多行语句中,前面的行将被 插入

如果未启用严格模式,MySQL 将列设置为 隐式 列数据类型的默认值

所以,振作起来。在业务逻辑(对象)中设置默认值,并让数据层从中获取方向。数据库默认值似乎是个好主意,但如果它们不存在,你会想念它们吗?如果一棵树倒在森林里……

【讨论】:

【参考方案4】:

MySQL 实际上按预期工作,and that behavior seems to be there to stay。 MariaDB also works the same way now.

删除"strict mode" (STRICT_TRANS_TABLES & STRICT_ALL_TABLES) 应该恢复到以前的行为,但我个人没有任何运气(也许我做错了什么,但我的@987654329 @ & @@SESSION.sql_mode 不包含严格模式)。

我认为这个问题的最佳解决方案是依赖 php 级别的默认值,而不是依赖数据库来提供它们。 There is an existing answer that explains it pretty well. cmets 也很有帮助。

这样,您还可以获得额外的好处,即您的模型/实体在实例化时将具有默认值,而不是在插入数据库时​​。此外,如果您想在插入后向用户显示这些值,您可以这样做,而无需在您的 INSERT 之后执行额外的 SELECT 查询。

显示默认值的另一种替代方法是使用RETURNING clause,这在 PostgreSQL 中可用,但在 MySQL 中(目前还没有)。它可能会在将来的某个时候添加,但现在是MariaDB only has it for DELETE statements。但是,我相信在 PHP 级别使用默认值仍然是优越的。即使您从未插入记录,它仍将包含默认值。自从将其付诸实践以来,我从未回头并使用过数据库默认值。

【讨论】:

【参考方案5】:

根据文档,一切都按预期工作。

测试用例:

mysql> use test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 5.6.16    |
+-----------+
1 row in set (0.00 sec)

mysql> SELECT @@GLOBAL.sql_mode 'sql_mode::GLOBAL',
              @@SESSION.sql_mode 'sql_mode::SESSION';
+------------------------+------------------------+
| sql_mode::GLOBAL       | sql_mode::SESSION      |
+------------------------+------------------------+
| NO_ENGINE_SUBSTITUTION | NO_ENGINE_SUBSTITUTION |
+------------------------+------------------------+
1 row in set (0.00 sec)

mysql> SET SESSION sql_mode := 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@GLOBAL.sql_mode 'sql_mode::GLOBAL',
              @@SESSION.sql_mode 'sql_mode::SESSION';
+------------------------+-----------------------------------------------------------------------------------------------------------------+
| sql_mode::GLOBAL       | sql_mode::SESSION                                                                                               |
+------------------------+-----------------------------------------------------------------------------------------------------------------+
| NO_ENGINE_SUBSTITUTION | NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION |
+------------------------+-----------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> SHOW CREATE TABLE `table_name`;
+------------+----------------------------------------------------------------------------+
| Table      | Create Table                                                               |
+------------+----------------------------------------------------------------------------+
| table_name | CREATE TABLE `table_name` (                                                |
|            |        `id` INT(11) UNSIGNED NOT NULL,                                     |
|            |        `field` VARCHAR(20) DEFAULT NULL,                                   |
|            |        `field_with_default_value` VARCHAR(20) NOT NULL DEFAULT 'myDefault' |
|            | ) ENGINE=InnoDB DEFAULT CHARSET=latin1                                     |
+------------+----------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> INSERT INTO `table_name`(`id`, `field`, `field_with_default_value`)
       VALUES
       (1, 'Value', NULL);
ERROR 1048 (23000): Column 'field_with_default_value' cannot be null

是否可以发布表格结构的相关部分以了解我们如何提供帮助?

更新

MySQL 5.7,使用触发器,可以为问题提供可能的解决方案:

Changes in MySQL 5.7.1 (2013-04-23, Milestone 11)

...

如果列声明为 NOT NULL,则不允许插入 NULL 到列中或将其更新为 NULL。然而,这种约束 即使有 BEFORE INSERT(或 BEFORE UPDATE 触发器),将列设置为非 NULL 值。现在的约束 根据 SQL 标准,在语句末尾检查。 (漏洞 #6295, Bug#11744964)。

...

可能的解决方案:

mysql> use test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 5.7.4-m14 |
+-----------+
1 row in set (0.00 sec)

mysql> DELIMITER $$

mysql> CREATE TRIGGER `trg_bi_set_default_value` BEFORE INSERT ON `table_name`
       FOR EACH ROW
       BEGIN
          IF (NEW.`field_with_default_value` IS NULL) THEN
             SET NEW.`field_with_default_value` := 
                (SELECT `COLUMN_DEFAULT`
                 FROM `information_schema`.`COLUMNS`
                 WHERE `TABLE_SCHEMA` = DATABASE() AND 
                       `TABLE_NAME` = 'table_name' AND
                       `COLUMN_NAME` = 'field_with_default_value');
          END IF;
       END$$

mysql> DELIMITER ;

mysql> INSERT INTO `table_name`(`id`, `field`, `field_with_default_value`)
       VALUES
       (1, 'Value', NULL);
Query OK, 1 row affected (0.00 sec)

mysql> SELECT `id`, `field`, `field_with_default_value` FROM `table_name`;
+----+-------+--------------------------+
| id | field | field_with_default_value |
+----+-------+--------------------------+
|  1 | Value | myDefault                |
+----+-------+--------------------------+
1 row in set (0.00 sec)

【讨论】:

看我的回答。我认为它解释了这种情况。

以上是关于将 NULL 插入具有默认值的 NOT NULL 列的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL:条件插入默认值

从 parquet 文件将具有默认值的数据加载到 Redshift

我将如何同时插入具有所有不同子查询值的同一行,几乎就像通过子查询进行迭代一样?

将 0 传递给具有默认值的函数

SQL:如果存在则更新,否则插入...但对于具有不同值的多行

将具有默认值的列添加到 SQL Server 中的现有表