Skip to content

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.

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.

QueryLogic.java
@QLiveQuery
public <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.

A field no column backs is fetched from the object rather than selected:

@QLiveComputed
public 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 WHERE clause. A filter path naming one is an error, not a condition quietly dropped.

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.