MySQL 触发器使用不同的 TIMEZONE

Posted

技术标签:

【中文标题】MySQL 触发器使用不同的 TIMEZONE【英文标题】:MySQL Triggers use different TIMEZONE 【发布时间】:2019-06-19 05:02:53 【问题描述】:

我创建了一个触发器来为购买设置一个 uniq 名称 (ref),使用时区作为 create_date 字段。 它工作正常,直到我注意到重复的引用,其中一个引用的引用中的日期与 create_date 字段中的日期不匹配(差异的 1 天)?!!

BEGIN
SET NEW.reference := concat(
(SELECT name FROM provider WHERE id = NEW.provider_id),
 date_format(NEW.create_date, '%Y%m%d'), '/',
  (SELECT LPAD(IFNULL(MAX(SUBSTRING_INDEX(reference, '/', -1)) + 1, 0), 3, '0') 
   FROM purchase
   WHERE date_format(NEW.create_date, '%Y%m%d') = date_format(create_date, '%Y%m%d')
      AND NEW.provider_id = provider_id
  )
);
END

有人知道发生了什么或更好的方法吗?

PS:create_date 字段中的日期是正确的,ref 中使用的 NEW.create_date 中的日期是错误的(可能是客户端时区的 b/c,因为我们将其作为字符串发送)


更新

表格结构:

购买:

CREATE TABLE purchase
(
  id             int          auto_increment primary key,
  provider_id    int          not null,
  create_date    timestamp    null,
  create_user    int          null,
  change_date    timestamp    null,
  change_user    int          null,
  group_id       int          null,
  reference      varchar(45)  null
);

CREATE INDEX purchase_reference_index ON purchase (reference);
CREATE INDEX purchase_provider_index ON purchase (provider_id);

提供者:

CREATE TABLE provider
(
  id           int auto_increment primary key,
  name         varchar(45) null,
  constraint name_uniq unique (name)
);

查询示例:

INSERT INTO purchase (provider_id, create_date, create_user, group_id)
VALUE (4, '2019-01-30 02:36:58', 1, 3);

当我从使用服务器时区的会话中选择2019-01-29 23:36:58 时,2019-01-30 02:36:58 在数据库中保存为2019-01-29 23:36:58

我用来设置时区的函数:

function update_timezone($timezone = null)

    if (is_null($timezone)) $timezone = __SERVER_TIMEZONE;

    if (in_array($timezone, timezone_identifiers_list())) 
        date_default_timezone_set($timezone);

        $tz = (new DateTime('now', new DateTimeZone(date_default_timezone_get())))->format('P');
        $conn = Database::Connect();
        Database::NonQuery("SET time_zone = '$tz';", $conn);
    

我的期望:

reference === 'provider_name20190129/00X'

我得到了什么:

reference === 'provider_name20190130/00Y'

如何重现问题:

CREATE DATABASE test;

CREATE TABLE purchase
(
  id             int          auto_increment primary key,
  provider_id    int          not null,
  create_date    timestamp    null,
  create_user    int          null,
  change_date    timestamp    null,
  change_user    int          null,
  group_id       int          null,
  reference      varchar(45)  null
);

CREATE INDEX purchase_reference_index ON purchase (reference);
CREATE INDEX purchase_provider_index ON purchase (provider_id);

CREATE TABLE provider
(
  id           int auto_increment primary key,
  name         varchar(45) null,
  constraint name_uniq unique (name)
);

CREATE TRIGGER test.purchase_ref_insert
  BEFORE INSERT
  ON test.purchase
  FOR EACH ROW
BEGIN
  SET NEW.reference := concat(
      (SELECT name FROM provider WHERE id = NEW.provider_id),
      date_format(NEW.create_date, '%Y%m%d'), '/',
      (SELECT LPAD(IFNULL(MAX(SUBSTRING_INDEX(reference, '/', -1)) + 1, 0), 3, '0')
       FROM purchase
       WHERE date_format(NEW.create_date, '%Y%m%d') = date_format(create_date, '%Y%m%d')
         AND NEW.provider_id = provider_id
      )
  );
END
;

INSERT INTO provider (name) VALUE ('test');


SET time_zone = '+00:00';
INSERT INTO purchase (provider_id, create_date, create_user, group_id)
VALUE (1, '2019-01-30 02:36:58', 1, 3);


SET time_zone = '+05:00';
INSERT INTO purchase (provider_id, create_date, create_user, group_id)
VALUE (1, '2019-01-30 02:36:58', 1, 3);

SET time_zone = '-05:00';
INSERT INTO purchase (provider_id, create_date, create_user, group_id)
VALUE (1, '2019-01-30 02:36:58', 1, 3);


SET time_zone = '+00:00';
SELECT create_date, reference FROM purchase;

这是我得到的:

【问题讨论】:

表结构和数据在这里会有所帮助 考虑使用TIMESTAMP 而不是DATETIME。并确保每台计算机都设置了适当的时区。 @RaymondNijland 我更新了问题,请再看看 @RickJames 我正在使用 DATATIME 并切换到 TIMESTAMP(当客户想要使用不同的时区时,我的问题就开始了) 【参考方案1】:

您可以在触发器中使用@@session.time_zone 将给定的时间戳CONVERT_TZ 转换为SYSTEM time_zone,然后再与表进行比较。

CREATE TRIGGER purchase_ref_insert
  BEFORE INSERT ON purchase
  FOR EACH ROW
BEGIN
 SET @ts := CONVERT_TZ(NEW.create_date, @@session.time_zone, 'SYSTEM');
 SET @providerName := (SELECT name FROM provider WHERE id = NEW.provider_id);
 SET @refPostfix := (
         SELECT 
         LPAD(IFNULL(MAX(SUBSTRING_INDEX(reference, '/', -1)) + 1, 0), 3, '0')
         FROM purchase
         WHERE NEW.provider_id = provider_id
           AND CAST(CONVERT_TZ(create_date, @@session.time_zone, 'SYSTEM') AS DATE) = CAST(@ts AS DATE)
     );
 SET NEW.create_date_tz = @@session.time_zone;
 SET NEW.reference := CONCAT(@providerName, DATE_FORMAT(@ts, '%Y%m%d'), '/', @refPostfix);
END;

您可以在表格中添加一个额外的列。 一个将包含插入记录时使用的会话时区。

create_date_tz varchar(6)   not null default 'SYSTEM'

这样,您仍然可以在客户的时区中显示 create_date。

dbfiddle here

的测试

例如:

 SELECT 
  id, create_date, reference, create_date_tz,
  CONVERT_TZ(create_date, 'SYSTEM', create_date_tz) as ts_at_TZ
 FROM purchase;

返回:

id | create_date         | reference        | create_date_tz | ts_at_TZ           
 1 | 2019-01-30 07:30:01 | test20190130/000 | -05:00         | 2019-01-30 02:30:01
 2 | 2019-01-30 02:30:02 | test20190130/001 | +00:00         | 2019-01-30 02:30:02
 3 | 2019-01-29 21:30:03 | test20190129/000 | +05:00         | 2019-01-30 02:30:03

【讨论】:

使用CONVERT_TZ 似乎是一个干净的方法,您还移动了provider_id 条件以便更好地使用primary key :) 只是为了让您知道,我们需要将create_date 转换为@ 987654322@ 也是为了避免重复条目。 我们真的不需要create_date_tz mysql将所有时区日期转换为UTC,当客户要求时,他使用会话时区将其转换回来 好吧,create_date_tz 更像是一个建议。它并不是真正需要的,但是当您想根据客户的当地时间进行查询时,它可以为您提供帮助。但我不明白您所说的“我们需要转换 create_date”是什么意思,因为 @ts 变量是 NEW.create_date,就在服务器的时区中。 我附上了 db<>fiddle 测试,看看,当添加另一个 +05:00 +10:00 测试时,我们得到了重复的 ref /000 为了防止这种情况发生,我使用了CAST(CONVERT_TZ(create_date, @@session.time_zone, 'SYSTEM') AS DATE) = CAST(@ts AS DATE)【参考方案2】:

我找到了解决方案:D

我只需要在触发器SET time_zone = 'SYSTEM';中设置时区

date_format 不遵守本地化时区

CREATE TRIGGER test.purchase_ref_insert
  BEFORE INSERT
  ON test.purchase
  FOR EACH ROW
  BEGIN
    SET @time_zone_tmp := @@time_zone;
    SET time_zone = 'SYSTEM';
    SET NEW.reference := concat(
        (SELECT name FROM provider WHERE id = NEW.provider_id),
        date_format(NEW.create_date, '%Y%m%d'), '/',
        (SELECT LPAD(IFNULL(MAX(SUBSTRING_INDEX(reference, '/', -1)) + 1, 0), 3, '0')
         FROM purchase
         WHERE date_format(NEW.create_date, '%Y%m%d') = date_format(create_date, '%Y%m%d')
           AND NEW.provider_id = provider_id
        )
    );
    SET time_zone = @time_zone_tmp;
  END
;

【讨论】:

以上是关于MySQL 触发器使用不同的 TIMEZONE的主要内容,如果未能解决你的问题,请参考以下文章

MySql:触发器

十MySQL触发器

我的MYSQL学习心得 触发器

MySQL触发器-条件触发器语法

MySQL玩转触发器

MySQL优化之——触发器