【问题标题】:Inserting NULL into NOT NULL columns with Default Value将 NULL 插入具有默认值的 NOT NULL 列
【发布时间】: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 设置的服务器。

标签: php mysql doctrine-orm zend-framework2 default-value


【解决方案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)
          

          【讨论】:

          • 看我的回答。我认为它解释了这种情况。
          猜你喜欢
          • 2017-03-14
          • 2011-04-12
          • 2018-10-22
          • 2011-07-20
          • 2020-08-20
          • 2016-08-30
          • 2018-01-25
          • 2011-03-12
          • 1970-01-01
          相关资源
          最近更新 更多