Hibernate ProjectionList and ResultTransformer to solve problem of massive queries with endless joins

Hibernate ProjectionList and ResultTransformer to solve problem of massive queries with endless joins

gardenoftwedenprofileHibernate can make query building and entity mapping easy, but if you let it take too much control you can also end up with huge queries that create a big performance drag.

If you know you only need data from a few specific columns of your table or tables, you can use a ProjectionList to target just those columns, and use a ResultTransformer to form the raw results into the sparsely populated entity.

To illustrate the point, let’s take an example of an invoice and its line items.  We have a one-to-many relationship between the invoice table and the line item table.  From the invoice table, we want the invoice_id, invoice_number, invoice_date columns.  The invoice table is linked to a vendor table, and from that table we want the vendor_name column.  From the invoice line items, we want the line_item_number, amount and description columns.

Criteria criteria = session.createCriteria(invoice.class);
criteria.createAlias("vendor", "v");
criteria.createAlias("lineItems", "li");
criteria.setProjection(Projections.projectionList()
    .add(Property.forName("id"))
    .add(Property.forName("invoiceNumber"))
    .add(Property.forName("invoiceDate"))
    .add(Property.forName("v.vendorName"))
    .add(Property.forName("li.lineItemNumber"))
    .add(Property.forName("li.amount"))
    .add(Property.forName("li.description)));

You need to specify aliases for “vendor” and “lineItems” in order to be able to specify the properties from those related entities.  If you had a situation where there wasn’t always a vendor, but you wanted information from invoices that didn’t have vendors, you would specify the alias like this:

criteria.createAlias("vendor", "v", Criteria.LEFT_JOIN);

Since you have a one-to-many relationship from invoice to lineItem, you will get a separate object from this query for every line item.  You will get the same invoice information repeated, with different line item information.  In other words, if you have an invoice that has two line items in it, the raw data returned from the query will look like this:

o = {java.lang.Object[7]}
[0] = 1234   -- the internal ID for the invoice
[1] = "LAP-12355" -- the invoice number
[2] = "12/31/2016" -- the invoice date
[3] = "George's Great Grill" -- the vendor name
[4] = "1" -- the invoice line item number
[5] = 400.23 -- the invoice line item amount
[6] = "ribs" -- the invoice line item description

And then you might have a second object returned with the same information exactly in elements 0 through 3, but with the following for elements 4, 5, and 6:

[4] = "2" -- the invoice line item number
[5] = 20.00 -- the invoice line item amount
[6] = "delivery charge" -- the invoice line item description

This is where the ResultTransformer comes in.  The ResultTransformer is a method that transforms the raw results returned from the SQL query into the entity you use in your Java code.  There are built-in ResultTransformers you can use.  For this example we will write our own, to illustrate how it works.

Specify the ResultTransformer on your criteria like this:

criteria.setResultTransformer(new ResultTransformer()
{
    @Override
    public Object transformTuple(Object[] o, String[] strings)
    {
        return transformObjectToInvoice(o);
    }

    @Override
    public List transformList(List list)
    {
        return consolidateInvoices(list);
    }
});

Then you write a private method transformObjectToInvoice that takes an Object[] and returns an Invoice.  Every invoice it will return will have one line item.  And you write a private method consolidateInvoices that takes a List and returns a List. But the incoming list will have invoices with only one line item, and the outgoing list will have fewer invoices, and the invoices will have 1 to n line items apiece.

So your  transformObjectToInvoice will look something like this:

private Invoice transformObjectToInvoice(Object[] o)
{
    Invoice invoice = new Invoice();
    invoice.setId((Integer) o[0]);
    invoice.setInvoiceNumber((String) o[1]);
    etc.
    return invoice;
}

And your consolidateInvoices will look something like this:

private List consolidateInvoices(List list)
{
    List consolidatedInvoices = new ArrayList<~>();
    Map<Integer, Invoice> invoices = new HashMap<~>();
    For (Invoice oneLineItemInvoice : list)
    {
        Invoice mapInv = invoices.get(oneLineItemInvoice.getId());
        if (mapInv != null)
        {
             mapInv.getLineItems().add(oneLineItemInvoice.getLineItems().iterator().next());
        }
        else
        {
            invoices.put(oneLineItemInvoice.getId(), oneLineItemInvoice);
        }
    }
    consolidatedInvoices.addAll(invoices.values());
    return consolidatedInvoices;
}
Advertisements