Identity - equals/hashCode
Do not implement equals()
and hashCode()
on @Entity
beans.
Instead, leave this to Ebean enhancement.
Ebean will automatically enhance entity beans with an optimal implementation of
equals()
and hashCode()
. Ebean does not enhance toString()
.
toString() - avoid getters
Avoid using getter methods in toString()
. We want to avoid invoking any accidental
lazy loading when using a debugger. Using the debugger and inspecting entity beans will implicitly call toString() methods on entity beans.
If the toString() implementation uses getter methods, this may cause different behavior when debugging.
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 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.
Only use these annotations when there are existing foreign keys, and they 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 (a.k.a prefer forward generation of DDL). This keeps all the DDL "in sync" for testing and db migrations. It eases support of multiple database platforms or migration between database platforms (e.g. MySql to Postgres).
Hand-crafting DDL increases the chance of variation between the model and actual database schema which 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
Rather than generate an additional builder class for a given entity (that duplicates all the properties of the entity and reduces maintainability) 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, or iterate and update 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 actually need to query the database).
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". We should be primarily focused on normalization and good database design principals that will last for the long term, regardless of any ORM or persistence layer we use to interact with the database.
Design for the long term.