Replace a generated type
When a table’s columns do not say everything about a type, replace the generated POJO with a handwritten class that extends it. QLive resolves a domain type by simple name, so yours takes the generated one’s place – including for the query document service, which materializes whatever the domain names for the type.
Extending the generated POJO is what keeps it able to hold a row: the columns, their JPA annotations and the fetcher context all come along.
The handwritten class does not live in domain/. The code generator owns
that directory outright and deletes anything in it that it did not write –
see qlive-test layout.
Register it
Section titled “Register it”A class enters the schema by appearing in a logic bean’s signature: as a
return type, as a parameter, or in the types of a @QLiveTypeParam.
The last is the usual one, because a type worth replacing is usually a type
you already have a
document query for.
@QLiveQuerypublic <T> @NotNull QueryDocument<T> queryDocument( @QLiveTypeParam( namePattern = "query*Document", typeNamePattern = "*Document", types = { Foo.class, Bar.class, // the handwritten Qux, which takes the generated one's place Qux.class } ) Class<T> type, DataFetchingEnvironment env, @NotNull QueryConfig config)Once the class is registered, the domain’s entry for Qux names your class
and keeps the generated jOOQ table behind it. The table, its columns and
its foreign keys are untouched, so relations configured on them go on
working.
Extending is what makes it an override. Two unrelated classes sharing a simple name are a clash, and QLive refuses to build the schema rather than silently declaring one of them for both – it names both classes and asks for a rename.
Do not register it with objectType(). That builder method is for a
type with no table of its own, and it builds a table reference out of a
@jakarta.persistence.Table annotation. Handed a subclass it throws,
because that annotation is not inherited; given one redeclared to get past
that, it replaces the generated table with a bare reference that has no
foreign keys – which is exactly what this type is supposed to keep.
Add a computed field
Section titled “Add a computed field”A field no column backs is fetched from the object rather than selected:
@QLiveComputedpublic String getSummary(){ return getName() + " / " + getStringValue();}The getter is all it takes; there is no setter to write. Do not mark it
@JSONProperty(readOnly = true) either: that keeps a property out of the
schema altogether.
Two consequences to plan around:
- A query selecting it should select the fields it is computed from as well. Nothing fetches a column on a computed field’s account.
- A computed property cannot go into a
WHEREclause. A filter path naming one is an error, not a condition quietly dropped.
Pin an ambiguous scalar
Section titled “Pin an ambiguous scalar”A column’s Java type does not always decide which scalar it should be. A
currency amount is a long of 1/10000th units, and nothing about the
column says so. @QLiveField(type = ...) names the scalar, on an
override of the generated getter that keeps its @Column:
@Override@Column(name = "currency_value")@QLiveField(type = "Currency")public Long getCurrencyValue(){ return super.getCurrencyValue();}A long or Long is a Long unless annotated: a 64-bit integer that
travels as a decimal string and is a bigint in the browser, since a JSON
number past 2^53 is rounded before anything can convert it. The other
scalars for a long are chosen by annotation:
| Scalar | Live type | For |
|---|---|---|
Long (default) |
bigint |
any 64-bit integer |
Number |
number |
a count, a size, a position: values that stay within ±2^53, refused beyond it |
Currency |
number |
an amount in 1/10000th units |
The name has to be a scalar the schema knows. Currency, Number, Long,
Timestamp, Date, Byte, BigDecimal and BigInteger come with QLive;
a scalar of your own needs
a converter on the client
side to go with it.
Adding a field or changing its type is a schema change, so regenerate once the backend is running with it.