MySQL 带有 MariaDB 驱动程序的服务器产生日期排序错误

MySQL Server with MariaDB driver produces dates collation error

背景与问题

我的公司决定从 MySQL 驱动程序更改为 MariaDB driver/connector,并且仍然使用 MySQL 服务器数据库作为我们 Spring 应用程序的数据库服务器.

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

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();

版本:


了解

阅读MySQL documentation后,它提到:

MySQL Connector/J is flexible in the way it handles conversions between MySQL data types and Java data types.

In general, any MySQL data type can be converted to a java.lang.String, and any numeric type can be converted to any of the Java numeric types, although round-off, overflow, or loss of precision may occur.

也许之前没有出现错误,因为MySQL连接器处理字符串和日期之间的转换,现在错误是由于尝试比较两种不同的数据类型造成的。

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


测试

我认为是 encoding/character 设置问题,并将所有表和列更改为 utf8_general_ci这没有解决问题。

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

?问题 ?

  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 解决步骤:

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

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


@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 驱动程序的决定。

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

重要提示:

下面的文本和代码行中的每个地方 latin1 只是一个例子,而实际的字符集(和可能的排序规则)值需要根据根据实验步骤中在日志中找到的信息。


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

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

and/or

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
   ...

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

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 并通过设置实际会话变量(从 character_set_client 开始)替换 SET NAMES 来进一步缩小范围。

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

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

等等

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

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

解决方法是将 LocalDateTime 转换为时间戳:

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 相同,如果对象 class 未知,对象将被序列化并发送到服务器。因为你想象的 org.joda.time.LocalDateTime 并没有在 JDBC 中定义,你一定已经遇到了一些惊喜。发送到服务器的数据不是时间值。

可能的解决方法:

b.valueDate <= :today改为b.valueDate < CURDATE() + INTERVAL 1 DAY