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)
    .findUnique();

Dependencies

querybean-generator

Query bean generator is a Java annotation processor so you add it as a dependency and then javac will automatically register it and whenever a entity is compiled the associated query beans are generated.

If the dependency is added with provided scope then the generator itself will not be included in the final build artifact.

<dependency>
  <groupId>io.ebean</groupId>
  <artifactId>querybean-generator</artifactId>
  <version>${version}</version>
</dependency>
<dependency org="io.ebean" name="querybean-generator" rev="${version}"/>
@Grapes(
  @Grab(group='io.ebean', module='querybean-generator', version='${version}')
)
'io.ebean:querybean-generator:${version}'
'io.ebean:querybean-generator:${version}'
libraryDependencies += "io.ebean" % "querybean-generator" % "${version}"
[io.ebean/querybean-generator "${version}"]

ebean-querybean

We need to add ebean-querybean as a dependency. This provides the property types for the generated query beans.

<dependency>
  <groupId>io.ebean</groupId>
  <artifactId>ebean-querybean</artifactId>
  <version>${version}</version>
</dependency>
<dependency org="io.ebean" name="ebean-querybean" rev="${version}"/>
@Grapes(
  @Grab(group='io.ebean', module='ebean-querybean', version='${version}')
)
'io.ebean:ebean-querybean:${version}'
'io.ebean:ebean-querybean:${version}'
libraryDependencies += "io.ebean" % "ebean-querybean" % "${version}"
[io.ebean/ebean-querybean "${version}"]

Enhancement

The query beans have been designed so for maximum readability of the query code. That is, some type safe query mechanisms achieve the type safety goal but in doing so result in query code that is virtually unreadable and we specifically want to avoid that issue.

Ebean's type safe query mechanism results in very readable queries with the downside that it requires a bit of AOP to enhance query beans and simulate properties. If the target JVM language supported properties then we would not need to use AOP but Java does not have proper properties support so we are using AOP instead.

The AOP enhancement is easy to incorporate with an IDE plugin available for IntelliJ IDEA (and planned for Eclipse) as well as the standard javaagent and maven plugin for load time and compile time enhancement.

IDEA Query bean plugin

In IntelliJ install the Ebean ORM Query bean enhancer plugin. It should then appear as an extra item on the Build menu where it can be toggled on and off.

Maven tile

Ebean supplies a maven tile that brings in both the enhancer for entity beans as well as the enhancement for query beans. This is simpler, cleaner less verbose than defining the plugin in the traditional (non-tile) fashion and now the preferred approach.

ebean-enhancement tile
<plugin>
  <groupId>io.repaint.maven</groupId>
  <artifactId>tiles-maven-plugin</artifactId>
  <version>2.8</version>
  <extensions>true</extensions>
  <configuration>
    <tiles>
      <tile>io.ebean.tile:enhancement:2.9</tile>
    </tiles>
  </configuration>
</plugin>

Use mvn help:effective-pom to view the plugins that the tile brings in.

Generation

Once the dependency to querybean-generator is added the query beans will be generated automatically whenever javac compile occurs.


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>${version}</version>
</dependency>
<dependency org="io.ebean.tools" name="finder-generator" rev="${version}"/>
@Grapes(
  @Grab(group='io.ebean.tools', module='finder-generator', version='${version}')
)
'io.ebean.tools:finder-generator:${version}'
'io.ebean.tools:finder-generator:${version}'
libraryDependencies += "io.ebean.tools" % "finder-generator" % "${version}"
[io.ebean.tools/finder-generator "${version}"]

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();
  }
}

Query bean source

For each entity bean a query bean is generated.

Example

package org.example.domain.query;

import io.ebean.EbeanServer;
import io.ebean.typequery.PEnum;
import io.ebean.typequery.PLong;
import io.ebean.typequery.PSqlDate;
import io.ebean.typequery.PTimestamp;
import io.ebean.typequery.TQRootBean;
import io.ebean.typequery.TypeQueryBean;
import org.example.domain.Order;
import org.example.domain.Order.Status;
import org.example.domain.query.assoc.QAssocAddress;
import org.example.domain.query.assoc.QAssocCustomer;
import org.example.domain.query.assoc.QAssocOrderDetail;

/**
 * Query bean for Order.
 */
@TypeQueryBean
public class QOrder extends TQRootBean<Order,QOrder> {

  private static final QOrder _alias = new QOrder(true);

  /**
   * Return the shared 'Alias' instance used to provide properties to
   * <code>select()</code> and <code>fetch()</code>
   */
  public static QOrder alias() {
    return _alias;
  }

  public PLong<QOrder> id;
  public PLong<QOrder> version;
  public PTimestamp<QOrder> whenCreated;
  public PTimestamp<QOrder> whenUpdated;
  public PEnum<QOrder,Status> status;
  public PSqlDate<QOrder> orderDate;
  public PSqlDate<QOrder> shipDate;
  public QAssocCustomer<QOrder> customer;
  public QAssocAddress<QOrder> shippingAddress;
  public QAssocOrderDetail<QOrder> details;


  /**
   * Construct with a given EbeanServer.
   */
  public QOrder(EbeanServer server) {
    super(Order.class, server)
  }

  /**
   * Construct using the default EbeanServer.
   */
  public QOrder() {
    super(Order.class);
  }

  /**
   * Construct for Alias.
   */
  private QOrder(boolean dummy) {
    super(dummy);
  }
}

Associated Query Beans

Along with query beans you'll see another set of generated beans that look very similar to query beans in a assoc sub-package - these are association query beans and represent beans when they are associated by OneToMany, ManyToOne, OneToOne and ManyToOne or are Embedded.

When beans are associated/related they have slightly different expressions like fetch(), filterMany() and equalTo() and need different internals to work as expected. They look a little redundant but they aren't (in order to get a nice representation of the query language features).