1. 新增场景优化:明确区分新增逻辑,手动设置ID或使用SEQUENCE策略,直接调用persist()避免merge()的额外查询开销。

2. 更新场景优化:先查询再修改替代直接merge(),批量操作时分批处理,配置动态更新避免全字段更新,自定义更新方法绕过isNew()判断。

使用建议:JPA 的最大优势是简单高效,不用书写大量SQL,通常在业务简单、并发较低、涉及单表增删改查的后台管理系统中,比如:应用维护、设备管理、部门管理、商家管理等场景。但其存在的风险和性能影响不可忽视,不建议在以下系统中使用:

  • 高并发、高性能,要极高的写入和读需求(性能低)。

  • 业务复杂,通常涉及复杂的多表联查(较笨重)。

  • 多写多读,需要经常更新多表数据(有风险)。

实体对象的四种状态

  • 瞬时态(TRANSIENT):当new一个实体对象后,这个对象处于瞬时态,即一个保存临时数据的内存区域,如果没有变量引用这个对象,会被JVM的垃圾回收机制回收。这个对象所保存的数据与数据库没有任何关系,除非通过Session的save()、saveOrUpdate()、persist()、merge()把瞬时态对象与数据库关联,并插入或者更新到数据库,才转换为持久态对象。

  • 持久态(PERSISTENT):持久态对象的实例在数据库中有对应的记录,并拥有一个持久化标识(ID)。对持久态对象进行delete操作后,数据库中对应的记录将被删除,那么持久态对象与数据库记录不再存在对应关系,持久态对象变成移除态(可以视为瞬时态)。持久态对象被修改变更后,不会马上同步到数据库,直到数据库事务提交。

  • 游离态(DETACHED):当Session进行了close()、clear()、evict()或flush()后,实体对象从持久态变成游离态,对象虽然拥有持久和与数据库对应记录一致的标识值,但是因为对象已经从会话中清除掉,对象不在持久化管理之内,所以处于游离态(也叫脱管态)。游离态的对象与临时状态对象是十分相似的,只是它还含有持久化标识。

  • 移除态(DELETED):对持久态对象进行delete操作后,数据库中对应的记录将被删除,那么持久态对象与数据库记录不再存在对应关系,持久态对象变成移除态(可以视为瞬时态)。

save() 性能分析

JPA 提供的save方法,既是新增也是更新,通过 isNew() 判断。不同情况的性能影响较大

1
2
3
4
5
6
7
8
9
10
11
public <S extends T> S save(S entity) {
// 判断是否为新实体
if (entityInformation.isNew(entity)) {
// 新实体 persist(插入)
em.persist(entity);
return entity;
} else {
// 旧实体 merge(更新或合并),不同状态的实体,会涉及 SELECT 影响性能
return em.merge(entity);
}
}

批量新增实际也是使用 save()

1
2
3
4
5
6
7
8
public <S extends T> List<S> saveAll(Iterable<S> entities) {
List<S> result = new ArrayList<S>();
// 实际调用 save()
for (S entity : entities) {
result.add(save(entity));
}
return result;
}

isNew(entity)

isNew() 的判断逻辑就是根据ID,通过一系列的方式获取实例ID值,如果为空就是新增,否则就是更新

  • 通过@Id判断:如果id为null或基本类型默认值,则是新实体

    1
    2
    3
    4
    5
    6
    7
    8
    @Entity
    public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 数据库生成
    private Long id;

    // isNew()逻辑:return id == null; 新实体,执行persist()
    }
  • 通过@Version判断:如果有@Version字段且为null,则是新实体

  • 自定义策略:实现Persistable接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Entity
    public class Product implements Persistable<String> {
    @Id
    private String sku; // 主键

    @Override
    public boolean isNew() {
    return true;
    }

    // 保存后设置为false
    @PostPersist
    @PostLoad
    private void markNotNew() {
    this.isNew = false;
    }
    }

persist(entity)

em.persist(entity) 插入新记录:

  1. 实体进入"managed"状态

  2. INSERT操作延迟到flush时执行

  3. 返回的是原对象(同一个引用)

  4. 如果id由数据库生成,执行后id会被设置

1
2
3
4
5
6
7
8
9
10
public void testPersist() {
User user = new User();
user.setName("测试");

// 用户变成managed状态,id 可能被设置(如果使用IDENTITY策略)但INSERT语句还没执行
em.persist(user);

// 执行 INSERT INTO user (id, name) VALUES (?, ?)
em.flush();
}

merge(entity)

em.merge(entity) 更新或合并对象。传入的实体可以是detachedtransient状态,返回的是新的managed实体,原实体不会被修改。

内部流程,先检查实体状态:

  1. 如果实体是managed:直接返回

  2. 如果实体是detached,从数据库查找(SELECT查询):

    1. 如果找到:复制属性,返回managed副本,执行UPDATE
    2. 如果没找到:当作新实体persist
  3. 如果实体是transient:当作新实体persist,执行 INSERT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void testMerge() {
// 更新已存在的实体,返回的是新的managed对象
User detachedUser = new User();
detachedUser.setId(1L);
detachedUser.setName("更新");
User managedUser = em.merge(detachedUser);
// detachedUser != managedUser
// detachedUser的状态不变,managedUser是新的

// merge新实体(没有id)
User newUser = new User();
newUser.setName("New");

User result = em.merge(newUser);
// 效果等同于persist + 返回新对象
}

合并原理

SessionImpl 的相关方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object merge(Object object) throws HibernateException {
checkOpen();
return fireMerge( new MergeEvent( null, object, this ));
}

private Object fireMerge(MergeEvent event) {
// 检查事务同步状态:检查当前会话是否与事务同步,确保Session在正确的事务边界内
checkTransactionSynchStatus();
// 检查操作前的未决动作:挂起的插入/更新/删除操作,未刷新的实体状态
checkNoUnresolvedActionsBeforeOperation();
// 触发合并事件监听器:触发所有注册的MergeEventListener,事件监听器链(核心)
fastSessionServices.eventListenerGroup_MERGE.fireEventOnEachListener( event, MergeEventListener::onMerge );
// 检查操作后的未决动作:合并操作可能产生的新动作,包括级联保存/更新,集合更新等
checkNoUnresolvedActionsAfterOperation();

return event.getResult();
}

事件监听器链

  • 自定义监听器(用户注册)

  • 默认监听器(Hibernate内置,包括MergeEventListener

  • Envers审计监听器(如果启用)

  • 验证器监听器(Bean Validation)

1
2
3
4
5
6
7
8
9
10
11
public void onMerge(MergeEvent event) throws HibernateException {
// 创建实体复制 Observer:跟踪实体在合并过程中的复制状态,防止循环合并
final EntityCopyObserver entityCopyObserver = createEntityCopyObserver( event.getSession().getFactory() );
// 创建合并上下文:保存合并过程中的状态信息
final MergeContext mergeContext = new MergeContext( event.getSession(), entityCopyObserver );

// 执行核心合并逻辑
onMerge( event, mergeContext );
// 标记顶层合并完成
entityCopyObserver.topLevelMergeComplete( event.getSession() );
}

处理实体合并的核心逻辑:根据不同的状态做不同处理,DETACHED(分离状态) 最复杂且影响性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public void onMerge(MergeEvent event, Map copiedAlready) {
Object original = event.getOriginal();
if (original == null) return;

// 1. 处理代理对象
if (isUninitializedProxy(original)) {
event.setResult(lazyLoadFromDB(original)); // 延迟加载
return;
}

// 2. 检查是否正在处理中
if (isAlreadyMerging(original, copiedAlready)) {
event.setResult(original); // 直接返回
return;
}

// 3. 判断实体状态
// TRANSIENT, // 临时状态 - 刚创建,未关联Session
// PERSISTENT, // 持久化状态 - 关联Session,被管理
// DETACHED, // 分离状态 - 曾关联Session,现无关联
// DELETED // 删除状态 - 标记为删除
EntityState state = getEntityState(entity);

// 4. 根据状态处理
switch (state) {
case DETACHED: // 分离状态:复制属性到已管理实体,会先查询数据库(SELECT),再执行更新
// 1. 曾经关联到 Session(通过 find/persist/merge)
// 2. 当前没有关联到任何 Session
// 3. 有数据库对应记录(有效的@Id)
// 4. 修改不会自动同步
handleDetached(event, copiedAlready);
break;
case TRANSIENT: // 临时状态:作为新实体持久化
// 1. 刚通过 new 创建
// 2. 没有关联到 Session
// 3. 没有数据库对应记录,不在 PersistenceContext 中
// 4. 没有设置 @Id 或 @Id 为 null
handleTransient(event, copiedAlready);
break;
case PERSISTENT: // 已管理状态:直接返回
// 1. 关联到当前 Session
// 2. 在 PersistenceContext 中有记录
// 3. 有数据库对应记录
// 4. 修改会自动同步到数据库
handlePersistent(event, copiedAlready);
break;
case DELETED: // 已删除:抛出异常
throw new ObjectDeletedException();
}
}

自定义 Listener

自定义 MergeEventListener 实现持久化前的数据处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component
public class CustomMergeEventListener implements MergeEventListener {

@Override
public void onMerge(MergeEvent event) {
Object entity = event.getEntity();

// 示例1:审计日志
if (entity instanceof Auditable) {
((Auditable) entity).setLastModified(new Date());
}

// 示例2:数据加密
if (entity instanceof SecureEntity) {
encryptSensitiveFields((SecureEntity) entity);
}

// 示例3:业务验证
if (entity instanceof Order) {
validateOrderBusinessRules((Order) entity);
}
}

@Override
public void onMerge(MergeEvent event, Map copiedAlready) {
// 处理循环引用的情况
onMerge(event);
}
}

性能影响

  • 当调用save()更新实体时,merge() 的额外SELECT查询,和一系列反射解析操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    User user = new User();
    user.setId(1L); // 已存在的ID
    user.setName("张三");

    repository.save(user); // 会调用merge()

    // merge()内部会先执行:
    // SELECT * FROM user WHERE id = 1
    // 然后执行UPDATE

    // 更新操作变成了 1次SELECT + 1次UPDATE

    可以开启日志查看详细记录:

    会打印1条 SELECT 和 1条INSERT或UPDATE

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    spring.jpa.show-sql=true
    spring.jpa.properties.hibernate.format_sql=true
    # 显示所有SQL
    logging.level.org.hibernate.SQL=DEBUG
    # 显示参数值
    logging.level.org.hibernate.type.descriptor.sql=TRACE
    # 显示统计信息
    logging.level.org.hibernate.stat=DEBUG
    # 查看merge过程
    logging.level.org.hibernate.event.internal=DEBUG
  • 返回不同对象引用

    1
    2
    3
    4
    5
    6
    User user = userRepository.findById(1L);
    user.setName("测试");
    // 执行save()
    User saved = userRepository.save(user);

    // 如果user是detached状态,saved != user(两个是不同对象,如何存在对象引用,会有问题)
  • 批量保存的性能陷阱

    1
    2
    3
    4
    5
    6
    List<User> users = getUsers();
    // 错误用法:循环调用save(),每次可能先SELECT再INSERT/UPDATE
    for (User user : users) {
    repository.save(user);
    }
    // 如果有1000个用户:可能执行1000次SELECT + 1000次INSERT/UPDATE
  • ID生成策略的影响

    • MySQL自增,persist()后需要立即INSERT才能获取id,不支持批处理

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Entity
      public class UserWithIdentity {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id; // 数据库生成 id == null,执行persist()

      // 如果persist()后需要立即获取id,就需要立即INSERT
      // 此时单条提交事务,性能影响严重。且无法使用JDBC批处理
      }
    • Oracle序列,预分配ID,性能较好,支持批处理

      1
      2
      3
      4
      5
      6
      7
      8
      @Entity
      public class UserWithSequence {
      @Id
      @GeneratedValue(strategy = GenerationType.SEQUENCE)
      private Long id; // 先获取序列值,id == null(但可能已预分配)

      // 调用persist()时获取序列值
      }
    • 手动分配ID,比如雪花算法或UUID,此时调用 save() 会导致多余查询

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @Entity
      public class UserWithManualId {
      @Id
      private String uuid;

      public UserWithManualId() {
      this.uuid = UUID.randomUUID().toString();
      }

      // isNew()会检查ID是否已存在,如果存在save()可能错误调用merge()导致SELECT
      }
    • 级联操作的影响

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public void testCascadeSave() {
      Order order = new Order();
      order.setId(1L);

      OrderItem item = new OrderItem();
      item.setProduct("袜子");
      order.getItems().add(item);

      // 保存order会自动保存item, 如果item有ID,且是已存在的。merge()会先SELECT,性能可能受影响
      orderRepository.save(order);
      }

优化建议和手段

  • 对于数据库自增ID,可以直接调用 save()

  • 对于需要提前设置 ID 的新实体,为了避免进入 merge() 执行 SELECT。自定义save(),跳过ID 判断,执行新增方法

  • 对于新增的方法,可以直接在同一个事务中,不执行 save()

避免 merge()

  • 新增数据

    当使用UUID或Snowflake等算法预生成ID,此时ID 不为空,为了避免执行 merge(),可以做如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // 1. 基础实体类
    @MappedSuperclass
    public abstract class BaseEntity implements Persistable<Long> {

    @Id
    private Long id;

    @Transient // 不持久化到数据库
    private transient boolean isNewFlag = true;

    @Override
    public Long getId() {
    return id;
    }

    @PrePersist
    protected void prePersist() {
    if (id == null) {
    id = UUID.randomUUID().toString(); // 预分配ID
    }
    }

    // 覆盖默认判断逻辑,确保isNew()总是返回true
    @Override
    public boolean isNew() {
    return true;
    }

    @PostPersist
    @PostLoad
    protected void markAsNotNew() {
    this.isNewFlag = false;
    }
    }
  • 更新数据

    第一种情况:先手动调用 findById() 查询,再覆盖参数,不执行 save()持久态(PERSISTENT)实体 Flush 后会自动更新

    先查询 managed实体,提交时不需要调用save(),flush时会自动UPDATE

    注意:必须要添加事务,且需要事务生效

    1
    2
    3
    4
    5
    6
    // 实体添加 @DynamicUpdate 只更新非null字段
    @Transactional
    public void updateDirectly(Long id, String newName) {
    User user = userRepo.findById(id); // 找到managed实体
    user.setName(newName);
    }

    第二种情况:直接实例化实体赋新值提交的问题

    存在问题:如果某个属性有 @Column(name = "code", nullable = false) 属于必填字段,但是更新时没有赋值,会出错 not-null property references a null or transient value

    即使取消Java 层面检查,DB 层也会出错,原因分析:

    merge() 内部:先SELECT获取数据库中的实体

    合并属性:code(null) 覆盖了数据库中的值,在持久化上下文中,实体状态变为:code = null(违反约束)

    抛出异常:not-null property references a null or transient value,这个异常在flush时抛出,不是在生成SQL时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 以下无法实现更新,会出错:Column 'code' cannot be null
    @Transactional
    public void updateDirectly(Long id, String newName) {
    User user = new User();
    user.setId(id);
    // user.setCode("123");
    user.setName(newName);

    return userRepo.save(user);
    }

自定义 save()

方案一:使用 EntityManager 重写方法,跳过判断和查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Transactional
public <T> T update(T entity) {
return this.saveOrUpdate(entity, false);
}

@Transactional
public <T> T save(T entity) {
return this.saveOrUpdate(entity, true);
}

private <T> T saveOrUpdate(T entity, boolean isNewId) {
if (entity == null) {
throw new IllegalArgumentException("Entity cannot be null");
}

// 获取实体ID
Object id = isNewId ? null : entityManager.getEntityManagerFactory()
.getPersistenceUnitUtil()
.getIdentifier(entity);

if (id == null) {
// 新增
entityManager.persist(entity);
return entity;
} else {
// 已有ID:直接更新
Session session = entityManager.unwrap(Session.class);

// 方法1:使用 update() - 会抛出异常如果实体已关联
session.update(entity);
return entity;

// 方法2:使用 merge() 但需要设置选项避免查询,在实体类添加注解 @SelectBeforeUpdate(false),经验证后无效,还是会有一次查询
// return (T) session.merge(entity);
// return entity;
}
}

方案二:使用 jdbcTemplate 书写原生SQL的方式,性能最佳:

1
2
3
4
5
6
7
8
9
jdbcTemplate.update(
"INSERT INTO `user`(`id`, `name`) VALUES (?, ?)",
id,newName
);

jdbcTemplate.update(
"UPDATE user SET name = ?, modify_date = ? WHERE id = ?",
newName, new Date(), id
);

方案三:只做独立的方法,避免多余的查询和判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 强制新增,不做任何查询
*/
public <S extends T> S insertOnly(S entity) {
if (entity == null) {
throw new IllegalArgumentException("entity must be null " );
}

em.persist(entity); // 纯新增,无查询
return entity;
}


/**
* 强制更新,不做任何查询
*/
public <S extends T> S updateOnly(S entity) {
if (entity == null) {
throw new IllegalArgumentException("entity must be null " );
}

Session session = entityManager.unwrap(Session.class);
session.update(entity);
return entity;
}

方案四:使用 @Modifying 写原生SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Modifying
@Query("UPDATE user p SET p.name = :name WHERE p.id = :id")
int updateName(@Param("id") Long id, @Param("name") String name);

// 或者拼接动态JPQL
default int dynamicUpdate(Long id, ProductUpdateDTO dto) {
StringBuilder jpql = new StringBuilder("UPDATE Product p SET ");
List<String> updates = new ArrayList<>();
Map<String, Object> params = new HashMap<>();

if (dto.getName() != null) {
updates.add("p.name = :name");
params.put("name", dto.getName());
}
if (dto.getPrice() != null) {
updates.add("p.price = :price");
params.put("price", dto.getPrice());
}

if (updates.isEmpty()) {
return 0;
}

jpql.append(String.join(", ", updates));
jpql.append(" WHERE p.id = :id");
params.put("id", id);

Query query = getEntityManager().createQuery(jpql.toString());
params.forEach(query::setParameter);
return query.executeUpdate();

// jdbcTemplate
return jdbcTemplate.update(sql.toString(), params.toArray());
}

批量新增

图示为使用 saveAll 的TPS性能

image-20251225184552022

图示为使用 jdbcTemplate.batchUpdate 后的插入性能:1秒约 44444 条数据(两库5表)

image-20251225184722280

批量新增配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring.jpa.properties.hibernate.jdbc.batch_size=500
spring.jpa.properties.hibernate.jdbc.fetch_size=1000
spring.jpa.hibernate.use-new-id-generator-mappings=true
spring.jpa.properties.hibernate.jdbc.batch_versioned_data=true
spring.jpa.properties.hibernate.jdbc.use_scrollable_resultset=true
# 批量操作优化
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.flushmode=COMMIT
# 查询缓存
spring.jpa.properties.hibernate.cache.use_second_level_cache=false
spring.jpa.properties.hibernate.cache.use_query_cache=false
spring.jpa.properties.hibernate.connection.provider_disables_autocommit=false
spring.jpa.properties.hibernate.connection.release_mode=after_transaction
spring.jpa.properties.hibernate.connection.handmode_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.show-sql=false
spring.jpa.open-in-view=false
spring.jpa.hibernate.ddl-auto=none
1
2
3
# 数据源关闭自动提交
# url 添加参数 rewriteBatchedStatements=true&useServerPrepStmts=false,支持批量提交,否则是单条提交
jdbc:mysql://localhost:3306/demo?rewriteBatchedStatements=true&useServerPrepStmts=false&useSSL=false&serverTimezone=GMT%2B8

方案一:使用jdbcTemplate

1
2
3
4
5
6
7
8
9
10
11
12
@Transactional
public void batchInsertWithJdbcTemplate(List<User> entities, int batchSize) {
jdbcTemplate.batchUpdate(
"INSERT INTO `user`(`id`, `name`) VALUES (?, ?)",
entities,
batchSize, // 每批大小
(PreparedStatement ps, User user) -> {
ps.setLong(1, user.getId());
ps.setString(2, user.getName());
}
);
}

或者通过 PreparedStatement 提交:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public int jdbcBatchInsert(List<User> entities) throws SQLException {
String sql = "INSERT INTO user (`id`, `name`) VALUES (?, ?)";

try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
conn.setAutoCommit(false); // 关键:关闭自动提交

for (User entity : entities) {
ps.setLong(1, entity.getId());
ps.setString(2, entity.getName());
ps.addBatch();

// 每1000条执行一次
if (entities.indexOf(entity) % 1000 == 0) {
ps.executeBatch();
conn.commit();
}
}

// 执行剩余批次
int[] results = ps.executeBatch();
conn.commit();

return Arrays.stream(results).sum();
}
}

方案二:使用 EntityManager 批量提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Transactional
public <S extends T> List<S> insertAll(Collection<S> entities) {
List<S> result = new ArrayList<>(entities.size());

for (S entity : entities) {
em.persist(entity); // 注册到持久化上下文
result.add(entity);

// 定期flush和clear,避免内存溢出
if (result.size() % 1000 == 0) {
em.flush();
em.clear();
}
}

em.flush(); // 最终提交
return result;
}

方案三:使用 TransactionTemplate 管理事务,实现批量提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void batchInsertWithTransaction(List<User> users) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
String sql = "INSERT INTO user (id, name) VALUES (?, ?)";

jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
User user = users.get(i);
ps.setLong(1, user.getId());
ps.setString(2, user.getName());
}

@Override
public int getBatchSize() {
return users.size();
}
});
}
});
}

方案四:使用 StatelessSession 管理事务(无一级缓存,性能更高)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void statelessBatchInsert(List<YourEntity> entities) {
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);

try (StatelessSession session = sessionFactory.openStatelessSession()) {
// 获取Druid连接
Connection connection = session.connection();
connection.setAutoCommit(false);

Transaction transaction = session.beginTransaction();

// 设置批处理参数
session.setJdbcBatchSize(100);

int count = 0;
for (YourEntity entity : entities) {
session.insert(entity);
count++;

// 定期提交
if (count % 1000 == 0) {
transaction.commit();
transaction = session.beginTransaction();
log.debug("已提交 {} 条记录", count);
}
}

// 提交剩余记录
transaction.commit();

} catch (Exception e) {
log.error("Stateless批量插入失败", e);
throw new RuntimeException(e);
}
}

批量更新

方案一:原生SQL批量UPDATE,使用 jdbcTemplate 实现原生SQL更新。一次SQL更新所有记录

使用 VALUES() JOIN 更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public int batchUpdateWithValuesJoin(List<User> users) {
// 构建临时表数据
StringBuilder valuesBuilder = new StringBuilder();
List<Object> params = new ArrayList<>();

for (int i = 0; i < users.size(); i++) {
User user = users.get(i);
valuesBuilder.append("(?, ?, ?)");
params.add(user.getId());
params.add(user.getName());
params.add(user.getEmail());

if (i < users.size() - 1) {
valuesBuilder.append(", ");
}
}

String sql = "UPDATE user u " +
"SET name = v.name, email = v.email " +
"FROM (VALUES " + valuesBuilder.toString() + ") " +
"AS v(id, name, email) " +
"WHERE u.id = v.id";

return jdbcTemplate.update(sql, params.toArray());
}

使用 CASE WHEN 批量更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public int batchUpdateWithCaseWhen(List<User> users) {
StringBuilder sql = new StringBuilder(
"UPDATE user SET name = CASE id "
);

List<Long> ids = new ArrayList<>();

// 构建CASE WHEN语句
for (User user : users) {
sql.append("WHEN ? THEN ? ");
ids.add(user.getId());
ids.add(user.getName());
}

sql.append("END, ")
.append("email = CASE id ");

for (User user : users) {
sql.append("WHEN ? THEN ? ");
ids.add(user.getId());
ids.add(user.getEmail());
}

sql.append("END ")
.append("WHERE id IN (")
.append(String.join(",",
users.stream()
.map(u -> "?")
.collect(Collectors.toList())))
.append(")");

// 添加ID到参数列表
users.forEach(u -> ids.add(u.getId()));

return jdbcTemplate.update(sql.toString(), ids.toArray());
}

方案二:Criteria API批量更新,类型安全,动态条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Transactional
public int batchUpdateWithCriteria(List<UserUpdateDTO> updates) {
CriteriaBuilder cb = em.getCriteriaBuilder();
int totalUpdated = 0;

for (UserUpdateDTO update : updates) {
CriteriaUpdate<User> criteriaUpdate = cb.createCriteriaUpdate(User.class);
Root<User> root = criteriaUpdate.from(User.class);

// 动态设置更新字段
if (update.getName() != null) {
criteriaUpdate.set(root.get("name"), update.getName());
}
if (update.getEmail() != null) {
criteriaUpdate.set(root.get("email"), update.getEmail());
}

// 乐观锁版本更新
criteriaUpdate.set(root.get("version"),
cb.sum(root.get("version"), 1));

// 条件
Predicate predicate = cb.equal(root.get("id"), update.getId());
if (update.getVersion() != null) {
predicate = cb.and(predicate,
cb.equal(root.get("version"), update.getVersion()));
}

criteriaUpdate.where(predicate);

int updated = em.createQuery(criteriaUpdate).executeUpdate();
totalUpdated += updated;
}

return totalUpdated;
}

方案三:JPQL批量UPDATE,类型安全,支持JPA实体映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Transactional
public int batchUpdateWithJpql(List<User> users) {
int totalUpdated = 0;

for (User user : users) {
String jpql =
"UPDATE User u SET u.name = :name, u.email = :email, u.version = u.version + 1 " +
"WHERE u.id = :id AND u.version = :version";

int updated = em.createQuery(jpql)
.setParameter("name", user.getName())
.setParameter("email", user.getEmail())
.setParameter("id", user.getId())
.setParameter("version", user.getVersion())
.executeUpdate();

totalUpdated += updated;

// 乐观锁检查
if (updated == 0) {
throw new OptimisticLockException(
"User " + user.getId() + " was modified concurrently"
);
}
}

return totalUpdated;
}

更新风险避坑

事务内多个持久态对象的更新风险与解决方案

如果在一个事务内包含多个持久态(PERSISTENT)状态的对象,当你想对其中一个执行更新时,有可能会更新到多个对象,导致一些异常发生。

此种情况在开发中较常见,是JPA的一大风险,即使你没有使用save()提交保存,JPA 在事务刷新时也会提交保存对象。

如果不想牵扯无关对象的更新,最简单的方式是转换为 瞬时态,或者事务精细化控制

可能的核心风险

  1. 脏检查意外更新:修改一个实体可能触发其他实体的更新

  2. 级联更新扩散:关联实体会被自动更新

  3. 乐观锁冲突:未修改的实体可能因版本检查失败

  4. 内存溢出:太多持久态实体占用内存

场景分析

在同一个事务中更新多个持久态对象,可能的潜在风险:

  • Hibernate 的脏检查可能意外更新其他对象

  • 其他对象可能被级联操作影响

  • 如果这些对象有关联关系,可能触发额外更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Service
@Transactional
public class BatchUpdateRiskService {

@Autowired
private UserRepository userRepository;

public void riskyBatchUpdate() {
// 批量查询 - 所有实体变成 PERSISTENT 状态
List<User> users = userRepository.findAllById(Arrays.asList(1L, 2L, 3L));
// 此时 users[0], users[1], users[2] 都是 PERSISTENT 状态

// 修改其中一个对象,存在潜在风险,可能会连同更新其他对象
users.get(0).setName("测试");
users.get(0).setEmail("test@email.com");
}


public void specificRiskExample() {
// 场景1:关联实体意外更新
User user1 = userRepository.findById(1L).get();
User user2 = userRepository.findById(2L).get();

// 假设 user1 和 user2 有相同的 manager
Manager sharedManager = new Manager();
sharedManager.setName("测试");
user1.setManager(sharedManager);
user2.setManager(sharedManager); // 同一个 manager

// 修改 user1 的 manager
sharedManager.setName("UpdatedManager");
// 风险:user2 的 manager 也会被更新,因为 sharedManager 是同一个对象,Hibernate会更新它
}
}

风险类型

脏检查更新

自动脏检查:任何字段修改都会在事务提交或手动flush时更新,只要在同一事务中

自动执行:UPDATE user SET name = '李四' WHERE id = 1
Hibernate比较当前状态与快照,发现name变化,自动生成UPDATE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Entity
public class User {
@Id
private Long id;
private String name;

@ManyToOne
@JoinColumn(name = "department_id")
private Department department; // 关联实体
}

public class DirtyCheckRisk {

@Transactional
public void dirtyCheckRiskExample() {
// 加载多个用户
User user1 = userRepository.findById(1L).get(); // PERSISTENT
User user2 = userRepository.findById(2L).get(); // PERSISTENT

user1.setName("张三");
user2.setName("李四");

// 问题:如果其他地方修改了关联实体
Department dept = user1.getDepartment();
dept.setName("研发");

// flush时,不仅user1和user2会更新,关联的department也会更新
// 如果user2也关联同一个department,会有冲突
}
}

自动更新的原理与限制:只有持久态(PERSISTENT)实体才会自动更新,游离态/分离态(DETACHED)、临时态(TRANSIENT)、删除态(DELETED)实体 都不会自动更新。

执行:User user = em.find(User.class, 1L);

Hibernate内部:

  1. 创建EntityEntry记录实体状态

  2. 创建快照(原始值副本)

  3. 将实体注册到PersistenceContext

  4. 修改属性后,Hibernate不立即检测变化,user.setName("测试")

  5. flush触发脏检查:em.flush()

    1. 遍历PersistenceContext中所有持久态实体
    2. 对比当前值与快照值(字段级比较)
    3. 发现变化, 标记为"脏"
    4. 为脏实体生成UPDATE语句
    5. 批量执行所有UPDATE
  6. 执行成功后,更新快照值为新值,下次flush时以此为新基准

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void dirtyCheckAtSourceLevel() {
// 在AbstractFlushingEventListener.flushEntities()中:
for (Object entity : persistenceContext.getEntities()) {
EntityEntry entry = persistenceContext.getEntry(entity);

// 检查实体状态
if (entry.getStatus() == Status.MANAGED) {
// 获取原始快照值
Object[] loadedState = entry.getLoadedState();
// 获取当前值
Object[] currentState = persister.getPropertyValues(entity);

// 逐字段比较
boolean dirty = false;
for (int i = 0; i < loadedState.length; i++) {
if (!isSame(loadedState[i], currentState[i])) {
dirty = true;
break;
}
}

// 如果是脏数据,安排更新
if (dirty) {
scheduleUpdate(entity, entry, currentState);
}
}
}
}

Flush的自动触发时机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class FlushTriggers {

@PersistenceContext
private EntityManager em;

@Transactional
public void autoFlushTriggers() {
// 时机1:事务提交时(最常见)
// @Transactional方法结束时

// 时机2:某些查询执行前(保持数据一致性)
User user = em.find(User.class, 1L);
user.setName("BeforeQuery");

// 执行JPQL查询前可能触发flush
List<User> users = em.createQuery(
"SELECT u FROM User u WHERE u.name LIKE '%test%'", User.class)
.getResultList();
// 可能触发flush,确保查询看到最新数据

// 时机3:调用em.flush()时(手动触发)
em.flush(); // 立即同步到数据库

// 时机4:调用native SQL查询前
Query nativeQuery = em.createNativeQuery("SELECT * FROM user");
// 可能触发flush

// 时机5:调用em.lock()时
em.lock(user, LockModeType.PESSIMISTIC_WRITE);
// 可能触发flush
}

/**
* 配置Flush模式:
*/
public void configureFlushMode() {
// 默认模式:AUTO(自动)
// 可配置模式:

// 1. AUTO(默认):需要时自动flush
em.setFlushMode(FlushModeType.AUTO);

// 2. COMMIT:只在事务提交时flush
em.setFlushMode(FlushModeType.COMMIT);
// 优点:减少flush次数,提高性能
// 缺点:某些查询可能看不到未flush的修改

// 3. MANUAL:完全手动控制
// Hibernate特有
Session session = em.unwrap(Session.class);
session.setFlushMode(FlushMode.MANUAL);
// 必须显式调用flush()
}
}

级联更新

级联操作:修改order会影响所有items,包含引用的另一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Entity
public class Order {
@Id
private Long id;

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> items = new ArrayList<>();
}

public class CascadeUpdateRisk {

@Transactional
public void cascadeRiskExample() {
// 加载多个订单
Order order1 = orderRepository.findById(1L).get();
Order order2 = orderRepository.findById(2L).get();

// 修改order1的一个item
OrderItem item = order1.getItems().get(0);
item.setPrice(new BigDecimal("99.99"));

// 假设flush前,其他代码修改了order2 的状态
order2.setStatus("SHIPPED");

// flush时,会出现以下情况:
// 1. order1 被更新(因为items修改)
// 2. order1.items 中的那个item更新
// 3. order2 被更新,因为状态被设置了
// 可能存在的意外:如果order2的items被延迟加载且触发,可能会不必要地加载和检查order2的所有items
}
}

乐观锁冲突

同一事务中的对象即使没有修改值,也可能存在乐观锁检查,导致其他对象事务提交失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Entity
public class Product {
@Id
private Long id;
private String name;
@Version
private Integer version; // 乐观锁
}

public class OptimisticLockRisk {

@Transactional
public void optimisticLockRisk() {
// 同一事务中加载多个产品
Product p1 = em.find(Product.class, 1L); // version=1
Product p2 = em.find(Product.class, 2L); // version=1

p1.setName("手机");

// 此时如果另一个事务修改了p2并提交,p2.version 在数据库中变成 2
// 如果flush时Hibernate检测到p2的变化,即使我们没有修改p2,可能抛出OptimisticLockException
}
}

解决方案

通过以下手段控制更新风险,避免数据异常问题。

在事务中操作多个持久态实体时,使用@DynamicUpdate + 及时clear() + 分字段更新策略,避免意外更新扩散。

精细控制刷新时机

  • 手动控制flush

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public void updateWithManualFlush(List<Long> userIds) {
    // 按需加载,及时flush和clear
    for (Long userId : userIds) {
    User user = em.find(User.class, userId);

    // 执行特定更新
    performUpdate(user);

    // 立即flush并clear当前实体
    em.flush();
    em.clear(); // 清除持久化上下文,确保当前实体被保存,释放内存,避免影响后续操作
    }
    }
  • 使用多个事务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateInNewTransaction(User user) {
    // 每个更新在独立事务中
    performUpdate(user);
    // 事务提交后自动flush,然后持久化上下文被清空
    }

    public void batchUpdateSafely(List<Long> userIds) {
    for (Long userId : userIds) {
    // 每次都重新查询(新事务,新持久化上下文)
    controlledFlushService.updateInNewTransaction(userId);
    }
    }

使用只读查询分离数据

  • 使用DTO模式,脱离持久态实体,并单独更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public void updateWithDTO(List<Long> userIds) {
    // 只读查询获取数据(不变成持久态)
    List<UserDTO> userDTOs = em.createQuery(
    "SELECT new com.example.UserDTO(u.id, u.name, u.email) " +
    "FROM User u WHERE u.id IN :ids", UserDTO.class)
    .setParameter("ids", userIds)
    .setHint(QueryHints.READ_ONLY, true) // 只读,不注册到持久化上下文
    .getResultList();

    // 业务逻辑处理
    processBusinessLogic(userDTOs);

    // 单独更新(每次一个)
    for (UserDTO dto : userDTOs) {
    userService.updateSingleUser(dto.getId(), dto);
    }
    }

    @Transactional
    public void updateSingleUser(Long userId, UserDTO dto) {
    // 单独查询和更新
    User user = em.find(User.class, userId);
    user.setName(dto.getName());
    user.setEmail(dto.getEmail());
    em.flush();
    em.clear(); // 清除,不影响其他
    }
  • 使用StatelessSession(Hibernate特有),脱离 持久化 状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public void updateWithStatelessSession(List<User> users) {
    SessionFactory sessionFactory = em.getEntityManagerFactory()
    .unwrap(SessionFactory.class);

    try (StatelessSession session = sessionFactory.openStatelessSession()) {
    Transaction tx = session.beginTransaction();

    for (User user : users) {
    // StatelessSession没有持久化上下文,不会进行脏检查,不会级联更新
    session.update(user); // 显式更新,只更新当前对象
    }

    tx.commit();
    }
    }

动态更新配置

使用@DynamicUpdate,只生成必要的UPDATE。但是可能会无效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Entity
@DynamicUpdate // 只更新变化的字段
@SelectBeforeUpdate(false) // 不执行SELECT检查
public class User {
@Id
private Long id;

private String name;
private String email;
private String phone;
private String address;

// 使用@DynamicUpdate后:如果只修改name,SQL为:UPDATE user SET name = ? WHERE id = ? 不会包含未修改的字段
}

@Repository
public class DynamicUpdateRepository {

@PersistenceContext
private EntityManager em;

/**
* 配置实体使用动态更新
*/
public void updateWithDynamicUpdate(List<User> users) {
for (User user : users) {
// 重新附着到持久化上下文(不merge)
User managed = em.find(User.class, user.getId());

// 只复制需要更新的字段
if (user.getName() != null) {
managed.setName(user.getName());
}
if (user.getEmail() != null) {
managed.setEmail(user.getEmail());
}
}
}
}

使用原生SQL精确控制

SQL 精确字段更新,只更新指定字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Repository
public class PreciseNativeUpdate {

@Autowired
private JdbcTemplate jdbcTemplate;

public void preciseBatchUpdate(List<UserUpdateDTO> updates) {
// 按字段分组,避免不必要更新
Map<String, List<UserUpdateDTO>> byName = new HashMap<>();
Map<String, List<UserUpdateDTO>> byEmail = new HashMap<>();

for (UserUpdateDTO dto : updates) {
if (dto.getName() != null) {
byName.computeIfAbsent(dto.getName(), k -> new ArrayList<>())
.add(dto);
}
if (dto.getEmail() != null) {
byEmail.computeIfAbsent(dto.getEmail(), k -> new ArrayList<>())
.add(dto);
}
}

// 只更新name的
for (Map.Entry<String, List<UserUpdateDTO>> entry : byName.entrySet()) {
updateOnlyName(entry.getKey(),
entry.getValue().stream().map(d -> d.getId()).collect(Collectors.toList()));
}

// 只更新email的
for (Map.Entry<String, List<UserUpdateDTO>> entry : byEmail.entrySet()) {
updateOnlyEmail(entry.getKey(),
entry.getValue().stream().map(d -> d.getId()).collect(Collectors.toList()));
}
}

private void updateOnlyName(String name, List<Long> ids) {
String sql = "UPDATE user SET name = ? WHERE id IN (" +
String.join(",", Collections.nCopies(ids.size(), "?")) + ")";

List<Object> params = new ArrayList<>();
params.add(name);
params.addAll(ids);

jdbcTemplate.update(sql, params.toArray());
}

private void updateOnlyEmail(String email, List<Long> ids) {
String sql = "UPDATE user SET email = ? WHERE id IN (" +
String.join(",", Collections.nCopies(ids.size(), "?")) + ")";

List<Object> params = new ArrayList<>();
params.add(email);
params.addAll(ids);

jdbcTemplate.update(sql, params.toArray());
}
}

使用Update DSL精确更新

  • 使用CriteriaUpdate构建精确更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    @PersistenceContext
    private EntityManager em;

    public int preciseCriteriaUpdate(List<PreciseUpdate> updates) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    int totalUpdated = 0;

    for (PreciseUpdate update : updates) {
    CriteriaUpdate<User> criteriaUpdate = cb.createCriteriaUpdate(User.class);
    Root<User> root = criteriaUpdate.from(User.class);

    // 动态构建SET子句
    boolean hasUpdate = false;

    if (update.shouldUpdateName()) {
    criteriaUpdate.set(root.get("name"), update.getName());
    hasUpdate = true;
    }

    if (update.shouldUpdateEmail()) {
    criteriaUpdate.set(root.get("email"), update.getEmail());
    hasUpdate = true;
    }

    // 如果没有需要更新的字段,跳过
    if (!hasUpdate) {
    continue;
    }

    // 精确WHERE条件
    criteriaUpdate.where(cb.equal(root.get("id"), update.getId()));

    totalUpdated += em.createQuery(criteriaUpdate).executeUpdate();
    }

    return totalUpdated;
    }
  • 使用QueryDSL构建精确更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Autowired
    private JPAQueryFactory queryFactory;

    public void updateWithQueryDSL(List<UserUpdateDTO> updates) {
    QUser user = QUser.user;

    for (UserUpdateDTO dto : updates) {
    // 构建动态更新
    SQLUpdateClause update = queryFactory.update(user);

    if (dto.getName() != null) {
    update.set(user.name, dto.getName());
    }
    if (dto.getEmail() != null) {
    update.set(user.email, dto.getEmail());
    }

    // 精确条件
    update.where(user.id.eq(dto.getId()));

    update.execute();
    }
    }