Overview

Generation

Generating query beans (via java annotation processor)

Using query beans

Using query beans and how they work

 

Ebean provides a mechanism for building type safe queries by generating query beans. The design/approach taken for the query beans and type safe query construction is orientated for maximum readability and to achieve that with Java (which does not have properties support) has meant that we need to use AOP to enhance the query beans. The AOP enhancement can be done via:

  • IntelliJ IDEA Plugin - Ideal for use during development
  • javaagent - 'Ok' during development, Good for production deployment
  • maven plugin - Good for production deployment

Recommendation

The recommendation is that during development the IntelliJ IDEA Plugin is used as that is just very easy to use relative to specifying a javaagent when running tests etc.

For deployment/production you can use the javaagent or maven plugin.

Benefits

Model changes

When entity bean model changes the query beans are regenerated. If there are queries that are broken by a model change then this results in a compile error (rather than a runtime error) and so breaking model changes are detected easily and early.

Typed bind values

Using query beans means that the bind values used for any given property must be compatible with the properties type. For example, you can bind a String value for a property that has type Timestamp.

Speed of development

Using query beans means we can use IDE auto completion to help write our queries. There is no need to check the model when we build our queries. This makes development a little bit faster, safer and fun!!

Long term maintenance

When applications get bigger and older using strongly type query beans makes maintenance easier as it helps guide new developers who are less familiar with the model and model changes across large applications are much less risky due to compile type checking relative to string based queries.

Examples

Example

List<Customer> customers =
  new QCustomer()
    .id.greaterThan(12)
    .name.startsWith("Rob")
    .findList();

Example: nested or() and and()

The following example contains an or() containing a nested and().

List<Customer> customers
  = new QCustomer()
  .billingAddress.city.equalTo("Auckland")
  .version.between(1,100)
  .billingAddress.country.equalTo(nz)
  .registered.before(new Date())
  .or()
    .id.greaterThan(1)
    .and()
      .name.icontains("Jim")
      .inactive.isTrue()
    .endAnd()
  .endOr()
  .orderBy()
    .name.asc()
    .id.desc()
  .findList();

Example: filterMany

filterMany provides the ability to filter the returned objects of an associated OneToMany or ManyToMany relationship.

For example, lets say Customers have a lot of orders and for a specific use case we want to find customers and only want their recent new orders. We want to fetch the orders (a OneToMany) for the customers but want to filter those orders to only contain 'recent new orders'

Date oneWeekAgo = ...

// Use QOrder to create an expression list ...
// .. use this expression list to then filter orders
ExpressionList<Order> recentNewOrders = new QOrder()
  .orderDate.after(oneWeekAgo)
  .status.eq(Order.Status.NEW)
  .getExpressionList();


List<Customer> customers = new QCustomer()
  .name.ilike("Rob")
  // filter the orders (not customers)
  .orders.filterMany(recentNewOrders)
  .findList();

Example: select() and fetch()

Use the alias() method to get a shared instance of the query bean that can be used to specify properties for select() and fetch().

Note that the 'alias' instance of the query bean returned by the alias() method does not have an underlying query associated with it and can only be used to provide properties to select() and fetch().

// special 'alias' instances of query beans
// used to supply properties to the select() and fetch()
QCustomer cus = QCustomer.alias();
QContact con = QContact.alias();

List<Customer> customers = new QCustomer()
  // query tuning
  .select(cus.name, cus.inactive)
  .contacts.fetch(con.email, con.firstName)

  // predicates
  .name.ilike("Rob")
  .findList();
// special 'alias' instances of query beans
// used to supply properties to the select() and fetch()
QContact con = QContact.alias();
QCustomer cus = QCustomer.alias();
QProduct pro = QProduct.alias();

Customer customer =
  Customer.find.where()
    // manual query tuning
    .select(cus.name, cus.registered)
    .orders.fetchAll()
    .orders.details.product.fetch(pro.name)
    .contacts.fetch(con.firstName)
    // predicates
    .id.eq(12)
    .findOne();

APT / KAPT (generating query beans)

To generate Java query beans we use the io.ebean:querybean-generator java annotation processor and to generate Kotlin query beans we use io.ebean:kotlin-querybean-generator.

Generating with Maven

Refer to docs / getting-started / maven for details of how to generate query beans using maven.

Generating with Gradle

Refer to docs / getting-started / gradle for details of how to generate query beans using gradle.

Enhancement

Note that prior to version 12.1.8 we need to edit a src/main/resources/ebean.mf manifest file to specify querybean-packages for the packages that should be enhanced for query beans. This is no longer needed from 12.1.8 onwards.


Manual generation

There is a mechanism to generate query beans via running code (not via javac annotation processing). This is manual in the sense that whenever there is a change to the entity beans we then need to re-run this process (where as with the java annotation processor it is automatically updated with each compile).

<dependency>
  <groupId>io.ebean.tools</groupId>
  <artifactId>finder-generator</artifactId>
  <version>12.1.1</version>
</dependency>

To generate the query beans add code similar to below to your src/test. The Generator reads compiled .class entity beans using the ASM library. It then uses this meta data to generate java source for the query beans.

Using this approach rather than using javac annotation processing means there is no requirement for javac - you can use any JVM language to write/compile your entity beans (so maybe Scala and SBT.

The requirement of this approach is that entity beans are compiled as .class files and that GeneratorConfig is configured to know where the classes are, where the source code should go and the package containing the entity beans.

package main;

import org.avaje.ebean.typequery.generator.Generator;
import org.avaje.ebean.typequery.generator.GeneratorConfig;

import java.io.IOException;

/**
 * Generate type query beans for each entity bean.
 */
public class MainQueryBeanGenerator {

  public static void main(String[] args) throws IOException {

    GeneratorConfig config = new GeneratorConfig();
    //config.setClassesDirectory("./target/classes");
    //config.setDestDirectory("./src/main/java");
    //config.setDestResourceDirectory("./src/main/resources");

    config.setEntityBeanPackage("org.example.domain");
    //config.setDestPackage("org.example.domain.query");

    //config.setOverwriteExistingFinders(true);

    Generator generator = new Generator(config);
    generator.generateQueryBeans();

    // Additionally generate 'finder's
    //generator.generateFinders();
    //generator.modifyEntityBeansAddFinderField();
  }
}