This is an automated email from the ASF dual-hosted git repository.
tomsun28 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new 0cf4a488e9 [ai]bugfix: read PostgreSQL chat lobs in transactions
(#4396)
0cf4a488e9 is described below
commit 0cf4a488e966a6dddacd9b419d0e4ef1cb234db8
Author: hutiefang76 <[email protected]>
AuthorDate: Mon Oct 5 19:48:15 2026 -0700
[ai]bugfix: read PostgreSQL chat lobs in transactions (#4396)
Co-authored-by: Logic <[email protected]>
Co-authored-by: Tomsun28 <[email protected]>
---
.../apache/hertzbeat/ai/dao/ChatMessageDao.java | 3 +
.../ChatMessageDaoPostgresqlTransactionTest.java | 152 +++++++++++++++++++++
2 files changed, 155 insertions(+)
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/ChatMessageDao.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/ChatMessageDao.java
index b0d0326881..7f663b0244 100644
--- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/ChatMessageDao.java
+++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/ChatMessageDao.java
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.ai.dao;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@@ -34,6 +35,7 @@ public interface ChatMessageDao extends
JpaRepository<ChatMessage, Long> {
* @param conversationId conversation id
* @return list of messages
*/
+ @Transactional(readOnly = true)
List<ChatMessage> findByConversationIdOrderByGmtCreateAsc(Long
conversationId);
/**
@@ -41,5 +43,6 @@ public interface ChatMessageDao extends
JpaRepository<ChatMessage, Long> {
* @param conversationIds list of conversation ids
* @return list of messages
*/
+ @Transactional(readOnly = true)
List<ChatMessage> findByConversationIdInOrderByGmtCreateAsc(List<Long>
conversationIds);
}
diff --git
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/dao/ChatMessageDaoPostgresqlTransactionTest.java
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/dao/ChatMessageDaoPostgresqlTransactionTest.java
new file mode 100644
index 0000000000..04d756a38f
--- /dev/null
+++
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/dao/ChatMessageDaoPostgresqlTransactionTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.startup.dao;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Properties;
+import javax.sql.DataSource;
+import org.apache.hertzbeat.ai.dao.ChatConversationDao;
+import org.apache.hertzbeat.ai.dao.ChatMessageDao;
+import org.apache.hertzbeat.common.entity.ai.ChatConversation;
+import org.apache.hertzbeat.common.entity.ai.ChatMessage;
+import org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import
org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.boot.hibernate.SpringImplicitNamingStrategy;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+import org.springframework.transaction.support.TransactionTemplate;
+import org.testcontainers.DockerClientFactory;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Verifies that PostgreSQL large-object message reads are enclosed in
repository transactions.
+ */
+class ChatMessageDaoPostgresqlTransactionTest {
+
+ private static final DockerImageName IMAGE =
DockerImageName.parse("postgres:17-alpine");
+
+ private static final String PASSWORD = "postgres123";
+
+ private static final int POSTGRESQL_PORT = 5432;
+
+ private static GenericContainer<?> container;
+
+ private static AnnotationConfigApplicationContext context;
+
+ @BeforeAll
+ static void startContext() {
+ assumeTrue(DockerClientFactory.instance().isDockerAvailable(),
+ "a Docker daemon is required to verify PostgreSQL large-object
reads");
+ container = new GenericContainer<>(IMAGE)
+ .withExposedPorts(POSTGRESQL_PORT)
+ .withEnv("POSTGRES_PASSWORD", PASSWORD)
+
.waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofMinutes(5)));
+ container.start();
+ context = new
AnnotationConfigApplicationContext(PostgresqlJpaConfiguration.class);
+ }
+
+ @AfterAll
+ static void stopContext() {
+ if (context != null) {
+ context.close();
+ context = null;
+ }
+ if (container != null) {
+ container.stop();
+ container = null;
+ }
+ }
+
+ @Test
+ void messageQueriesReadPostgresqlLobsOutsideCallerTransaction() {
+ ChatConversationDao conversationDao =
context.getBean(ChatConversationDao.class);
+ ChatMessageDao messageDao = context.getBean(ChatMessageDao.class);
+ TransactionTemplate transactionTemplate =
context.getBean(TransactionTemplate.class);
+
+ ChatConversation conversation = transactionTemplate.execute(status ->
conversationDao.save(
+ ChatConversation.builder().title("PostgreSQL LOB transaction
test").build()));
+ ChatMessage message = transactionTemplate.execute(status ->
messageDao.save(
+ ChatMessage.builder()
+ .conversationId(conversation.getId())
+ .role("user")
+ .content("message stored as a PostgreSQL large object")
+ .build()));
+
+ List<ChatMessage> messages =
messageDao.findByConversationIdOrderByGmtCreateAsc(conversation.getId());
+ List<ChatMessage> messagesByIds =
messageDao.findByConversationIdInOrderByGmtCreateAsc(
+ List.of(conversation.getId()));
+
+ assertEquals(message.getContent(), messages.getFirst().getContent());
+ assertEquals(message.getContent(),
messagesByIds.getFirst().getContent());
+ }
+
+ @EnableJpaRepositories(basePackageClasses = ChatMessageDao.class)
+ @EnableTransactionManagement
+ static class PostgresqlJpaConfiguration {
+
+ @Bean
+ DataSource dataSource() {
+ DriverManagerDataSource dataSource = new DriverManagerDataSource(
+ "jdbc:postgresql://" + container.getHost() + ":" +
container.getMappedPort(POSTGRESQL_PORT)
+ + "/postgres",
+ "postgres", PASSWORD);
+ dataSource.setDriverClassName("org.postgresql.Driver");
+ return dataSource;
+ }
+
+ @Bean
+ LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource
dataSource) {
+ LocalContainerEntityManagerFactoryBean factory = new
LocalContainerEntityManagerFactoryBean();
+ factory.setDataSource(dataSource);
+ factory.setPackagesToScan("org.apache.hertzbeat.common.entity.ai");
+ factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
+ Properties properties = new Properties();
+ properties.setProperty("hibernate.hbm2ddl.auto", "create-drop");
+ properties.setProperty("hibernate.implicit_naming_strategy",
SpringImplicitNamingStrategy.class.getName());
+ properties.setProperty("hibernate.physical_naming_strategy",
+ CamelCaseToUnderscoresNamingStrategy.class.getName());
+ factory.setJpaProperties(properties);
+ return factory;
+ }
+
+ @Bean
+ PlatformTransactionManager
transactionManager(jakarta.persistence.EntityManagerFactory
entityManagerFactory) {
+ return new JpaTransactionManager(entityManagerFactory);
+ }
+
+ @Bean
+ TransactionTemplate transactionTemplate(PlatformTransactionManager
transactionManager) {
+ return new TransactionTemplate(transactionManager);
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]