是否可能拥有不可变的JPA实体?

29
在我们的Hibernate项目中,实体是使用Java beans模式编码的。我们的代码中有一些地方有人忘记设置一个mutator,导致我们由于NOT NULL约束而出现异常。
有人在使用构建器来构建他们的实体或者使它们不可变吗?
我正在尝试找到一种有效的模式,不是基于Java beans模式的风格。
3个回答

17

如果您将Java beans设置为不可改变的,则必须使用字段级别访问,但这会带来自己的问题,如此处详细讨论的。我们采用的方法是使用构建器/工厂来强制实施/验证所需规则等。


所以,您正在使用构建器来构造实体,但仍然有变异器?您能详细说明一下吗? - Billworth Vandory
4
这是我们的做法(这可能不适用于所有应用程序)。比如我们有一个Person类。然后我们将有一个PersonFactoryBuilder,它将创建一个Person对象,但将其强制转换为ReadablePerson。ReadablePerson是一个只公开“get”方法的接口,因此使其成为不可变的。在从数据库中获取时,我们有一个PersonRepository(类似于DAO),它从数据库中获取人员,并根据意图(更新或显示)将其强制转换/返回为ReadablePerson或Person(可变)。此设计受到joda-time库的启发。http://joda-time.sourceforge.net - Aravind Yarram
1
转换为接口!抽象化万岁。这是一个好主意。谢谢。 - b3bop
1
@Pangea,我知道这是一个旧的线程,但是那些ReadablePerson对象是线程安全的吗?此外,您是否有任何使用此模式的小代码示例或示例项目?谢谢。 - oberger
很好奇PersonRepository会如何使用PersonFactoryBuilder。我很期待能看到它的用法。设计得非常棒。 - thirdy
最好在答案中加入一些代码,这样会更清晰明了。 - emeraldhieu

15

在我们的项目中,我们采用传统的构造器方式(@see Effective Java)。请看下面的例子:

@Entity
public class Person {
   public static class Builder {
        private String firstName;
        private String lastName;
        private PhoneNumber phone;

        public Builder() {}

        public Builder withFullName(String fullName) {
            Preconditions.notNull(fullName); 
            String[] split = fullName.split(" ");
            if (split == null || split.length != 2) {
                throw new IllegalArgumentException("Full name should contain First name and Last name. Full name: " + fullName);
            }  
            this.firstName = split[0];
            this.lastName = split[1];
            return this;
        }

        public Builder withPhone(String phone) {
            // valueOf does validation
            this.phone = PhoneNumber.valueOf(phone);
            return this;
        }

        public Person build() {
            return new Person(this);
        }
   }

   //@Columns
   private long id;//@Id
   private String firstName;
   private String lastName;
   private String phoneNumber;

   // hibernate requires default constructor
   private Person() {} 

   private Person(Builder builder) {
       this.firstName = Preconditions.notNull(builder.firstName);
       this.lastName = Preconditions.notNull(builder.lastName);
       this.phoneNumber = builder.phone != null ? builder.phone : null;
   }

   //Getters
   @Nonnull
   public String getFirstName() { return firstName;}
   @Nonnull
   public String getLastName() { return lastName;}
   @Nullable
   public String getPhoneName() { return phone;}
   public long getId() { return id;}
}

如果您希望有时更改实体,我建议引入new Builder(Person person),它将复制所有数据,因此您可以使用构建器进行更改。当然,它会产生一个新的实体,旧的实体仍然是只读的。

(带突变的)用法非常简单:

Person.Builder personBuilder = new Person.Builder();
Person person = personBuilder.withFullName("Vadim Kirilchuk").withPhone("12345678").build();

Person modified = new Person.Builder(person).withPhone("987654321").build();

需要注意的是,在这个例子中,Person类并不是100%不可变的(也不能完全不可变):首先,id会被JPA设置;另外,懒加载关联可能会在运行时获取;最后,你不能拥有final字段(因为需要默认构造函数):( 最后一点也是多线程环境下的一个问题,即在#build()之后传递实体到另一个线程可能会导致各种错误,因为其他线程无法保证看到完全构建的对象。

JPA 2.1规范第"2.1 The Entity Class"节指出:

实体类的任何方法或持久化实例变量都不能是final。

还有一种类似的方法:http://vlkan.com/blog/post/2015/03/21/immutable-persistence/

在我的情况下,我只会将id添加到构建器中,而不是在草稿上构建服务。


1
我看到Person类有一个私有的无参构造函数。它不需要公共的无参构造函数吗? - seal
2
根据JPA提供程序的不同,Hibernate可以使用private构造函数。https://coderanch.com/t/639487/certification/Private-Constructor-error - Vadim Kirilchuk
我没有运行测试它。Intellij 显示警告,应该有公共的无参构造函数。不管怎样,谢谢。 - seal

3

7
这是一个Hibernate文档,而不是JPA文档。JPA定义了@Immutable注解吗? - Jonathan Rosenne

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接