将 NULL 插入具有默认值的 NOT NULL 列
Posted
技术标签:
【中文标题】将 NULL 插入具有默认值的 NOT NULL 列【英文标题】:Inserting NULL into NOT NULL columns with Default Value 【发布时间】:2014-11-19 07:42:42 【问题描述】:作为背景知识,我们在工作中使用 Zend Framework 2 和 Doctrine。 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 模式下运行,则任何列都没有显式 给定值设置为其默认值(explicit 或 implicit)。为了 例如,如果您指定的列列表未命名所有 表中的列,未命名的列设置为其默认值。 默认值分配在第 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 列的主要内容,如果未能解决你的问题,请参考以下文章
从 parquet 文件将具有默认值的数据加载到 Redshift
我将如何同时插入具有所有不同子查询值的同一行,几乎就像通过子查询进行迭代一样?