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();
版本:
- Java: 7
- MySQL: 5.5
- 玛丽亚驱动程序:1.4.6
了解
阅读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 驱动程序 - 并得到以下结果:
尽管在使用 MariaDB 时测试通过了,但这不是一个选项。
测试 #3 和 4 有效,如果查询参数转换为日期:... and b.valueDate <= DATE(:today)";
但这意味着对代码进行许多更改。
测试 #2 是(唯一失败的)并且是我想遵循的选项,因为它意味着更改量较少。但是,我似乎无法让它工作。
?问题 ?
有没有办法使用 MySQL 和 MariaDB 连接器而不会导致这个问题?
是否有比 DATE(:today)
迄今为止的所有参数更好的选择?
另一种解决方案?谢谢。
更新:
这些是代码中设置的数据源属性:
dataSource.setJdbcUrl("jdbc:mysql://"+ hostname + ":3306/"
+ databaseName +
"?useUnicode=true&" +
"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.COLUMNS
和 INFORMATION_SCHEMA.TABLES
都是 CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;
。
@elenst 解决步骤:
- 已启用general_log;
- 确认
NAMES
和 character_set_results
的值在使用时相同(latin1 和 NULL) MySQL 连接器;
- 切换回 MariaDB 并设置这些值
- 运行 与 application/query 并且 错误仍然存在 。
- 我也试过
?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 驱动程序的决定。
非常感谢所有愿意花时间提供帮助的人。
重要提示:
下面的文本和代码行中的每个地方 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
背景与问题
我的公司决定从 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();
版本:
- Java: 7
- MySQL: 5.5
- 玛丽亚驱动程序:1.4.6
了解
阅读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 驱动程序 - 并得到以下结果:
尽管在使用 MariaDB 时测试通过了,但这不是一个选项。
测试 #3 和 4 有效,如果查询参数转换为日期:
... and b.valueDate <= DATE(:today)";
但这意味着对代码进行许多更改。测试 #2 是(唯一失败的)并且是我想遵循的选项,因为它意味着更改量较少。但是,我似乎无法让它工作。
?问题 ?
有没有办法使用 MySQL 和 MariaDB 连接器而不会导致这个问题?
是否有比
DATE(:today)
迄今为止的所有参数更好的选择?另一种解决方案?谢谢。
更新:
这些是代码中设置的数据源属性:
dataSource.setJdbcUrl("jdbc:mysql://"+ hostname + ":3306/"
+ databaseName +
"?useUnicode=true&" +
"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.COLUMNS
和 INFORMATION_SCHEMA.TABLES
都是 CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;
。
@elenst 解决步骤:
- 已启用general_log;
- 确认
NAMES
和character_set_results
的值在使用时相同(latin1 和 NULL) MySQL 连接器; - 切换回 MariaDB 并设置这些值
- 运行 与 application/query 并且 错误仍然存在 。
- 我也试过
?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 驱动程序的决定。
非常感谢所有愿意花时间提供帮助的人。
重要提示:
下面的文本和代码行中的每个地方 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