5.2 Repository

Most applications nowadays are using relational databases as the primary means to store and query their business data. Since entities are used as an abstraction for business (domain) data and behaviour its obvious to provide some mechanism to map these entities to a persistent database representation and to provide some querying facilities. This is what the repository model element is used for. Up to now the generator only supports hibernate based repository implementations and expects the repository declaration to be in the same model (file) as the corresponding entity.

A repository element starts with the repository keyword followed by a name and a reference to the entity this provider is mapped to.The following example shows a fairly complete example of the currently supported repository mapping elements and how they relate to the referenced entity. We will refer to this example when we are going to explain each of the currently possible elements in the following sections.

Note: For the sake of completness he previous figure shows all possible features of repository elements. Many of them are optional or automatically derived from a namingstrategy or from some information already available in the entity corresponding element and doesnt have to be explicitly defined.

A repository element accepts the following attributes.

Operations come in two flavours. The first one is only used to create the signature and a placeholder for data access code which must be manually implemented as shown in the listing below. Manual operations are used in all cases where QL operations (shown next) are not sufficient enough.

operation CustomerContactReport[] loadCustomerReport(String name)
The declaration of the operation shown above generates the following signature and empty method body which has to be implemented manually.

Note: The generation of an empty method body in the manual CustomerDaoImpl actually happens only the first time if the file doesn't exist yet.

// in CustomerDaoGen
Collection<CustomerContactReport> loadCustomerReport(String name);
// in CustomerDaoImpl
public Collection<CustomerContactReport> loadCustomerReport(String name) {
    return null;
}

Query operations look alike manual operations with the additional specification of a hibernate query language (and the subset standardized as JPA QL) statement. A particualar advantage of this kind of operation is the instant syntax verification and content assist for the given query string and parameters.

operation Customer[] findAllByFirstNameAndLastName(String firstName,String lastName) :
        from Customer c
        where
            c.firstName like :firstName and
            c.lastName like :lastName

The declaration of this query operation creates the following code within the generated DaoGenImpl class.

public Collection<Customer> findAllByFirstNameAndLastName(String firstName,String lastName) {
        Query namedQuery = this.sessionFactory.getCurrentSession().getNamedQuery("Customer.FindAllByFirstNameAndLastName");
        namedQuery.setParameter("firstName", firstName);
        namedQuery.setParameter("lastName", lastName);
        applyFindAllByFirstNameAndLastNameQueryHints(namedQuery,firstName,lastName);
        return list(namedQuery);
}

protected void applyFindAllByFirstNameAndLastNameQueryHints(Query query,String firstName,String lastName) {
    // override this method and apply other hints that influence how a query is executed
}

The query statement gets generated as an externalized string into the mapping configuration file of the corresponding entity. The name of the query in the mapping document gets derived from the entity and operation name.

<query name="Customer.FindAllByFirstNameAndLastName">
    <![CDATA[
from CustomerImpl as c
where c.firstName like :firstName and c.lastName like :lastName
    ]]>
</query>

We support the following kinds of query statements:

  • delete statements

    operation /* optional Integer */ deleteAllCustomersLikeName(lastName) :
            delete from Customer as customer
            where customer.lastName = :lastName
    

  • update statements

    operation updateCustomer(String newName,String oldName) :
            update Customer customer set
                customer.name = :newName
            where customer.name = :oldName
    
    // according to the EJB3 specification, HQL UPDATE statements, by default, do not effect the version or 
    // the timestamp property values for the affected entities..
    // .. force Hibernate to reset the version or timestamp property values through the use of a versioned update
    
    operation updateCustomerVersioned(String newName,String oldName) :
            update versioned Customer customer set
                customer.name = :newName
            where customer.name = :oldName

  • insert statements

    insert into Customer ( firstName , lastName )
            select
                customer.firstName,
                customer.lastName
            from Customer customer
            where
                customer.clientId = :clientId

  • select statements returning entities or scalar values

    operation Customer[] findAllByFirstNameAndLastName(String firstName,String lastName) :
            from Customer c
            where
                c.firstName like :firstName and
                c.lastName like :lastName
    
    operation Customer findCustomerByOid(oid) :
            from Customer as customer
            where
                customer.oid = :oid
    
    operation String[] getAllCustomerNames() :
            select
                distinct customer.lastName
            from Customer customer
    

  • select statements returning projected dataviews

    dataview CustomerNameReport {
        Integer groupCount
        Customer.lastName
    }
    .
    .
    operation CustomerNameReport[] loadCustomerNameReport() :
            select
                customer.lastName,
                count(customer) as groupCount
            from Customer customer
            group by customer.lastName

The last select statement is a so called report query which let you specify which attributes to retrieve . Since report queries doesnt returned managed entity instances but only data tuples the generator needs to create some additional mapping code to map from those tuples to the specified query return type like the following one.

protected CustomerNameReport mapLoadCustomerNameReportTuple(Object[] tuple, int rowNumber) {
  CustomerNameReport customerNameReport = new CustomerNameReport();
  customerNameReport.setLastName((String)tuple[0]);
  customerNameReport.setGroupCount((Integer)tuple[1]);
  return customerNameReport;
}

Note: This pattern supports the recommendation from the hibernate best practice chapter given below:

"Consider externalizing query strings: This is recommended if your queries call non-ANSI-standard SQL functions. Externalizing the query strings to mapping files will make the application more portable."

Callable Statement operations support the execution of Sql Stored Procedures and Functions. They allow for the specification of IN and OUT (IN-OUT) parameters combined with the mapping of complex structures passed into or returned from the declaring operation as shown in the examples below. The general structure of callable statement operations is outlined in the following figure.

Examples of input and output parameter mappings

The following section gives an overview of the currently supported options of callable statement usage

The generated body of an callable statement operation uses spring SimpleJdbcCall abstraction for the actual stored procedure or function call. Those operations are further split up into three protected subcalls which allow for better customization and organization of the generated code. The following section lists the generated subcalls together with an example and short explanation of each

  1. prepare%OPERATION_NAME%JdbcCall

    protected SimpleJdbcCall prepareSpWithComplexTypeOutputMappingJdbcCall(Date fromDate,Date toDate) {
        SimpleJdbcCall jdbcCall = createJdbcCall(SP_SP_WITH_COMPLEX_TYPE_OUTPUT_MAPPING);
        return jdbcCall;
    }

    The purpose of prepare%OPERATION_NAME%JdbcCall is to simply create and setup the SimpleJdbcCall instance of the actual stored procedure or function call. By default this operation returns a new SimpleJdbcCall instance for every invocation. Since SimpleJdbcCallis designed as reusable (multi-threaded) object an overriden, customized implementation of prepare%OPERATION_NAME%JdbcCall could for example return a cached SimpleJdbcCall instance for repeated invocations. (or mock for unit tests) Another performance related optimization version could turn-off the default meta data processing of SimpleJdbcCall and explicitly declare all input- and output parameter names together with their matching Sql type as shown in the following example. Also, if you need to pass any database-vendor specific types (e.g. oracles Array datatype) you probably have to override this implementation too.

    @Override
    protected SimpleJdbcCall prepareSpWithComplexTypeOutputMappingJdbcCall(Date fromDate, Date toDate) 
      if (this.jdbcCall == null) {
        jdbcCall = super.prepareSpWithComplexTypeOutputMappingJdbcCall(fromDate, toDate);
        jdbcCall.withoutProcedureColumnMetaDataAccess()
        .declareParameters(new SqlParameter("IN_FROM_DATE", Types.DATE))
        .declareParameters(new SqlParameter("IN_TO_DATE", Types.DATE))
        .declareParameters(new SqlOutParameter("OUT_FIRST_NAME", Types.VARCHAR));
      }
      return jdbcCall;
    }
  2. execute%OPERATION_NAME%

    protected Map<String, Object> executeSpWithComplexTypeOutputMapping(SimpleJdbcCall jdbcCall,Date fromDate,Date toDate) {
      Map<String,Object> parametersMap = new HashMap<String,Object>();
      parametersMap.put("fromDate",fromDate);
      parametersMap.put("toDate",toDate);
      return executeJdbcCall(jdbcCall,parametersMap);
    }

    Uses the previously created SimpleJdbcCall instance to execute the jdbc call with a map of configured parameter names and values. Override this method to provide default parameters or values. (override #executeJdbcCall to handle all jdbc calls of a given repository)

  3. map%OPERATION_NAME%Result

    protected FooOutput mapSpWithComplexTypeOutputMappingResult(Map<String, Object> _resultMap,Date fromDate,Date toDate) {
      FooOutput _fooOutput = new FooOutput();
      _fooOutput.setRowCount((Integer)_resultMap.get("rowCount"));
      FooResult _result = new FooResult();
      _fooOutput.setResult(_result);
      _result.setCreated((Date)_resultMap.get("DAT_CREATED"));
      _result.setName((String)_resultMap.get("NAM_LAST_NAME"));
      return _fooOutput;
    }

    Only for callable statements with a declared return type. Receives the result of a preceeding jdbc call invocation as generic map structure and converts from map entries to the specified return type based on the output parameter mapping of the corresponding return statement.

Columns map attributes to respective columns and only have to be explicitly specified if

For the first case there are actually two ways how to map attributes to column names. One can either explicitly declare the attribute mapping with the column keyword (as already shown above) or adapt and configure the namingstrategy which the generator will use to provide the name in the automatically created column elements.

Columns also support the nested mapping of multi-valued attribute types like Address or Money like in the following mapping scenario.

valueobject Money {
  String currency
  Integer amount 
}

entity Customer extends BaseEntity {
  Money money
}

repository CustomerDao for Customer {
    column money {
      column currency <-> "MONEY_CURRENCY"
      column amount <-> "MONEY_AMOUNT" 
    }
}

Used to map an ordinary reference or containment (cardinality 1 or 0) defined in the enclosing entity to a foreign-key (constrained) column of another table (entity).

repository CustomerDao for Customer {
    many-to-one invoiceAddress <-> "ID_INV_ADRE"
}

Many-To-One elements support the following optional attributes:

Usually it's not required to explicitly declare this many-to-one element (besides to explicitly override and set the above mentioned properties) because it (i.e. the table which holds the foreign key) can always be derrived from the one-ended reference end as shown in the next example.

entity Customer extends BaseEntity {
    String firstName
    String lastName
    String ssn
    Boolean premiumMember
    Address address
}

entity Address extends BaseEntity {
    String(30) streetName
    String streetNumber
    String(10) zip
    Customer customer oppositeof address
}

In this bi-directional Customer - Address relation the generator can derive the many-to-one side from the cardinality info (zero or many address vs. zero or one customer) of the two involved references. In a uni- or bidirectional one-to-one relation the following rules are applied to derive the many-to-one side

Used to map a collection-valued (with list or set collection type) reference or containment to as persistent relation.

provider CustomerDao for Customer {
    one-to-many orders <-> "ID_CUST"
}

One-To-Many elements support the following optional attributes.

As already stated in the previous section about many-to-one mappings it is not necessary to explicitly declare this one-to-many element since the generator can derive this information from the 'oppositeof' reference declaration.

Used to map the opposite role in a bi-directional one-to-one association. This is the opposite end of the association containing the foreign-key (many-to-one) as shown in the following example. The only reason up to now to declare this mapping element expliciltly is to manually set the cascade behaviour. For all other bi-directional one-to-one (to one-to-many) association mapping its NOT required to declare this element since the generator can infer it automatically. By default the generator infers the 'one-to-one side' from the opposite reference declaration of a bi-directional relationship like so.

entity Customer extends BaseEntity {
    String firstName
    String lastName
    String ssn
    Boolean premiumMember
    ref Address address
}

entity Address extends BaseEntity {
    String(30) streetName
    String streetNumber
    String(10) zip
    Customer customer oppositeof address
}

Or to put it in another way the one reference WITHOUT an existing opposite declaration (address) is always assumed to be the many-to-one side (i.e. the table which holds the foreign key) within a bi-directional one-to-many or one-to-one relation. Because of this rather limited usage we are currently evaluating to deprecate or remove this element from the dsl.

For each repository element two or more java classes and three hibernate mapping files are generated. The first java file is the DaoGen interface which extends a generic Dao interface from the platform library and provides several ready to use factory and crud methods. One factory method without parameter and an additional factory method taking all required attributes and (to-one) references to provide a consistent way of entity construction. The second java file represents the actual hibernate based implementation which implements the generated interface.

The three hibernate mapping files are denominated with three different suffixes:

Explanation: Hibernate mapping elements found in the fragment file, will be merged into the hbm.xml. Elements in hbm.xml, which also exist within the fragment will be overwritten by the ones in the fragment.

This empowers the developer to further customise the generated hibernate mapping file used at runtime, which would otherwise not be possible only by the means of the entity model. The reason why is, that hibernate offers a wide array of configuration options and it dos not make sense to abstract this on a model level. The hbm.gen.xml file should not be used at runtime as it does not reflect the customisations made in the fragment file. It merely works as a reference for the raw generated hibernate mapping file content. It will be generated every time the generator workflow is started, like the hbm.xml file.

After the generator workflow has generated the hbm.xml and hbm.gen file, it searches for a fragment file. If it found none, it will create one. If it found one, it will check, if there are elements to be merged into the hbm.xml file, keeping the hbm.gen file untouched. Therefore the developer has to be aware of his or her customisations made in the fragment file, as they overrule the mapping elements in the generated hbm.xml file.

Note: The generation of a separate mapping configuraton document for each entity supports the recommendation from the hibernate best practice chapter given below:

"Place each class mapping in its own file: Do not use a single monolithic mapping document. Map com.eg.Foo in the file com/eg/Foo.hbm.xml. This makes sense, particularly in a team environment."