我们目前正在将Spring Boot 2应用程序迁移到Spring Boot 3(3.0.2
),其中包括迁移到Hibernate 6(6.1.6
)。
所有时间戳都被规范化为UTC,并使用TIMESTAMP数据类型(不带时区)的列保存到底层OracleDB。要使Hibernate使用UTC,我们在application.yaml
中将jpa.properties.hibernate.jdbc.time_zone
配置属性设置为true
。
迁移到Spring Boot 3和Hibernate 6(引入了更具体的时间戳转换配置)后,时间戳迁移不再按预期工作:
当通过时间戳属性查找实体时,将找不到它们。
- 简短示例:**
将java.time.Instant
时间戳为2023-12-13T00:00:00Z
的实体保存到数据库会按预期创建新记录。
但是,当尝试使用相同的时间戳检索记录时,数据存储库将返回空结果。因此,保存和查询之间的时间戳转换似乎不同。
我们尝试使用配置属性spring.jpa.properties.hibernate.timezone.default_storage
来调整这个行为,但是无论我们设置什么值,行为都保持不变。我们还尝试了jdbc.time_zone
和timezone.default_storage
配置属性的不同组合,但没有发现对应用程序行为的任何影响。我们最初认为无论出于什么原因,这些属性都不会被应用。但分配无效值会在应用程序启动时引发异常。
在使用H2数据库时,同样的代码就像一个护身符。
我们使用的application.yaml
的相关部分如下所示:
spring:
datasource:
driverClassName: oracle.jdbc.OracleDriver
url: jdbc:oracle:thin:@localhost:1521:xe
username: [dbuser]
password: [dbpassword]
type: org.springframework.jdbc.datasource.SimpleDriverDataSource
jpa:
show-sql: false
generate-ddl: true
hibernate.ddl-auto: none
task:
scheduling:
pool:
size: 10
properties:
hibernate:
jdbc:
time_zone: UTC
timezone:
default_storage: NORMALIZE_UTC
实体示例:
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
@Entity
@Table(name ="entity")
public class DemoEntity {
@Id
@Column(name = "`id`")
UUID id;
@Column(name = "`demo_timestamp`" ,columnDefinition = "TIMESTAMP")
private Instant timestamp;
public DemoEntity() {
this.id = UUID.randomUUID();
}
}
存储库:
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
@Repository
public interface EntityRepository extends JpaRepository<DemoEntity, UUID>, JpaSpecificationExecutor<DemoEntity> {
Optional<DemoEntity> findAllByTimestamp(Instant timestamp);
}
显示观察到的行为的服务:
import java.time.Instant;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class EntityService {
private final EntityRepository repository;
@Autowired
public EntityService(EntityRepository repository) {
this.repository = repository;
}
@EventListener(ContextRefreshedEvent.class)
@Transactional
public void init() {
Instant timestamp = Instant.parse("2022-12-31T23:00:00Z");
Optional<DemoEntity> findResult = repository.findAllByTimestamp(timestamp);
if(findResult.isPresent()) {
log.info("Entity was found for timestamp {}", timestamp);
return;
}
log.info("No entity was found for timestamp {}, creating one", timestamp);
DemoEntity demoEntity = new DemoEntity();
demoEntity.setTimestamp(timestamp);
this.repository.save(demoEntity);
}
}
由于无法查找持久化的时间戳,因此服务不断创建具有正确时间戳的新记录:
1.* * 实体创建正确...**
1.* ...但之后SQL查询未找到...*
2023-01-26T07:20:47.986+01:00 INFO 1274 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-01-26T07:20:48.105+01:00 DEBUG 1274 --- [ restartedMain] org.hibernate.SQL : select d1_0."id",d1_0."demo_timestamp" from entity d1_0 where d1_0."demo_timestamp"=?
2023-01-26T07:20:48.106+01:00 TRACE 1274 --- [ restartedMain] org.hibernate.orm.jdbc.bind : binding parameter [1] as [TIMESTAMP_UTC] - [2022-12-31T23:00:00Z]
2023-01-26T07:20:48.130+01:00 INFO 1274 --- [ restartedMain] com.example.demo.EntityService : No entity was found for timestamp 2022-12-31T23:00:00Z, creating one
2023-01-26T07:20:48.138+01:00 DEBUG 1274 --- [ restartedMain] org.hibernate.SQL : select d1_0."id",d1_0."demo_timestamp" from entity d1_0 where d1_0."id"=?
2023-01-26T07:20:48.138+01:00 TRACE 1274 --- [ restartedMain] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BINARY] - [1ccd9b88-4d18-416a-938d-d8c3fb6dac7d]
2023-01-26T07:20:48.150+01:00 DEBUG 1274 --- [ restartedMain] org.hibernate.SQL : insert into entity ("demo_timestamp", "id") values (?, ?)
2023-01-26T07:20:48.150+01:00 TRACE 1274 --- [ restartedMain] org.hibernate.orm.jdbc.bind : binding parameter [1] as [TIMESTAMP_UTC] - [2022-12-31T23:00:00Z]
2023-01-26T07:20:48.150+01:00 TRACE 1274 --- [ restartedMain] org.hibernate.orm.jdbc.bind : binding parameter [2] as [BINARY] - [1ccd9b88-4d18-416a-938d-d8c3fb6dac7d]
1.* ...导致创建另一个实体*
我们还发现,查找实体时,就好像它们实际上不使用UTC而是使用我们的本地时区CET(即UTC +1)一样,可以得到预期的结果。更具体地说,查找2022-31-12T22:00:00Z
的Instant的记录时,返回的实体的时间戳为2022-31-12T23:00:00Z
。此外,使用TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
或-Duser.timezone=UTC
设置JVM时区时,一切都正常。
似乎表示为Instants的UTC时间戳在被查询时实际上转换为本地时间,但在写入数据库时得到了正确处理。
我们是否遗漏了配置中的任何内容,或者Spring/Hibernate中是否存在导致这种奇怪行为的bug?
据我所知,Hibernate 6的默认行为应该与版本5提供的行为相匹配,因此根本不需要更改配置来保留以前的行为。
2条答案
按热度按时间yi0zb3m41#
Hibernate 5
使用以下方法执行所有日期/时间相关计算:现在,对于您的"即时案例",
Hibernate 6
使用以下方法:这看起来很奇怪,因为HBN把时区敏感的数据类型绑定到非时区敏感的列,问题是:表达式
st.setObject(index, odt, 2014)
的真正含义是什么?DB引擎在选择行时将使用什么实际值?答:
ALTER SESSION SET TIME_ZONE='UTC'
解决了这个问题,并将JDK时区设置为UTC
)-如果我们看一下执行计划,这一点实际上就很清楚了:一个二个一个一个
因此,是的,
Hibernate 6
中有"突破性的变化"(或bug),现在在非时区感知列中存储时区感知数据可能会导致大量问题(错误的结果、缓慢的查询等)tcomlyy62#
好的,听着,在我发表评论之后,我真的希望你能尝试在Spring之外重现这个问题,这样我们就可以确定它的原因。
但无论如何,我现在我已经设法重现它与以下设置:
1.将JVM时区设置为
EST
,将Oracle服务器时区设置为UTC
1.* 不要设置 *
hibernate.jdbc.time_zone
,这样Hibernate就不会知道时区之间存在不匹配1.使用JPA
@Column
注解手动覆盖Hibernate模式导出生成的默认列类型,从timestamp with time zone
导出为timestamp
1.* 不要设置 *
hibernate.type.preferred_instant_jdbc_type
,也不要使用@JdbcTypeCode(TIMESTAMP)
,这样Hibernate就不会知道您正在使用本地timestamp
作为列类型1.最后,* 也是最重要的 *,在绑定查询参数时使用
setParameter("timestamp", Timestamp.from(instant))
,而不是只传递Instant
对象FTR,我使用的测试代码如下:
现在,取消注解三个注解行中的 * 任何一个 *,或者删除
@Column
注解,就可以通过此测试。因此,解决方案是:
hibernate.type.preferred_instant_jdbc_type=TIMESTAMP
,@JdbcTypeCode(TIMESTAMP)
,或hibernate.jdbc.time_zone
设置为服务器时区(您说您尝试过这种方法,但我敢打赌,如果您调试,您会发现它没有坚持)。请注意,我 * 只能 * 使用
setParameter("timestamp", Timestamp.from(instant))
最终重现了这个问题。如果使用setParameter("timestamp", instant)
,它似乎不会发生,这当然更好,更自然。这是有道理的。我想(但不知道)如果调试Spring生成的代码,您会发现它在将Instant
传递给setParameter()
之前(不正确地)将您的Instant
转换为Timestamp
。FTR,H5和H6之间的区别在于,默认情况下,H6使用
timestamp with time zone
来存储Instant
,而不是timestamp
,因为没有时区的timestamp
更像是JavaLocalDateTime
。让它知道JVM和Oracle时区之间的不匹配。