You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何JpaTransactionManager会在提交时重置只读标记?

基于只读数据源的Spring Boot JPA项目优化实践

我最近刚好处理过完全一致的场景,结合《Spring Data JPA参考文档》的要求,给你整理一套完整的实现方案,从仓库定义到配置、测试全流程覆盖:

一、只读仓库实现

我们可以通过两种方式定义只读仓库,核心是给底层JDBC驱动传递只读标记,同时限制写操作:

方式1:使用Spring Data内置的只读仓库接口

Spring Data JPA 3.x+ 内置了ReadOnlyRepository,直接继承即可自动禁用写操作,并传递只读提示:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.ReadOnlyRepository;
import com.yourdomain.model.User;

// 仅包含查询方法,自动禁用save/delete等写操作
public interface UserReadOnlyRepository extends ReadOnlyRepository<User, Long> {
    User findByUsername(String username);
}

方式2:自定义仓库实现类(兼容低版本)

如果用的是旧版本Spring Data,可自定义仓库基类手动设置只读属性:

import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import jakarta.persistence.EntityManager;
import org.springframework.data.jpa.repository.JpaEntityInformation;
import java.sql.Connection;

public class CustomReadOnlyRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID> {
    private final EntityManager entityManager;

    public CustomReadOnlyRepositoryImpl(JpaEntityInformation<T, ID> entityInfo, EntityManager entityManager) {
        super(entityInfo, entityManager);
        this.entityManager = entityManager;
    }

    // 直接禁用所有写操作
    @Override
    public <S extends T> S save(S entity) {
        throw new UnsupportedOperationException("只读仓库不支持写操作");
    }

    @Override
    protected void applyTransactionMode() {
        // 给JDBC驱动传递只读标记,触发数据库层面的性能优化
        try {
            Connection connection = entityManager.unwrap(Connection.class);
            connection.setReadOnly(true);
        } catch (Exception e) {
            throw new RuntimeException("设置只读连接失败", e);
        }
    }
}

二、JPA配置类

需要让Spring识别我们的只读仓库配置,通过@EnableJpaRepositories指定相关参数:

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.yourdomain.repository",
        // 如果用自定义实现类,指定repositoryBaseClass
        repositoryBaseClass = CustomReadOnlyRepositoryImpl.class,
        // 禁用默认事务,避免不必要的写事务开销
        enableDefaultTransactions = false
)
public class JpaReadOnlyConfig {
}

三、YML配置属性

在application.yml中配置只读数据源,同时从连接池、Hibernate层面强化只读优化:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/readonly_db
    username: readonly_user
    password: your_readonly_pass
    driver-class-name: org.postgresql.Driver
    hikari:
      # 连接池默认创建只读连接,直接传递给JDBC驱动
      read-only: true
      maximum-pool-size: 10
  jpa:
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        # 禁用二级缓存和查询缓存,只读场景不需要
        cache.use_second_level_cache: false
        cache.use_query_cache: false
        # 告诉Hibernate处于只读模式,跳过脏检查等写操作逻辑
        jdbc.read_only: true
        connection.provider_disables_autocommit: true

四、Docker化PostgreSQL测试

为了验证配置有效性,我写了一个基于Testcontainers的Docker PostgreSQL测试,调用save方法只是为了验证只读限制是否生效:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.yourdomain.model.User;

@Testcontainers
@SpringBootTest
public class ReadOnlyRepositoryTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("test_readonly_db")
            .withUsername("readonly_user")
            .withPassword("test_pass");

    @DynamicPropertySource
    static void configureDBProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private UserReadOnlyRepository userRepository;

    @Test
    void testSaveOperationThrowsException() {
        User testUser = new User();
        testUser.setUsername("test_user");
        // 预期抛出不支持写操作的异常
        org.junit.jupiter.api.Assertions.assertThrows(UnsupportedOperationException.class, () -> userRepository.save(testUser));
    }

    @Test
    void testQueryOperationWorksNormally() {
        // 假设容器启动时已初始化测试数据
        User user = userRepository.findByUsername("existing_user");
        org.junit.jupiter.api.Assertions.assertNotNull(user);
        org.junit.jupiter.api.Assertions.assertEquals("Existing User", user.getNickname());
    }
}

额外优化建议

  • 数据库权限加固:给只读数据源对应的数据库用户配置SELECT权限,即使代码层面的限制被绕过,数据库层面也能阻止写操作,双重保障。
  • 查询计划优化:PostgreSQL会针对只读连接生成更高效的查询计划,避免不必要的锁和WAL日志写入,这也是我们传递只读标记的核心目的。

内容的提问来源于stack exchange,提问作者mirko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 07:52:31