Identity - equals/hashCode

Do not implement equals() and hashCode() on @Entity beans (but instead leave that to Ebean enhancement).

Ebean will enhance entity beans with an optimal implementation of equals() and hashCode() and we should not try and do better and instead just leave it to Ebean.

toString() - avoid getters

Avoid using getter methods in toString(). We want to avoid invoking any accidental lazy loading when using the debugger. That is, using the debugger and inspecting entity beans in the debugger will implicitly call toString() methods on entity beans. We don't want to have different behaviour when stepping through with a debugger. Ebean does not enhance toString() methods but if the toString() implementation uses getter methods then those methods could invoke lazy loading and we generally want to avoid that.

Kotlin data classes

Do not use Kotlin data classes for @Entity beans as the equals/hashCode implementation is not desirable. Instead use normal Kotlin classes for entity beans.

Do use Kotlin data classes for @EmbeddedId beans.

Prefer List over Set

For @OneToMany and @ManyToMany collections prefer the use of List over Set. The use of Set will implicitly calls equals() and hashCode() and generally it's preferable to not call those methods until entity beans have Id values.

@JoinColumn

Don't use @JoinColumn or @JoinTable unless we have to. The naming convention will provide good names of foreign key columns and join tables so only use these annotations when there are existing foreign keys etc and these do not match the naming convention.

@Column(name=...)

Do not use @Column(name=...) to map database column names but instead use the naming convention. Only specify explicit @Column(name=...) when it for some reason does not match the naming convention used.

@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

Use @MappedSuperclass

Make use of @MappedSuperclass to hold common properties. A common mapped superclass might have:

...
@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 Generation

Use Ebean to generate all DDL including DB migrations - aka prefer forward generation of DDL. This keeps all the DDL "in sync" for testing and db migrations and makes it easier to support multiple database platforms or migrate between database platforms (e.g. MySql to Postgres).

If we hand craft DDL we increase the chance of variation between the model and actual database schema and can make testing harder.

Promote use of NOT NULL constraint

Make as much of the model NOT NULL as we can - prefer DB columns to have the NOT NULL constraint if possible. Reduce the amount of 3 valued logic required - have a "tighter" model.

Use Constructors

Use constructors to help enforce non nullable properties (Kotlin) or promote non nullable properties (Java).

For example, if a Customer should always have a name define a constructor that takes the name property.

@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

}

Now when we create a new Customer we must create it with a name.

For Kotlin we should make name a non-nullable type. Ebean will treat Kotlin non-nullable types as NOT NULL from a database perspective as well giving us a tighter model.

Getters Setters

Ebean does NOT need getters and setters as it adds it's own accessor methods via enhancement.

We can omit getter and setter methods as desired. We can have setter methods follow a fluid style returning this if desired.

Builder pattern

If we want to use the Builder pattern rather than generate an additional builder class for a given entity (that then duplicates all the properties of the entity making things harder to maintain) a simpler approach is to have the "setter" methods on the entity bean use the fluid style and return 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);

Bulk update queries

Prefer the use of bulk update and delete statements where appropriate. Avoid fetching beans just to iterate and delete them or iterate and update them all in the same way.

Reference beans

When we have the id value, use a reference bean rather than execute a query to find by id - unless we really want to query the database of course.

That is, for inserts and updates when we have the @Id value we can use reference bean for the foreign key value> rather than execute an extra query against the database.

Do NOT do this - as it executes an extra database query

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)

Do THIS - use reference bean instead

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)

Naming entity beans

As a "Rob Preference" rather than strictly a "best practice" I have found that I often prefer to give entity beans a D prefix (Hungarian notation) like:

  • DCustomer instead of Customer
  • DProduct instead of Product
  • DOrder instead of Order

Entity beans are generally considered internal and not publicly exposed. Entity bean names often match/clash with types/names that we want to use in the public API and we frequently want to map to/from the publicly exposed API types and our internal entity beans.

Giving the entity beans a D prefix (D for Domain) generally:

  • Avoids name clashes with public API types
  • Avoids needing full package qualified types in mappers
  • Can be more obvious when code is using internal entity beans (persistence domain objects)

Generally entity beans are in a domain package. The beans being generally related to each other via @OneToMany, @ManyToOne etc means that I prefer to keep them together as a holistic model.

Database design mindset

When we are building / modelling entity beans I strongly subscribe to being in a "database design mindset". What this means is that we are primarily focused on normalisation and good database design principals with a resulting database design that we are happy will last for the long term and be good regardless of any ORM or persistence layer we are using to interact with the database.

Design for the long term.