标识 - equals/hashCode

不要在 @Entity Bean 上实现 equals()hashCode()(而是将其留给 Ebean 增强)。

Ebean 将使用 equals()hashCode() 的最佳实现增强实体 Bean,我们不应该尝试做得更好,而应该将其留给 Ebean。

toString() - 避免 Getter

避免在 toString() 中使用 Getter 方法。我们希望在使用调试器时避免调用任何意外的延迟加载。也就是说,使用调试器并在调试器中检查实体 Bean 将隐式调用实体 Bean 上的 toString() 方法。我们不希望在使用调试器逐步执行时出现不同的行为。Ebean 不会增强 toString() 方法,但如果 toString() 实现使用 Getter 方法,则这些方法可能会调用延迟加载,我们通常希望避免这种情况。

Kotlin 数据类

不要对 @Entity Bean 使用 Kotlin 数据类,因为 equals/hashCode 实现不可取。相反,对实体 Bean 使用常规 Kotlin 类。

@EmbeddedId Bean 使用 Kotlin 数据类。

优先使用 List 而不是 Set

对于 @OneToMany@ManyToMany 集合,优先使用 List 而不是 Set。使用 Set 将隐式调用 equals()hashCode(),通常最好不要在实体 Bean 具有 Id 值之前调用这些方法。

@JoinColumn

除非必须,否则不要使用 @JoinColumn@JoinTable。命名约定将提供外键列和联接表的良好名称,因此仅在存在现有外键等并且这些外键与命名约定不匹配时才使用这些注释。

@Column(name=...)

不要使用 @Column(name=...) 映射数据库列名,而应使用命名约定。仅在命名约定不匹配时才指定显式的 @Column(name=...)

@Column(name="when_activated")    // This is redundant, just adds "annotation noise"
OffsetDateTime whenActivated;
@Column(name="when_activated")    // This is redundant, just adds "annotation noise"
val whenActivated: OffsetDateTime? = null

使用 @MappedSuperclass

使用 @MappedSuperclass 保存通用属性。通用映射超类可能具有

...
@MappedSuperclass
public abstract class BaseDomain extends Model {

  @Id
  long id;

  @Version
  long version;

  @WhenCreated
  Instant whenCreated;

  @WhenModified
  Instant whenModified;

  // getters and setters
  ...
}
...
@MappedSuperclass
open class BaseDomain : Model() {

  @Id
  var id: Long = 0

  @Version
  var version: Long = 0

  @WhenModified
  lateinit var whenModified: Instant

  @WhenCreated
  lateinit var whenCreated: Instant

}

DDL 生成

使用 Ebean 生成所有 DDL,包括数据库迁移 - 也称为 DDL 的前向生成。这会使所有 DDL 在测试和数据库迁移中“保持同步”,并使支持多个数据库平台或在数据库平台之间迁移(例如,从 MySql 到 Postgres)变得更容易。

如果我们手工制作 DDL,模型和实际数据库模式之间出现差异的可能性会增加,并且会使测试变得更加困难。

促进使用 NOT NULL 约束

尽可能使模型的 NOT NULL - 如果可能,优先让数据库列具有 NOT NULL 约束。减少所需的三值逻辑量 - 拥有一个“更紧凑”的模型。

使用构造函数

使用构造函数来帮助强制非空属性(Kotlin)或提升非空属性(Java)。

例如,如果客户始终应该具有名称,则定义一个获取名称属性的构造函数。

@Entity
public class Customer extends BaseModel {

  @NotNull @Length(100)
  private String name;

  ...

  public Customer(String name) {
    this.name = name;
  }

  // getters and setters

}
...
@Entity
class Customer(name : String) : BaseModel() {

  @Length(100)
  var name: String = name    // Ebean knows this is Kotlin non-nullable type

}

现在,当我们创建新客户时,我们必须使用名称创建它。

对于 Kotlin,我们应该将name设为非空类型。Ebean 会将 Kotlin 非空类型视为NOT NULL,从数据库角度来看,这也为我们提供了一个更紧凑的模型。

Getter 和 Setter

Ebean 不需要 getter 和 setter,因为它通过增强添加了自己的访问器方法。

我们可以根据需要省略 getter 和 setter 方法。如果需要,我们可以让 setter 方法遵循返回 this 的流畅风格。

构建器模式

如果我们希望使用 Builder 模式,而不是为给定实体生成额外的 builder 类(这会复制实体的所有属性,使维护变得更加困难),一种更简单的方法是在实体 bean 上使用流畅风格的“setter”方法并返回 this。

@Entity
public class Customer extends BaseModel {

  @NotNull @Length(100)
  private String name;

  private String notes;

  private int level;
  ...

  public Customer(String name) {
    this.name = name;
  }

  // accessors

  public Customer setNotes(String notes) { // fluid style
    this.notes = notes;
    return this;
  }

  public Customer setLevel(int level) {  // fluid style
    this.level = level;
    return this;
  }
  ...
}

// using fluid style

Customer customer =
  new Customer("Roberto")
    .setNotes("An example")
    .setLevel(42);

批量更新查询

在适当的情况下,优先使用批量更新删除语句。避免获取 bean 只是为了迭代并删除它们,或以相同的方式迭代并更新它们。

引用 Bean

当我们有 id 值时,使用引用 bean 而不是执行查询按 id 查找 - 除非我们当然真的想查询数据库。

也就是说,对于插入和更新,当我们有 @Id 值时,我们可以使用引用 bean 作为外键值,而不是对数据库执行额外的查询。

不要这样做 - 因为它会执行额外的数据库查询

Order order = new Order()
order.setCustomer(database.find(Customer.class, 42)); // extra db query for customer
...
database.save(order);
val order = Order()
order.setCustomer(database.find(Customer.class, 42));
...
database.save(order)

这样做 - 相反使用引用 Bean

Order order = new Order()
order.setCustomer(database.getReference(Customer.class, 42)); // no extra db query
...
database.save(order);
val order = Order()
order.setCustomer(database.getReference(Customer.class, 42));
...
database.save(order)

命名实体 Bean

作为“Rob 偏好”而不是严格的“最佳实践”,我发现我经常更喜欢给实体 Bean 一个 D 前缀(匈牙利表示法),例如

  • DCustomer 而不是 Customer
  • DProduct 而不是 Product
  • DOrder 而不是 Order

实体 Bean 通常被认为是内部的,而不是公开暴露的。实体 Bean 名称通常与我们希望在公共 API 中使用的类型/名称匹配/冲突,并且我们经常希望映射到/从公开暴露的 API 类型和我们的内部实体 Bean。

通常给实体 Bean 一个D 前缀(D 表示域)

  • 避免与公共 API 类型发生名称冲突
  • 避免在映射器中需要完全限定的包类型
  • 当代码使用内部实体 Bean(持久性域对象)时,可能会更加明显

通常实体 Bean 位于 domain 包中。由于 Bean 通常通过 @OneToMany@ManyToOne 等彼此相关,这意味着我更喜欢将它们作为一个整体模型放在一起。

数据库设计理念

当我们构建/建模实体 Bean 时,我强烈赞同处于“数据库设计思维模式”中。这意味着我们主要关注规范化和良好的数据库设计原则,并由此产生我们满意的数据库设计,无论我们使用哪种 ORM 或持久性层与数据库交互,它都将长期存在并且是好的。

为长期设计。