Overview
A formula property is a read only entity property whose value is computed by a SQL expression at query time rather than stored in its own column. This is typically a SQL literal value, SQL case statement, SQL function or similar. Ebean provides two annotations for this:
-
@Formula- we write the physical SQL for theselect(and anyjoin), using a${ta}placeholder for the base table alias. This gives maximum control but is more verbose. -
@Formula2- we write a logical expression using dot-notation property paths (e.g.parent.familyName). Ebean translates the paths to the correct table aliases and adds the required joins automatically.
Both annotations produce read only properties and are included in queries by default (see Default inclusion and @Transient below to make them opt-in instead).
@Formula
With @Formula we supply the SQL select fragment, and an
optional join. We use ${ta} wherever we need the base
table alias of the entity.
@Entity
public class ParentPerson {
// aggregation via a derived join, ${ta} is the base table alias
@Formula(select = "coalesce(f2.child_count, 0)",
join = "left join (select parent_id, count(*) as child_count"
+ " from child group by parent_id) f2 on f2.parent_id = ${ta}.id")
Integer childCount;
// coalesce across a joined table using an explicit join alias (j1)
@Formula(select = "coalesce(${ta}.family_name, j1.family_name)",
join = "join parent_person j1 on j1.id = ${ta}.parent_id")
String effectiveFamilyName;
}
Notes on @Formula:
- The
joinstring must start withjoinorleft join. - We manage the join aliases (
j1,f2, etc) ourselves and reference them inselect. @Formulais@Repeatableand supports aplatforms()restriction.
@Formula2
@Formula2 is the easier, path based replacement for @Formula.
We write the expression using property paths and Ebean resolves each path to the
correct table alias, adding the joins it needs automatically.
@Entity
public class ParentPerson {
@ManyToOne
GrandParentPerson parent;
String familyName;
// Ebean automatically left joins 'parent' and resolves the aliases
@Formula2("coalesce(familyName, parent.familyName)")
String derivedFamilyName;
}
A query selecting derivedFamilyName produces (roughly):
select t0.id, coalesce(t0.family_name, t1.family_name) from parent_person t0 left join grand_parent_person t1 on t1.id = t0.parent_id
Multi-level paths join through each step:
// joins parent and parent.parent automatically
@Formula2("coalesce(familyName, parent.familyName, parent.parent.familyName)")
String deepFamilyName;
@Formula2 works wherever a normal property does - the required joins are
added automatically in each case:
// selected explicitly
DB.find(ParentPerson.class).select("derivedFamilyName").findList();
// used in where (auto-joins even when not selected)
DB.find(ParentPerson.class).where().eq("derivedFamilyName", "Smith").findList();
// used in order by
DB.find(ParentPerson.class).orderBy("derivedFamilyName").findList();
// referenced via a path from another bean
DB.find(ChildPerson.class).where().eq("parent.derivedFamilyName", "Smith").findList();
It also resolves correctly inside nested fetch() joins, so a
@Formula2 on a fetched association is computed with its own joins
relative to that association.
Notes on @Formula2:
- The expression supports any SQL function whose arguments are logical property paths.
@Formula2supports aplatforms()restriction.- No
${ta}and no hand written join - that is the point of@Formula2.
Default inclusion and @Transient
Both annotations are included in queries by default (just like a normal mapped
property). When no explicit select()/fetch() is given, the
formula - and for @Formula2 the joins it requires - are added to the
query.
Add @Transient to make the formula opt-in: it is then not selected by
default and must be requested explicitly via select() or
fetch(). Do this when the formula (or the joins it needs) is relatively
expensive.
// not selected by default, must be requested explicitly
@Transient
@Formula2("coalesce(familyName, parent.familyName)")
String lazyDerivedFamilyName;
DB.find(ParentPerson.class)
.select("lazyDerivedFamilyName") // explicitly included, join auto-added
.findList();
Which should we use?
Prefer @Formula2 for expressions over property paths (coalesce/case/functions
across associations). It is shorter, refactor-friendly, and the joins stay correct as
the model changes.
Use @Formula when we need raw SQL that does not map cleanly to property
paths - for example a derived aggregate sub-select / dynamic view, or vendor-specific
SQL.