Jdbi: ZonedDateTime 的内置映射器丢弃时区信息

创建于 2017-12-21  ·  43评论  ·  资料来源: jdbi/jdbi

存在ZonedDateTime的内置映射器,看起来像直接支持 SQL 的TIMESTAMP WITH TIME ZONE ,但映射器不这样做。

我认为公开区域感知类型的映射器是不正确的。

bug feature improvement

最有用的评论

老实说,我同意@findepi 的观点,他说 jdbi 不应该为实际上无法准确映射的事物提供映射器,例如未持久化的时区/偏移量......这里的“妥协”想法听起来非常危险对我来说 - 我一直认为完整的数据完整性是任何项目中的最高优先级之一。 用户不需要再次检查库是否正常工作。

我宁愿在测试运行期间发现运行时异常 jdbi 没有为给定类型提供映射器,然后自己编写一个,而不是找出谁知道 - jdbi 提供的映射器何时偷偷改变我的数据。

我有一个大型单元测试,它写入然后读取多种类型的值,以验证 db 本身 (hsqldb) 和 jdbi 的实现正确性/数据完整性,因此我可以编写 jdbi 映射器/参数来处理任何问题。 我现在不得不写几个,因为 hsqldb 目前基本上不支持 java.time 尽管规范这么说(巨大的错误)。 我还没有想过测试带有区域/偏移量而不是我的系统的 java.time 对象,所以我只是这样做了,果然,它们都返回偏移量/区域重置为系统值,但由于 jdbi 的映射到时间戳,不是因为 hsqldb 错误。

我建议从 jdbi3 中删除那些有缺陷的映射器,并将它们移到特定于供应商的工件(jdbi3-hsqldb 和其他)中,在那里它们的实现可以完全正确。

我也许可以自己贡献 jdbi-hsqldb,因为我已经为我自己的项目编写了所有必要的映射器和参数。

所有43条评论

您是对的,但这就是 JDBC 目前支持的全部内容。 我们必须将所有 java.time 类型转换为缺少时区/偏移数据的 java.util 或 java.sql 类型。

出于好奇,您在使用 Postgres 吗? 因为他们的 TIMESTAMP WITH TIME ZONE 的行为与您预期的不同。

你在使用 Postgres 吗? 因为他们的 TIMESTAMP WITH TIME ZONE 的行为与您预期的不同。

我知道。 (顺便说一句,对其他非常符合标准的数据库感到非常失望。)

您是对的,但这就是 JDBC 目前支持的全部内容。

我期待 JDBI 无需我手动执行此操作即可拉出 H2 的org.h2.api.TimestampWithTimeZone ,_somehow_。
由于我不知道任何“便携式”(跨不同驱动程序工作)方式来提取 TIMESTAMP W/TZ 值,因此我没有任何真正的解决方案。
但是,如果您不提取所有信息,则不应假装您这样做了 - 即 IMO 不应该有到ZonedDateTime内置映射器。 否则你就像 Postgres 一样——不是我所期望的。

我认为如果用户想要使用ZonedDateTime并且不关心它在应用服务器的时区而不是数据库的时区中返回,它仍然很方便。

顺便说一句,Dropwizard 为 JDBI2 使用了不同版本的映射器,用户可以在其中显式设置数据库的时区: https :

如果您可以提供有关如何使用 H2 获取/设置ZonedDateTime示例代码,我很乐意将其添加到插件中。

我认为如果用户想要使用 ZonedDateTime 并且不关心它在应用服务器的时区而不是数据库的时区中返回,它仍然很方便。

@artteam ,如果用户知道做出这个选择——我同意。

但是,AFAIR https://github.com/jdbi/jdbi/blob/master/core/src/main/java/org/jdbi/v3/core/mapper/BuiltInMapperFactory.java#L182甚至不适用于 H2 TS w TZ,因为该类型不支持rs.getTimestamp(..)

如果您可以提供有关如何使用 H2 获取/设置 ZonedDateTime 的示例代码,我很乐意将其添加到插件中。

我不知道有什么可移植的方式——我知道的唯一方法是(org.h2.api.TimestampWithTimeZone) rs.getObject(..)并且对于例如 Oracle 来说它是类似的,但是使用特定于 Oracle 的类。

通常,我更愿意被 API _forced_ 使用正确的转换(时间戳到LocalDateTime )并显式添加“zone-ing”。

你似乎不同意,这意味着,让这个问题保持开放是没有意义的。

我们在堆栈中做的一件事是,我们实际上断言 Java 的时区 == Postgres 的时区,在这种情况下,一切都恰到好处。 如果不匹配,我们是否应该发出警告? 在启用人们期望的功能和 TZ 正确之间,这是否是一个不错的折衷方案?

不知道你为什么这么快就关闭了,我觉得还有更多要讨论的。

即使我们不能为所有供应商一般性地解决这个问题,我们也绝对可以在每个供应商的基础上支持这一点。

Jdbi 附带的映射器和参数工厂可以简单地通过注册另一个映射器/参数来覆盖。 最后注册(每种类型)获胜。

如果您查看PostgresPlugin的源代码,您会看到它注册了JavaTimeMapperFactory ,它使用ResultSet.getObject(Class)LocalDateLocalTime提供替代映射器LocalDateTimeOffsetDateTime ,它们由 Postgres JDBC 驱动程序直接支持。

如果 H2 有办法在不删除区域的情况下存储ZonedDateTime ,我们可以修改H2DatabasePlugin以使用自定义映射器。

我现在重新打开这个,这里有很好的讨论, @findepi我们非常关心正确地做事! 我们也很务实,人们希望能够真正使用时间类型......

我是否从这次会议中正确理解 jdbi 团队有兴趣拥有例如 jdbi3-hsqldb 子项目/工件/...,它提供专门用于 hsqldb 的参数和映射器(它支持直接通过 setObject/getObject 使用 java.time 对象,而无需转换)?

绝对地。

老实说,我同意@findepi 的观点,他说 jdbi 不应该为实际上无法准确映射的事物提供映射器,例如未持久化的时区/偏移量......这里的“妥协”想法听起来非常危险对我来说 - 我一直认为完整的数据完整性是任何项目中的最高优先级之一。 用户不需要再次检查库是否正常工作。

我宁愿在测试运行期间发现运行时异常 jdbi 没有为给定类型提供映射器,然后自己编写一个,而不是找出谁知道 - jdbi 提供的映射器何时偷偷改变我的数据。

我有一个大型单元测试,它写入然后读取多种类型的值,以验证 db 本身 (hsqldb) 和 jdbi 的实现正确性/数据完整性,因此我可以编写 jdbi 映射器/参数来处理任何问题。 我现在不得不写几个,因为 hsqldb 目前基本上不支持 java.time 尽管规范这么说(巨大的错误)。 我还没有想过测试带有区域/偏移量而不是我的系统的 java.time 对象,所以我只是这样做了,果然,它们都返回偏移量/区域重置为系统值,但由于 jdbi 的映射到时间戳,不是因为 hsqldb 错误。

我建议从 jdbi3 中删除那些有缺陷的映射器,并将它们移到特定于供应商的工件(jdbi3-hsqldb 和其他)中,在那里它们的实现可以完全正确。

我也许可以自己贡献 jdbi-hsqldb,因为我已经为我自己的项目编写了所有必要的映射器和参数。

我同意这可能是更明智的方法,但此时 v3 已发布,猫已经不在了。

删除这些映射器或参数工厂对于依赖它们的任何现有用户来说都是一个重大变化。

如果有人已经依赖他们(是否意识到他们的缺陷),我认为这些用户将他们重新修补起来很简单:转换几乎是单行的。 另一方面,保留它们会导致越来越多的人像我一样认为它可以工作,因为它是提供的(老实说,谁知道所有技术规范,例如 jdbc 不支持时区?)并且可能会或可能不会后来发现他们的数据被错误处理了......
基本上,我认为允许它们留下来对未来的潜在损害远远大于现在对少数通过删除它们进行更新的人造成的损害。

@TheRealMarnes我认为删除一个功能(即使我们同意它不应该存在)并不是那么简单。 人们倾向于假设语义版本控制(无论项目是否遵循它)。 我不知道项目规则,但通常你最多会在 v3 中弃用它(这里弃用很棘手,因为没有可以标记为弃用的 API 方法,文档和日志是剩下的)并在 v4 中删除该功能,期待人们阅读发行说明/迁移指南。

我同意这样的事情从来都不是简单的,解决方案总是会让某人不高兴,但我倾向于认为最终证明手段是合理的,并且破坏有缺陷的系统以用更好的系统取代它从长远来看总是值得的。 而且我不认为期望人们阅读发行说明/迁移指南是不合理的......有维护和小更新,当然,但不是大更新。 如果不阅读新指南,则来自 jdbi2 的 Jdbi3 本身是无法使用的。

我想我可以支持禁用这些映射器/参数的解决方案,前提是恢复它们是单行的——也许将这些映射器移到插件中?

像非供应商特定的jdbi3-core-experimental
编辑:我的错,你的意思是jdbi.installPlugin(new ExperimentalMappersPlugin())

这就是想法,尽管这些并不是真正的“实验性”。

TimezoneInsensitiveJavaTimeMappers 然后:P

这不仅仅是映射器,它也是参数工厂。

JavaTimeYoloTZPlugin怎么样 :wink:

即使我们不能为所有供应商一般性地解决这个问题,我们也绝对可以在每个供应商的基础上支持这一点。

@qualidafial我最近了解到,在后 Java8 世界中,似乎有一个新兴的事实上的标准来检索日期/时间值。

首先,多一点背景:

  • 我创建了这个关于 JDBI 的TIMESTAMP WITH TIME ZONE转换为ZonedDateTime ,它丢弃了时区信息
  • 还有另一个问题,所有 JDBC API。 这就是TIMESTAMPDATETIME对象应该被检索的方式,即使用java.sql.{Timestamp,Date,Time}类。 这些类都从java.util.Date扩展。 任何人都记得Calendar是如何被引入的,以及java.util.Date处理年/月/日/小时/分钟/秒的方法被弃用了吗? 有一个很好的理由(我并不是说Calendar解决了所有问题)。

    • 如果您的 _JVM 时区_ 没有遵守这样的本地时间,则您无法从数据库中检索java.sql.Timestamp 。 因此,在美国运行的应用程序可以在某个共享数据库中存储2017-03-26 02:30:00 (没有区域!)的时间戳值,但是在欧洲运行的应用程序(例如Europe/Berlin作为 JVM 区域)不会能够检索到这个时间戳,因为当时欧洲有夏令时。

    • 哦,同样适用于java.sql.Date ,因为这几乎是相同的类。 java.sql.Date表示日期就像午夜的java.sql.Timestamp ,但有时没有午夜。 有些区域在午夜(当前或过去)发生夏令时更改。

    • 您是否认为java.sql.Time没有这个问题,因为它将检索时间表示为 1970-01-01 的时间戳? 不幸的是没有,因为有足够好的区域在那个日期改变政策(例如America/Bahia_BanderasAmerica/Hermosillo )。 如果 JVM 是这些区域之一,则使用 JDBC 的程序将无法检索某些TIME值。

一个非解决方案是使用 JDBC 旧 API 检索并将它们转换为新类。 但这通常是错误的:

  • ~ resultSet.getTimestamp(col).toLocalDateTime() ~
  • ~ resultSet.getDate(col).toLocalDate() ~
  • ~ resultSet.getTime(col).toLocalTime() ~

这编译并运行良好,除了这不能解决上述任何问题(即使检索日期也不起作用,如果 JVM 区域为Pacific/Apia并且检索的日期为2011-12-30 ;)

现在,到新兴的解决方案。 ResultSet有这个不太流行的 API,允许您要求 JDBC 驱动程序为您转换类型:

  • resultSet.getObject(col, LocalDateTime.class) // for TIMESTAMP
  • resultSet.getObject(col, LocalDate.class) // for DATE
  • resultSet.getObject(col, LocalTime.class) // for TIME
  • resultSet.getObject(col, ZonedDateTime.class) // for TIMESTAMP WITH TIME ZONE

这似乎适用于 PostgreSQL、MySQL、Oracle 和 H2 的 JDBC 驱动程序。 而且,神奇的是,规避了上述“JVM 区域中的不可表示性”的问题。
尽管这种转换尚未在例如 SQL Server 驱动程序中实现,但我认为这可以成为 JDBI 提供的 Java Time 类的_the_默认映射。

笔记:
JDBC 4.2 规范没有强制要求驱动程序必须实现这一点。 但是,规范要求PreparedStatement.setObjectRowSet.setObject接受 Java Time 类的实例并正确对待它们,因此驱动程序无论如何都需要明确支持这些类。

这很酷! 我主要担心的是,对使用深奥驱动程序的用户而言,更改为这种方法可能会表现为回归。 但也许通过发行说明中的​​适当消息,我们可以以正确的方式做事的名义进行这个小的技术性突破性更改......

@stevenschlansker你是对的。 我认为 JDBI 可以提供

  • 用户可以插入以恢复原始行为的可选映射集
  • 发行说明中的​​一些适当评论(甚至是主要版本的碰撞)

让我们将其保留为次要版本,我们刚刚发布了 3,如果您认为错误,这只是一个重大更改......;)

只是为了播放我听到的声音:

  • 将现有的内置 java.time 映射器移动到一个单独的插件中(我喜欢YoloTimePlugin
  • 在使用ResultSet.getObject(column, type)而不是从 java.sql 类型转换的位置添加新的内置映射器。
  • 在发行说明中记录此重大更改的内容:解释此更改的影响以及哪些数据库供应商将受到影响,以及用户需要采取哪些措施来缓解问题

我可以支持这个。

我们仍然没有回答关于绑定这些日期/时间对象的问题。 我们对映射结果_out of_数据库所做的任何更改都需要以我们将日期/时间参数_into_SQL 语句绑定的方式进行镜像。

我们对从数据库映射结果所做的任何更改都需要以我们将日期/时间参数绑定到 SQL 语句的方式进行镜像。
@qualidafial ,很好的问题。 我没有测试过这个。 JDBC 4.2 规范明确涵盖了这一点,因此 _should just work™_。

JDBC 4.2 规范明确涵盖了这一点,因此 _should just work™_。

这假定 JDBC 驱动程序实现者完全遵循规范。 我还没有找到没有错误的 JDBC 驱动程序实现。

我觉得为每个数据库甚至是对数据类型有不同支持的不同版本提供带有映射器的插件会太麻烦。 也许一个更空白的方法可以工作:你从一个没有预安​​装映射器的 Jdbi 实例开始,核心工件提供了一堆不同的映射器,你可以手动选择安装,有些具有不同的逻辑来处理相同的数据类型 - get/ setObject、get/setX、转换为其他类型等。您需要自己弄清楚哪些映射器最适合您的数据库(版本)和应用程序逻辑,并使用单元测试来检查它。

这基本上就是我现在在我的项目中已经在做的事情。 Jdbi 中预装了一些映射器,它们不适用于我的数据库,我现在不需要它们,所以我实际上已经用抛出 UnsupportedOperationException 的参数/映射器工厂覆盖了它们,而我已经用更好的参数/映射器工厂覆盖了它们实现(见下文)。

对于那些喜欢与他们的 db 保持亲密关系以确保一切正常的人来说,这将是一个解决方案,并且对于想要 Just Werk™ 的人来说什么都不会改变(就它实际而言):

  • 向 Jdbi 实例添加一个方法来清除它们的所有映射器/参数(干净的石板)
  • 公开当前内置的 mapper/arg impls 供我们手动安装
  • 当对它们的需求增加时,继续向核心工件添加替代映射器实现,并让人们自己安装它们,生成任何可能的东西,如下所示

通过这样做,我设法在 hsqldb 驱动程序中找到了太多偏离他们自己规范的点(在源代码中报告并修复,仍在等待他们最终发布更新的工件),并且工作通过自己找到工作映射逻辑来解决这些缺陷。

// I want strings handled differently
<strong i="13">@Component</strong>
public class CleanStringArgumentFactory extends AbstractArgumentFactory<String> {
    protected CleanStringArgumentFactory() {
        super(Types.VARCHAR);
    }

    <strong i="14">@Override</strong>
    protected Argument build(String value, ConfigRegistry config) {
        return (position, statement, ctx) -> statement.setString(position, StringUtils.trimToNull(value));
    }
}
// no need for jdbi's built-in logic
<strong i="17">@Component</strong>
public class SetObjectArgumentFactory implements ArgumentFactory {
    private static final Set<Type> SUPPORTED = ImmutableSet.of(UUID.class, LocalDateTime.class);

    <strong i="18">@Override</strong>
    public Optional<Argument> build(Type type, Object value, ConfigRegistry config) {
        return SUPPORTED.contains(type)
            ? Optional.of(new LoggableArgument(value, (position, statement, context) -> statement.setObject(position, value)))
            : Optional.empty();
    }
}
// these built-ins don't work and I don't need them anyway
<strong i="21">@Component</strong>
public class UnsupportedArgumentFactory implements ArgumentFactory {
    private static final Set<Type> UNSUPPORTED = ImmutableSet.of(ZonedDateTime.class, OffsetDateTime.class, OffsetTime.class);

    <strong i="22">@Override</strong>
    public Optional<Argument> build(Type type, Object value, ConfigRegistry config) {
        if (UNSUPPORTED.contains(type)) {
            throw new UnsupportedOperationException(type.getTypeName() + " is not supported at the moment");
        } else {
            return Optional.empty();
        }
    }
}
// voodoo
<strong i="25">@Inject</strong>
    public JdbiConfiguration(
        @Named("dataSource") DataSource dataSource,
        Set<ArgumentFactory> arguments,
        Set<ColumnMapper> mappers,
        Set<ColumnMapperFactory> mapperFactories
    ) {
        jdbi = Jdbi.create(dataSource);
        jdbi.clearMappings();

        jdbi.installPlugin(new SqlObjectPlugin());

        arguments.forEach(jdbi::registerArgument);
        mapperFactories.forEach(jdbi::registerColumnMapper);
        mappers.forEach(jdbi::registerColumnMapper);

        // ...

        // Jdbi instance fine-tuned for my exact database and demands, without surprises and at my own responsibility

豪博达? 例如,您可以提供一个 SetObjectArgumentFactory,它需要使用一组它应该激活的类进行初始化,以及许多数据类型的映射逻辑的各种变体,以及一些适用的可配置行为。 大家都很开心。 最终用户需要做一些工作,但我很高兴知道 jdbi 完全按照我的意愿去做,以及我的数据库支持做什么。

我觉得为每个数据库甚至是对数据类型有不同支持的不同版本提供带有映射器的插件会太麻烦。

我很想为许多数据库供应商提供插件。 来吧,来吧:如果您的数据库需要/想要一些自定义以使 Jdbi 适应数据库供应商或其 JDBC 驱动程序的特性,我们想听听它。 或者更好的是,我们想要拉取请求! :)

如果 TravisCI 直接支持您的数据库,那么我们可以对其进行实际测试: https ://docs.travis-ci.com/user/database-setup/

对于你的“白板”建议,我只有轻微的反对意见:

  • 我可能会将clearMappings()方法直接放在RowMappersColumnMappersArgumentsJdbiCollectors配置类上,而不是放在Jdbi ,我将它们命名为clear()
  • 我有点犹豫要不要公开内置的映射器、参数和收集器。 其中一些具有古怪的名字或设计,我们可以侥幸逃脱,因为它们是内部的。 在使它们成为 API 的一部分之前,我可能想对它们进行一些重构。
  • 我在质疑白板是否真的有必要。 Jdbi 的所有注册表类都使用最后注册的获胜方法,因此始终可以覆盖开箱即用的行为。 从这个意义上说,空白的石板感觉有点矫枉过正。

@jdbi/contributors 还有其他人愿意参与吗?

我包含了一个空白的想法,以避免在您忘记注册您选择的一个时意外使用默认映射器。 我宁愿有一个 NotImplementedException 而不是默认的 impl。 最后的胜利很好,但没有办法知道你需要否决哪些,以防万一你不想要。

拥有它并不痛。 如果人们不想要这种方法,他们就不需要调用 clear 方法,并且没有任何改变。 它只适合那些喜欢无默认工作方式的人。

我同意我们不应该只是公开现有的映射器。 如果我们选择提供一个全新的选项,我们应该将映射器和绑定器捆绑成合理的Plugin块、 PrimitivesPluginBoxedPrimitivesPluginCollectionsPlugin等等。

然后,我们可以在没有任何配置的情况下为“基本”jdbi 添加一个新工厂,并修改现有工厂以使用它并添加WholeJdbiEnchiladaPlugin

我有点反对“clear”,除非我们可以展示难以附加构建它的情况——对我来说感觉就像一种代码气味。

我有点反对“clear”,除非我们可以展示难以附加构建它的情况——对我来说感觉就像一种代码气味。

好吧,如果您添加一个单独的无预配置 jdbi 工厂,那么添加构建它并不困难,因此我们确实不需要任何 clear() 。

是的,我认为我们在这里达成了一致,我只是表达了在clear()样式上添加新的“干净”工厂方法的偏好。

那么,这样就解决了大家的问题吗? 我个人认为这个计划听起来很性感。

  • make Jdbi no-defaults(几乎所有东西都抛出异常)
  • 使所有现有的映射器和绑定器(以及 tx 处理程序等)可插入( PrimitivesPluginBoxedPrimitivesPlugin等),作为插件包和单独的类
  • 提供一些替代实现( UTCJavaTimePlugin vs LocalJavaTimePlugin vs ConvertJavaTimeToUtilDatePluginGetObjectMapper.forClasses(Class... cs)SetObjectArgumentFactory.forClasses(Class... cs)NullsToDefaultsPrimitivesPlugin vs NullThrowsExceptionPrimitivesPlugin )并以相同的方式公开它们(在插件中和单独),因此人们可以编写他们自己的全部所需映射逻辑,只获取他们想要的内容。 这里可能需要很多映射器配置,比如处理非法空值或丢失时区的不同策略,以及其他可能的差异
  • 也许制作特定于数据库的捆绑插件,因此具有流行数据库+版本的用户可以从无配置Jdbi并再次为他们的数据库( HsqlDb2_4_0Plugin )单行安装已知的良好映射方案具有不同策略的配置选项
  • 并通过使当前工厂方法执行我们现在拥有的预配置( BuiltInGenericPreconfigurationPlugin )以及无配置工厂方法来使其全部向后兼容。

使Jdbi无默认值

我没有非常强烈的意见,但从用户的角度来看,这一切听起来都非常复杂。 用户需要了解他们想要使用的类型的各种可能的映射并选择合适的映射。 这很难,尤其是当时间在起作用时——了解可能的不同选项确实不是一件容易的事。

用户需要了解他们想要使用的类型的各种可能的映射并选择合适的映射

不,只有他们愿意。 现有的工厂方法将继续输出Jdbi实例,其预装映射器与现在完全相同。 我们将另外获得 DIY 配置工厂方法,选择这些方法的人需要弄清楚映射的亲密关系。 也会有一个中途,在那里你得到一个 DIY 实例,只需安装YourDbAndVersionPlugin并滚动它所做的任何事情。 我冗长的解释主要是 jdbi 内部和高级用法,而不是基线。

TLDR:您仍然可以做到Jdbi.create(dataSource)并且您无需再提供任何您尚未提供的额外提示。 别人只会选择Jdbi.createBlank(dataSource).install(x).install(y).install(z)...

我支持,除了我认为我们不应该弃用现有的工厂之外,我相信大多数用户会继续使用它,因为“开箱即用”的行为非常好。

很公平

@TheRealMarnes整个对话开始于处理 JDBC 不直接支持的 JSR-310 数据类型。

我认为关于删除错误的 java.time 映射器和参数,并将它们放入插件而不是内置中,已达成共识。

你还想追求吗?

只是 java.time 的? 一个 JavaTimePlugin?

是的,一个仅用于 java.time 类型的插件,但命名为传达参数和映射器是天真的,如@findepi所述的有损实现。

SimplisticJavaTimePlugin ?

等待直到我的其他 PR 得到照顾......同时有点太多了。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

johanneszink picture johanneszink  ·  4评论

buremba picture buremba  ·  5评论

kkrgwbj picture kkrgwbj  ·  4评论

dhardtke picture dhardtke  ·  3评论

Romqa picture Romqa  ·  5评论