【问题标题】:MySQL Server with MariaDB driver produces dates collation error带有 MariaDB 驱动程序的 MySQL 服务器产生日期排序错误
【发布时间】:2017-05-24 20:23:44
【问题描述】:

背景与问题

我公司决定从 MySQL 驱动改为 MariaDB 驱动/连接器,仍然使用 MySQL Server DB 作为 Spring App 的数据库服务器。

在此迁移过程中,我发现了与驱动程序/连接器如何处理日期相关的问题,这导致了以下错误:

Illegal mix of collations for operation '<='

在运行以下查询时会发生这种情况,请注意查询最后一行的日期比较。

String sql2 = "select coalesce(sum(b.value), 0) " +
            "  from acc_sub_account_booking b " +
            "    join acc_sub_account sa ON sa.id = b.subAccount_id " +
            "    join km_cash_bond cb ON cb.virtualPayInAccountNumber = sa.iban " +
            "    join km_rented_object ro ON ro.id = cb.rentedObject_id " +
            " where b.bookingType in ('PAY_IN', 'PAY_OUT') " +
            "       and ro.id = :rentedObjectId " +
            "       and b.valueDate <= :today";

Object sum1 = entityManager.createNativeQuery(sql).
            setParameter("rentedObjectId", pRentedObject.getId()).
            setParameter("today", new LocalDateTime(d.getYear(), d.getMonthOfYear(), d.getDayOfMonth(), 23, 59)).
            getSingleResult();

版本:

  • Java:7
  • MySQL:5.5
  • 玛丽亚驱动程序:1.4.6


了解

看了MySQL documentation之后提到:

MySQL Connector/J 在处理 MySQL 数据类型和 Java 数据类型之间的转换方面非常灵活。

一般来说,任何 MySQL 数据类型都可以转换为 java.lang.String,任何数字类型都可以转换为任何 Java 数字类型,尽管可能会出现舍入、溢出或精度损失。

也许之前没有发生该错误,因为 MySQL 连接器处理了 String 和 Date 之间的转换,而现在该错误是由于尝试比较两种不同的数据类型而导致的。

使用 MariaDB 作为服务器时不会出现此错误,可能是 Maria 中的转换处理是在数据库级别而不是在连接器中完成的。


测试

我认为是编码/字符集问题,将所有表和列都更改为utf8_general_ci这并没有解决问题。

我运行了一些测试 - 在通过应用程序运行查询时始终使用 MariaDB 驱动程序 - 并得到以下结果:

  • 虽然使用 MariaDB 时测试通过,但这不是一种选择

  • 如果查询参数转换为日期:... and b.valueDate &lt;= DATE(:today)";,则测试 #3 和 4 有效,但这意味着对代码进行许多更改。

  • 测试#2 是(唯一失败的并且)我想遵循的选项,因为它意味着更改量较少。但是,我似乎无法让它工作。

?问题?

  1. 有没有办法使用 MySQL 和 MariaDB 连接器而不会导致此问题?

  2. 有没有比将DATE(:today) 的所有参数强制转换为迄今为止更好的选择?

  3. 另一种解决方案?谢谢。


更新:

这些是代码中设置的数据源属性:

dataSource.setJdbcUrl("jdbc:mysql://"+ hostname + ":3306/"
            + databaseName +
            "?useUnicode=true&amp;" +
            "characterEncoding=utf-8");

更新 #2:

更多信息:

还执行了以下集合:

ALTER DATABASE km CHARACTER SET utf8 COLLATE utf8_general_ci;
SET collation_connection = 'utf8_general_ci';
SET collation_server = 'utf8_general_ci';

另外,每个INFORMATION_SCHEMA.COLUMNSINFORMATION_SCHEMA.TABLES 都是CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;


@elenst 解决步骤:

  • 启用general_log;
  • 确认使用 MySQL 连接器时 NAMEScharacter_set_results 的值相同(latin1NULL);
  • 切换回 MariaDB 并设置这些值
  • 使用应用程序/查询运行,错误仍然存​​在
  • 我也试过?sessionVariables=character_set_client=latin1?sessionVariables=character_set_client=utf8,结果是一样的。

@DiegoDupin 无法将Timestamp.valueOf() 应用于Joda Time LocalDateTime。你可以把它包装起来:

  • setParameter("today", Timestamp.valueOf(String.valueOf(new LocalDateTime(d.getYear(), d.getMonthOfYear(), d.getDayOfMonth(), 23, 59)))) 但会导致时间戳格式错误
  • 不过setParameter("today", new LocalDateTime(d.getYear(), d.getMonthOfYear(), d.getDayOfMonth(), 23, 59).toDate())工作

问题依然存在,系统的其他部分,由于驱动的变化,仍然可以分担这个故障。


@RickJames:SHOW CREATE TABLE acc_sub_account_booking。比较是在 valueDate 字段上完成的,输入 date,尽管在另一个(类似)查询中存在 datetime 字段也会发生同样的情况。

| acc_sub_account_booking | CREATE TABLE `acc_sub_account_booking` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `creationDate` datetime DEFAULT NULL,
  `lastModifiedDate` datetime DEFAULT NULL,
  `bookingText` varchar(255) DEFAULT NULL,
  `bookingType` varchar(255) DEFAULT NULL,
  `uuid` varchar(255) DEFAULT NULL,
  `value` decimal(19,2) DEFAULT NULL,
  `valueDate` date DEFAULT NULL,
  `zkaGVC` int(4) NOT NULL,
  `subAccount_id` bigint(20) DEFAULT NULL,
  `bankStatementDate` date DEFAULT NULL,
  `counterpartHolder` varchar(255) DEFAULT NULL,
  `counterpartIban` varchar(255) DEFAULT NULL,
  `customerReference` varchar(255) DEFAULT NULL,
  `endToEndReference` varchar(255) DEFAULT NULL,
  `returnReason` varchar(255) DEFAULT NULL,
  `customerSpecificInformations` text,
  `counterpartBic` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uuid` (`uuid`),
  KEY `FK_SAB_SA` (`subAccount_id`),
  CONSTRAINT `FK_SAB_SA` FOREIGN KEY (`subAccount_id`) REFERENCES `acc_sub_account` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3407 DEFAULT CHARSET=utf8 |

最终更新:

这里列出的问题可以通过转换为Java Date 对象来解决,而不是直接使用JodaTime

setParameter("today", new LocalDateTime(d.getYear(), d.getMonthOfYear(), d.getDayOfMonth(), 23, 59).toDate())

尽管如此,还是发现了其他问题,最后我公司决定恢复使用 MariaDB 驱动程序的决定。

非常感谢所有愿意花时间提供帮助的人。

【问题讨论】:

  • valueDate 的数据类型是什么? (或SHOW CREATE TABLE:today被替换后的sql可以显示吗?
  • 您能否启用查询日志并查看服务器端实际收到的内容? LocalDateTime 没有在驱动程序中处理,也许它是由 Spring 以某种方式处理的,对此并不熟悉。但也许你最好使用 java.sql.Timestamp 或类似的东西。
  • @VladislavVaintroub 查询日志未显示“已翻译”查询:15 准备 select coalesce(sum(u.betrag), 0) from km_rented_object ro join km_cash_bond cb ON cb.rentedObject_id = ro。 id join konto k ON k.uuid = cb.truster_account_uuid join umsatz u ON u.konto_id = k.id where ro.id = ?和 u.vorfallKennung 在 (100, 200) 和 u.buchungszeitpunkt
  • 好吧,那么它是准备好的语句,服务器端。您可以在 JDBC URL 中使用 useServerPrepStmts=false 来确保客户端准备。但它可能会向您显示序列化的二进制 LocalDateTime。因为这就是此驱动程序中的 setObject 对未知类型的工作方式。
  • buchungszeitpunkt?或b.valueDate??

标签: java mysql jdbc database-connection mariadb


【解决方案1】:

重要提示:

latin1 下面的文本和代码行中的所有内容只是一个示例,而实际的字符集(可能还有排序规则)值需要根据日志中找到的信息来选择如实验步骤中所述。


如果 MariaDB 和 MySQL Connector/J 在其他方面相同的情况下存在与字符集和排序规则相关的差异,这通常是因为 MySQL Connector/J 可以在新连接时自动执行这些 SET 语句:

SET NAMES <character set>;
SET character_set_results = NULL;

和/或

SET character_set_results = <character set>;
SET collation_connection = <collation>;

对于后两者,应该有对应的连接属性,所以比较明显。对于前两个,算法更加晦涩难懂。

MariaDB 连接器不这样做,它只会设置连接属性中提供的会话变量。

发现确切差异的简单方法是:

在服务器上启用常规日志(SET GLOBAL general_log=1); 使用 MySQL Connector/J 运行脚本; 检查一般日志,找到一个连接的开始,它应该看起来像这样:

   99 Query     /* mysql-connector-java-5.1.39 ( Revision: 3289a357af6d09ecc1a10fd3c26e95183e5790ad ) */SELECT  @@session.auto_increment_increment AS auto_increment_increment, @@character_set_client AS character_set_client, @@character_set_connection AS character_set_connection, @@character_set_results AS character_set_results, @@character_set_server AS character_set_server, @@init_connect AS init_connect, @@interactive_timeout AS interactive_timeout, @@license AS license, @@lower_case_table_names AS lower_case_table_names, @@max_allowed_packet AS max_allowed_packet, @@net_buffer_length AS net_buffer_length, @@net_write_timeout AS net_write_timeout, @@query_cache_size AS query_cache_size, @@query_cache_type AS query_cache_type, @@sql_mode AS sql_mode, @@system_time_zone AS system_time_zone, @@time_zone AS time_zone, @@tx_isolation AS tx_isolation, @@wait_timeout AS wait_timeout
   99 Query     SET NAMES latin1
   99 Query     SET character_set_results = NULL
   99 Query     SET autocommit=1
   ...

SETs 中的值可能不同)。 然后,对于一个实验,添加完全相同的语句,以便在建立连接后立即从您的代码中直接执行,例如

con= DriverManager.getConnection(...);
Statement st= con.createStatement();
st.execute("SET NAMES latin1");
st.execute("SET character_set_results = NULL");

(或您将使用的任何更好的语法,当然还有您在常规日志中看到的相同值)。

现在使用 MariaDB Connector/J 重新编译并运行。检查日志,确保语句被执行。检查结果。

如果 MySQL 和 MariaDB 连接器之间的行为差​​异现在消失了,那么您已经找到了原因。

您可以通过尝试删除可能不重要的character_set_results 来进一步缩小范围,并通过设置实际会话变量替换SET NAMES,从character_set_client 开始。

最后,在 MariaDB 连接器中,会话变量可以通过将它们添加到连接行来配置

"jdbc:mysql://localhost:3306/test?sessionVariables=character_set_client=latin1"

等等

【讨论】:

  • 如果你使用的是utf8/utf8mb4,那么我认为连接线需要两个东西:useUnicode=yes&amp;characterEncoding=UTF-8
  • 这些是 MySQL 连接器选项,MariaDB 连接器/J 没有。
  • 哎呀,太淘气了。向 MariaDB 提交错误。
  • 已经有at least one。也许如果人们报告得更多,这个决定会被重新考虑;但问题是,MariaDB Connector/J 从未被定位为 MySQL Connector/J 的替代品,它不提供相同的功能。 It was developed specifically as a lightweight JDBC connector.
  • 事情正朝着utf8mb4 的方向发展,成为首选的CHARACTER SET(在MySQL 之外又名“UTF-8”)。我希望所有的连接器等都在这种思维框架中。
【解决方案2】:

Query.setParameter 依赖于 PrepareStatement.setObject(...)

MariaDB jdbc 驱动程序不处理 setObject 中的 LocalDateTime 对象
我只是创建this issue 来处理它。

一种解决方法是将 LocalDateTime 转换为 Timestamp :

Object sum1 = entityManager.createNativeQuery(sql).
            setParameter("rentedObjectId", pRentedObject.getId()).
            setParameter("today", Timestamp.valueOf(new LocalDateTime(d.getYear(), d.getMonthOfYear(), d.getDayOfMonth(), 23, 59))).
            getSingleResult());

编辑

如果 LocalDateTime 对应于 java.time.LocalDateTime,则使用 Timestamp.valueOf((LocalDateTime)x) 是一种解决方法。

如果 LocalDateTime 对应 org.joda.time.LocalDateTime,那么 toDate() 是一个解决方案。

在这种特殊情况下,MySQL 驱动程序的工作方式与 MariaDB 相同,如果 Object 类未知,则 Object 将被序列化并发送到服务器。由于您想象中的 org.joda.time.LocalDateTime 没有在 JDBC 中定义,因此您在这里一定已经遇到了一些惊喜。发送到服务器的数据不是时间值。

【讨论】:

  • 无法将 Timestamp.valueOf() 应用到 Joda Time LocalDateTime。您可以像这样包装它:setParameter("today", Timestamp.valueOf(String.valueOf(new LocalDateTime(d.getYear(), d.getMonthOfYear(), d.getDayOfMonth(), 23, 59)))) 但它会导致时间戳格式错误。根据我的测试,参数必须是Stringjava.util.Date。这将解决这种情况,但我不知道还能存在多少。
  • 编辑后:我同意你的看法。它应该从一开始就被定义为Date。现在,toDate() 确实可以解决这个问题,但不能保证没有其他类似情况。我知道 MariaDB 不仅仅是一个插件,但问题仍然是这个查询是如何与 MySQL 驱动程序一起工作的?为什么不使用 MariaDB 驱动程序?
  • 也许在 MySQL 中它是用 toString() 序列化的;) 我认为,对于 JDBC,在 setObject 中使用本机 JDBC 类型,例如 java.sql.Timestamp 或本机 JDK 类型是合适的,比如 java.util.Date,或者(叹气)LocalDateTime,而不是 Joda,Java8 的
  • @VladislavVaintroub 那——“用 toString() 序列化”——也是我的猜测。使用本机 JDBC 类型是绝对正确的。我不知道 PrepStmts 中使用了 JodaTime(公司的新手)。因为它起作用了,所以每个人都认为它没问题......但是现在它不起作用,所以让修复开始吧:)
【解决方案3】:

一种可能的解决方法:

b.valueDate &lt;= :today 更改为b.valueDate &lt; CURDATE() + INTERVAL 1 DAY

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-22
    • 1970-01-01
    • 2017-09-01
    • 2022-10-07
    • 2017-05-23
    • 2014-10-24
    相关资源
    最近更新 更多