
在许多业务场景中,某些实体字段在数据首次创建后,其值应当保持不变。例如,创建时间、初始状态或特定标识符等。尽管这些字段可能在应用程序中被读取和使用,但任何后续尝试修改其值的操作都应被阻止,并最好能立即抛出错误,以防止数据不一致或违反业务规则。开发者通常会尝试使用jpa提供的@column(updatable = false)注解来满足这一需求,但有时会发现即使使用了该注解,字段仍然在数据库中被更新,这导致了对该注解行为的困惑。
@Column(updatable = false)注解是JPA规范的一部分,它指示ORM框架(如Hibernate)在生成SQL UPDATE语句时,不包含该注解所修饰的字段。这意味着,当一个实体被加载、修改并调用EntityManager.merge()或Spring Data JPA的repository.save()方法时,如果该字段被标记为updatable = false,Hibernate将不会在生成的SQL UPDATE语句中设置该字段的新值。
然而,需要注意的是,updatable = false主要影响的是ORM框架生成的SQL语句。它并不能阻止:
如果您的测试显示带有@Column(updatable = false)的字段仍然被更新,这通常意味着:
为了在应用程序层面提供更即时和明确的反馈,并在尝试更新时抛出异常,我们可以在实体类的setter方法中加入自定义逻辑。这种方法可以在数据持久化之前就捕获到不合法的更新尝试。
考虑以下Application实体,其中ins字段需要在初次创建后保持不变:
import javax.persistence.*; // 或使用jakarta.persistence for Jakarta EE 9+
@Entity
@Table(name = "application")
public class Application {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String field1;
private String field2;
// 尽管我们会在setter中强制阻止更新,但保留updatable = false仍然是良好的实践,
// 以便Hibernate在生成UPDATE语句时自动忽略此字段。
@Column(name = "ins", updatable = false)
private String ins;
// 默认构造函数
public Application() {
}
// 带有初始值的构造函数,用于创建新实体
public Application(String field1, String field2, String ins) {
this.field1 = field1;
this.field2 = field2;
this.ins = ins;
}
// Getters
public Long getId() {
return id;
}
public String getField1() {
return field1;
}
public String getField2() {
return field2;
}
public String getIns() {
return ins;
}
// Setters
public void setId(Long id) {
this.id = id;
}
public void setField1(String field1) {
this.field1 = field1;
}
public void setField2(String field2) {
this.field2 = field2;
}
/**
* 自定义setter方法,用于阻止对'ins'字段的更新。
* 如果实体已经存在(即ID不为null),并且尝试修改'ins'字段的值,
* 则抛出IllegalStateException。
*
* @param ins 新的'ins'值
* @throws IllegalStateException 如果尝试更新已存在的'ins'字段
*/
public void setIns(String ins) {
// 检查实体是否已存在(通过ID判断)
// 并且新值与当前值不同,才认为是尝试更新
if (this.id != null && !this.ins.equals(ins)) {
throw new IllegalStateException("Field 'ins' cannot be updated after initial creation for existing entity (ID: " + this.id + ").");
}
this.ins = ins;
}
// toString, equals, hashCode (省略)
}在上述代码中,setIns方法首先检查this.id是否为null。如果id不为null,表示这是一个已存在的实体。接着,它会比较传入的新值ins与当前字段的旧值this.ins。如果两者不同,就意味着尝试修改一个不可更新的字段,此时会抛出IllegalStateException。
使用上述修改后的实体类,原有的测试代码将按预期失败,并抛出IllegalStateException:
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Transactional // 确保测试在事务中运行,并在结束后回滚
class ApplicationRepositoryTest {
@Autowired
private ApplicationRepository applicationRepository; // 假设您有一个Spring Data JPA Repository
// 辅助方法:创建并保存一个初始实体
private Application createAndSaveApplication(String field1, String field2, String ins) {
Application app = new Application(field1, field2, ins);
return applicationRepository.save(app);
}
@Test
void updateIns_ShouldThrowError() {
// 1. 创建并保存一个初始实体
Application initialApp = createAndSaveApplication("f1_initial", "f2_initial", "ins_initial");
Long entityId = initialApp.getId();
assertThat(entityId).isNotNull();
// 2. 从数据库加载实体
Optional<Application> fetchedAppOptional = this.applicationRepository.findById(entityId);
assertThat(fetchedAppOptional).isPresent();
Application fetchedApp = fetchedAppOptional.get();
// 3. 尝试更新'ins'字段,这将触发setter中的校验逻辑
// 期望此处抛出IllegalStateException
Assertions.assertThrows(IllegalStateException.class, () -> {
fetchedApp.setIns("ins-update"); // 尝试修改'ins'字段
// 虽然setter已经抛出异常,但为了完整性,我们也可以将save操作包含在断言中
// this.applicationRepository.save(fetchedApp);
});
// 4. 验证数据库中的'ins'字段值未被修改
// 重新从数据库加载实体,确保它保持原始值
Optional<Application> finalAppOptional = this.applicationRepository.findById(entityId);
assertThat(finalAppOptional).isPresent());
Application finalApp = finalAppOptional.get();
assertThat(finalApp.getIns()).isEqualTo("ins_initial"); // 验证'ins'字段仍是初始值
assertThat(finalApp.getIns()).isNotEqualTo("ins-update"); // 确认没有被更新
}
@Test
void createApplication_ShouldSetInsValue() {
// 测试创建新实体时'ins'字段可以被设置
Application newApp = new Application("f1", "f2", "new_ins_value");
Application savedApp = applicationRepository.save(newApp);
assertThat(savedApp.getId()).isNotNull();
assertThat(savedApp.getIns()).isEqualTo("new_ins_value");
}
}通过在实体setter方法中嵌入校验逻辑,我们能够有效地在应用程序早期阶段捕获到对不可更新字段的修改尝试,并提供明确的错误提示,这比仅仅依赖@Column(updatable = false)注解更能满足业务对数据完整性和实时反馈的需求。
以上就是JPA/Hibernate中防止特定字段更新的策略与实践的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号