Showing posts with label hibernate. Show all posts
Showing posts with label hibernate. Show all posts

Sunday, May 20, 2012

RigaJUG - Demo 2 - JPA2

In the first demo the model was limited to 3 tables.
The second demo marks an evolution of our model. Now there are 10 tables.
This demo presents the track JPA2 of minuteproject.


Now we want to know:
  • Who translate what via translation request. 
  • What a user can speak and can translate.
In fact the model can be altered in multiple ways, here is just one possibility.
In the current diagram there are multiple many-to-many relationships
  • request_key
  • application_x_key
  • language_x_translator
  • language_x_speaker
And 2 (language_x_translator and language_x_speaker) link twice user to language...

This demo will illustrate:
  • Enrichment facilities and customisation
  • Generation for JPA2
  • Integration technics of resulting artefacts

Model source

SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL';

DROP SCHEMA IF EXISTS `tranxy` ;
CREATE SCHEMA IF NOT EXISTS `tranxy` DEFAULT CHARACTER SET latin1;
USE `tranxy` ;

DROP TABLE IF EXISTS `tranxy`.`traduction` ;
DROP TABLE IF EXISTS `tranxy`.`translation_key` ;
DROP TABLE IF EXISTS `tranxy`.`language` ;
-- -----------------------------------------------------
-- Table `tranxy`.`translation_key`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`translation_key` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`translation_key` (
  `id` BIGINT NOT NULL AUTO_INCREMENT ,
  `key_name` VARCHAR(200) NULL ,
  `description` VARCHAR(400) NULL ,
  PRIMARY KEY (`id`) )
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `tranxy`.`language`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`language` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`language` (
  `idlanguage` INT NOT NULL AUTO_INCREMENT ,
  `code` VARCHAR(45) NOT NULL ,
  `locale` VARCHAR(10) NOT NULL ,
  `description` VARCHAR(45) NOT NULL ,
  PRIMARY KEY (`idlanguage`) ,
  UNIQUE INDEX `code_UNIQUE` (`code` ASC) )
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `tranxy`.`user`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`user` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`user` (
  `iduser` BIGINT NOT NULL AUTO_INCREMENT ,
  `first_name` VARCHAR(45) NOT NULL ,
  `last_name` VARCHAR(45) NOT NULL ,
  `email` VARCHAR(200) NULL ,
  PRIMARY KEY (`iduser`) )
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `tranxy`.`translation`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`translation` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`translation` (
  `id` BIGINT NOT NULL AUTO_INCREMENT ,
  `translation` VARCHAR(800) NULL ,
  `language_id` INT NOT NULL ,
  `Key_id` BIGINT NOT NULL ,
  `is_final` TINYINT NOT NULL ,
  `date_finalization` DATE NULL ,
  `translator_id` BIGINT NOT NULL ,
  PRIMARY KEY (`id`) ,
  INDEX `fk_traduction_language` (`language_id` ASC) ,
  INDEX `fk_traduction_Key1` (`Key_id` ASC) ,
  INDEX `fk_traduction_user1` (`translator_id` ASC) ,
  CONSTRAINT `fk_traduction_language`
    FOREIGN KEY (`language_id` )
    REFERENCES `tranxy`.`language` (`idlanguage` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_traduction_Key1`
    FOREIGN KEY (`Key_id` )
    REFERENCES `tranxy`.`translation_key` (`id` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_traduction_user1`
    FOREIGN KEY (`translator_id` )
    REFERENCES `tranxy`.`user` (`iduser` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `tranxy`.`application`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`application` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`application` (
  `idapplication` BIGINT NOT NULL AUTO_INCREMENT ,
  `name` VARCHAR(100) NULL ,
  `description` VARCHAR(200) NULL ,
  `type` VARCHAR(45) NOT NULL ,
  PRIMARY KEY (`idapplication`) ,
  UNIQUE INDEX `name_UNIQUE` (`name` ASC) )
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `tranxy`.`application_x_key`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`application_x_key` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`application_x_key` (
  `Key_id` BIGINT NOT NULL ,
  `application_id` BIGINT NOT NULL ,
  INDEX `fk_application_x_key_Key1` (`Key_id` ASC) ,
  INDEX `fk_application_x_key_application1` (`application_id` ASC) ,
  PRIMARY KEY (`Key_id`, `application_id`) ,
  CONSTRAINT `fk_application_x_key_Key1`
    FOREIGN KEY (`Key_id` )
    REFERENCES `tranxy`.`translation_key` (`id` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_application_x_key_application1`
    FOREIGN KEY (`application_id` )
    REFERENCES `tranxy`.`application` (`idapplication` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `tranxy`.`translation_request`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`translation_request` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`translation_request` (
  `idtranslation_request` BIGINT NOT NULL AUTO_INCREMENT ,
  `name` VARCHAR(45) NULL ,
  `request_date` DATE NOT NULL ,
  `user_id` BIGINT NOT NULL ,
  `Key_id` BIGINT NOT NULL ,
  PRIMARY KEY (`idtranslation_request`) ,
  INDEX `fk_translation_request_user1` (`user_id` ASC) ,
  INDEX `fk_translation_request_Key1` (`Key_id` ASC) ,
  CONSTRAINT `fk_translation_request_user1`
    FOREIGN KEY (`user_id` )
    REFERENCES `tranxy`.`user` (`iduser` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_translation_request_Key1`
    FOREIGN KEY (`Key_id` )
    REFERENCES `tranxy`.`translation_key` (`id` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `tranxy`.`request_key`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`request_key` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`request_key` (
  `translation_request_id` BIGINT NOT NULL ,
  `language_id` INT NOT NULL ,
  INDEX `fk_request_key_translation_request1` (`translation_request_id` ASC) ,
  PRIMARY KEY (`translation_request_id`, `language_id`) ,
  INDEX `fk_request_key_language1` (`language_id` ASC) ,
  CONSTRAINT `fk_request_key_translation_request1`
    FOREIGN KEY (`translation_request_id` )
    REFERENCES `tranxy`.`translation_request` (`idtranslation_request` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_request_key_language1`
    FOREIGN KEY (`language_id` )
    REFERENCES `tranxy`.`language` (`idlanguage` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `tranxy`.`language_x_translator`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`language_x_translator` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`language_x_translator` (
  `language_id` INT NOT NULL ,
  `user_id` BIGINT NOT NULL ,
  PRIMARY KEY (`language_id`, `user_id`) ,
  INDEX `fk_request_key_language1` (`language_id` ASC) ,
  INDEX `fk_language_x_translator_user1` (`user_id` ASC) ,
  CONSTRAINT `fk_request_key_language10`
    FOREIGN KEY (`language_id` )
    REFERENCES `tranxy`.`language` (`idlanguage` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_language_x_translator_user1`
    FOREIGN KEY (`user_id` )
    REFERENCES `tranxy`.`user` (`iduser` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `tranxy`.`language_x_speaker`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`language_x_speaker` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`language_x_speaker` (
  `language_id` INT NOT NULL ,
  `user_id` BIGINT NOT NULL ,
  PRIMARY KEY (`language_id`, `user_id`) ,
  INDEX `fk_request_key_language1` (`language_id` ASC) ,
  INDEX `fk_language_x_translator_user1` (`user_id` ASC) ,
  CONSTRAINT `fk_request_key_language100`
    FOREIGN KEY (`language_id` )
    REFERENCES `tranxy`.`language` (`idlanguage` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_language_x_translator_user10`
    FOREIGN KEY (`user_id` )
    REFERENCES `tranxy`.`user` (`iduser` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB;

SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;

Enrichment facilities and customisation

Minuteproject configuration allows to define naming conventions working globally and allow specific enrichment whose granularity is limited to a table or a field.

New configuration

TRANXY-JPA2-2.xml
<!DOCTYPE root>
<generator-config xmlns="http://minuteproject.sf.net/xsd/mp-config" 
xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" 
xs:noNamespaceSchemaLocation="../config/mp-config.xsd">
 <configuration>
  <conventions>
   <target-convention type="enable-updatable-code-feature" />
  </conventions>
  <model name="tranxy" version="1.0" package-root="net.sf.mp.demo">
   <data-model>
    <driver name="mysql" version="5.1.16" groupId="mysql"
     artifactId="mysql-connector-java"></driver>
    <dataSource>
     <driverClassName>org.gjt.mm.mysql.Driver</driverClassName>
     <url>jdbc:mysql://127.0.0.1:3306/tranxy</url>
     <username>root</username>
     <password>mysql</password>
    </dataSource>
    <primaryKeyPolicy oneGlobal="false" >
     <primaryKeyPolicyPattern name="autoincrementPattern"></primaryKeyPolicyPattern>
    </primaryKeyPolicy>
   </data-model>
   <business-model>
    <business-package default="tranxy">
     <condition type="package" startsWith="trans" result="translation"></condition>
    </business-package>
    <enrichment>
     <conventions>
      <!-- manipulate the structure and entities BEFORE manipulating the 
       entities -->
      <column-naming-convention type="apply-fix-primary-key-column-name-when-no-ambiguity" 
              default-value="ID"/>  
      <column-naming-convention type="apply-strip-column-name-suffix"
       pattern-to-strip="ID" />
      <reference-naming-convention
       type="apply-referenced-alias-when-no-ambiguity" is-to-plurialize="true" />
      <reference-naming-convention type="apply-many-to-many-aliasing" is-to-plurialize="true"/>
     </conventions>
          <entity name="language_x_translator">
              <field name="language_id" linkReferenceAlias="translating_language" linkToTargetEntity="LANGUAGE"/>
              <field name="user_id" linkReferenceAlias="translator" linkToTargetEntity="USER"/>
          </entity>
          <entity name="LANGUAGE_X_SPEAKER">
              <field name="LANGUAGE_ID" linkToTargetEntity="LANGUAGE"
                  linkToTargetField="IDLANGUAGE" linkReferenceAlias="spoken_language" />
              <field name="user_id" linkReferenceAlias="speaker" linkToTargetEntity="USER"/>
          </entity>
          <entity name="APPLICATION" alias="registered application">
              <field name="TYPE" alias="obedience">
                  <property tag="checkconstraint" alias="application_type">
                      <property name="OPENSOURCE"/>
                      <property name="COPYRIGHT" />
                  </property>
              </field>
          </entity>
          <entity name="LANGUAGE" content-type="reference-data"/>
    </enrichment>
   </business-model>
  </model>
  <targets>
   <target refname="JPA2" fileName="mp-template-config-JPA2.xml"
    outputdir-root="../../dev/latvianjug/output/JPA2" templatedir-root="../../template/framework/jpa">
    <property name="add-querydsl" value="2.1.2"></property>
    <property name="add-jpa2-implementation" value="hibernate"></property>
    <property name="add-cache-implementation" value="ehcache"></property>
   </target>
            <target refname="CACHE-LIB" fileName="mp-template-config-CACHE-LIB.xml"
                templatedir-root="../../template/framework/cache">
            </target>
   <target refname="LIB" fileName="mp-template-config-bsla-LIB-features.xml"
    templatedir-root="../../template/framework/bsla">
   </target>
  </targets>
 </configuration>
</generator-config>

Global conventions

Make database convention java-friendly.

Fix primary key variable

<column-naming-convention type="apply-fix-primary-key-column-name-when-no-ambiguity" 
    default-value="ID"/>
In Language class the variable + getter/setter are related to 'id' althought mapped to 'idlanguage'.
    @Id @Column(name="idlanguage" )
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

Give readable collection name

When there is no ambiguity (only one foreign key between 2 entities), there is the possibility to have the collection variable made of the name of the linked table plurialized.
<reference-naming-convention 
    type="apply-referenced-alias-when-no-ambiguity" is-to-plurialize="true" />
In TranslationKey
private Set <translation> translations = new HashSet<translation>(); 

Simplify many-to-many relationship

Simplify many-to-many relationship variable when there is an ambiguity. In fact language_x_translator and language_x_speaker both user and language. So it is by default not possible to add 'users' variable to language since there should be 2 'users' variable. And vice-versa for user entity where 'languages' collection variable would be duplicate. This means that by default the variable is ambiguous and its name is a combination of link table foreign key and the other-end table, which gives quite a complexe name to read. The convention below in combination to some enrichment offers the possibility to get simple names declaratively.
<reference-naming-convention type="apply-many-to-many-aliasing" is-to-plurialize="true"/>
Combined with many-to-many tables enrichment
                    <entity name="language_x_translator">
                        <field name="language_id" linkReferenceAlias="translating_language" linkToTargetEntity="LANGUAGE"/>
                        <field name="user_id" linkReferenceAlias="translator" linkToTargetEntity="USER"/>
                    </entity>
                    <entity name="LANGUAGE_X_SPEAKER">
                        <field name="LANGUAGE_ID" linkToTargetEntity="LANGUAGE"
                            linkToTargetField="IDLANGUAGE" linkReferenceAlias="spoken_language" />
                        <field name="user_id" linkReferenceAlias="speaker" linkToTargetEntity="USER"/>
                    </entity>
Gives for User entity
    @ManyToMany
    @JoinTable(name="LANGUAGE_X_SPEAKER", 
        joinColumns=@JoinColumn(name="user_id"), 
        inverseJoinColumns=@JoinColumn(name="language_id") 
    )
    private Set <language> spokenLanguages = new HashSet <Language> ();

    @ManyToMany
    @JoinTable(name="LANGUAGE_X_TRANSLATOR", 
        joinColumns=@JoinColumn(name="user_id"), 
        inverseJoinColumns=@JoinColumn(name="language_id") 
    )
    private Set <language> translatingLanguages = new HashSet <Language> ();
Gives for Language entity
    @ManyToMany
    @JoinTable(name="LANGUAGE_X_SPEAKER", 
        joinColumns=@JoinColumn(name="language_id"), 
        inverseJoinColumns=@JoinColumn(name="user_id") 
    )
    private Set <user> speakers = new HashSet <User> ();

    @ManyToMany
    @JoinTable(name="LANGUAGE_X_TRANSLATOR", 
        joinColumns=@JoinColumn(name="language_id"), 
        inverseJoinColumns=@JoinColumn(name="user_id") 
    )
    private Set <user> translators = new HashSet <User> ();

Local enrichment

Fine grain tuning: Granularity at the level of the entity or attribute.

Modify the name of the entity with alias

<entity name="APPLICATION" alias="registered application" >
Gives
@Entity (name="RegisteredApplication")
@Table (name="application")
public class RegisteredApplication ...

Modify the name of the field with alias

<field name="TYPE" alias="obedience" >
Gives
    @Column(name="type")
    private ApplicationType obedience;

Add enumeration

                        <field name="TYPE" alias="obedience" >
                            <property tag="checkconstraint" alias="application_type">
                                <property name="OPENSOURCE"/>
                                <property name="COPYRIGHT" />
                            </property>
                        </field>
Gives
    @Enumerated (EnumType.STRING)
    @Column(name="type")
    private ApplicationType obedience; 
And a Enum artefact
public enum ApplicationType {

    OPENSOURCE("OPENSOURCE"),
    COPYRIGHT("COPYRIGHT");

    private final String value;
...

Providing content type of an entity

<entity name="LANGUAGE" content-type="reference-data"/>
Gives an entry in ehcache.xml configuration
   <cache
    name="net.sf.mp.demo.tranxy.domain.tranxy.Language"
        maxElementsInMemory="5000"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="false"
   />

Generation

Same as for demo one:
Put TRANXY-JPA2-2.xml in /mywork/config
Run
>model-generation.cmd TRANXY-JPA2-2.xml

But what happened to my altered code?

It is kept. Your validation annotation are not erased!

With Minuteproject one shot-generation as 'too-much-often' seen is over!
Be ready for continuous-refactoring of your backend!

Build and test

The model has changed and 2 new fields are mandatory to create a Language. The unit test is modified.
package mytest;

import javax.persistence.*;
import javax.validation.*;
import javax.validation.constraints.*;

import static junit.framework.Assert.*;

import org.junit.*;

import net.sf.mp.demo.tranxy.domain.tranxy.*;

public class TranxyTest {

 EntityManagerFactory emf = Persistence.createEntityManagerFactory("tranxy");
 EntityManager em = emf.createEntityManager();
 
 @Test
 public void testLanguage() {
  EntityTransaction tx = em.getTransaction(); 
  tx.begin();
  Language language = new Language();
  language.setCode("FR");
  
  //demo2
  language.setDescription("France");
  language.setLocale("fr");
  
  em.persist(language);
  tx.commit();
 }
 
    @Test
    public void testTooSmallLanguage() {

        try {
      EntityTransaction tx = em.getTransaction(); 
      tx.begin();
      Language language = new Language();
      language.setCode("F");
      em.persist(language);
            fail("Expected ConstraintViolationException wasn't thrown.");
            tx.commit();
        } 
        catch (ConstraintViolationException e) {
            assertEquals(1, e.getConstraintViolations().size());
            ConstraintViolation violation = 
                e.getConstraintViolations().iterator().next();

            assertEquals("code", violation.getPropertyPath().toString());
            assertEquals(
                Size.class, 
                violation.getConstraintDescriptor().getAnnotation().annotationType());
        }
    } 
 
 @Before
 public void clean () {
  EntityTransaction tx = em.getTransaction(); 
  tx.begin();
  Query q = em.createQuery("delete Language");
  int i = q.executeUpdate();
  tx.commit();
 }
}

To build run
>mvn clean package

Integration technics

Two are standard
  • integration by extension
  • integration by overriding
Minuteproject adds a new one
  • Integration by alteration/mutation
  • 3 types of alteration
    • artifact level (exclude for next generation)
    • snippet level
      • added part
      • updatable part

Download

The result can be downloaded on google code minuteproject.







RigaJUG - Demo 1 - JPA2

First Demo of RigaJUG agenda.
This demo presents the track JPA2 of minuteproject. 

Starting from a simple model that holds translation information.
Minuteproject will generate a JPA2 layer.

This demo will illustrate:
  • Generation by console or via config file
  • Altering generated code to add JSR 303 (validation) annotation.
  • Writing a unit test
  • Enrichment facilities and customisation
Prerequisits
  • Download Minuteproject last version
  • Java 6 in path
  • Maven in path
  • Install model on myql
SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL';

DROP SCHEMA IF EXISTS `tranxy` ;
CREATE SCHEMA IF NOT EXISTS `tranxy` DEFAULT CHARACTER SET latin1 ;
USE `tranxy` ;

-- -----------------------------------------------------
-- Table `tranxy`.`translation_key`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`translation_key` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`translation_key` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT ,
  `key_name` VARCHAR(200) NULL DEFAULT NULL ,
  `description` VARCHAR(400) NULL DEFAULT NULL ,
  PRIMARY KEY (`id`) )
ENGINE = InnoDB
DEFAULT CHARACTER SET = latin1;


-- -----------------------------------------------------
-- Table `tranxy`.`language`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`language` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`language` (
  `idlanguage` INT(11) NOT NULL AUTO_INCREMENT ,
  `code` VARCHAR(45) NOT NULL ,
  PRIMARY KEY (`idlanguage`) ,
  UNIQUE INDEX `code_UNIQUE` (`code` ASC) )
ENGINE = InnoDB
DEFAULT CHARACTER SET = latin1;


-- -----------------------------------------------------
-- Table `tranxy`.`traduction`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tranxy`.`traduction` ;

CREATE  TABLE IF NOT EXISTS `tranxy`.`traduction` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT ,
  `translation` VARCHAR(800) NULL DEFAULT NULL ,
  `language_id` INT(11) NOT NULL ,
  `Key_id` BIGINT(20) NOT NULL ,
  PRIMARY KEY (`id`) ,
  INDEX `fk_traduction_language` (`language_id` ASC) ,
  INDEX `fk_traduction_Key1` (`Key_id` ASC) ,
  CONSTRAINT `fk_traduction_Key1`
    FOREIGN KEY (`Key_id` )
    REFERENCES `tranxy`.`translation_key` (`id` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_traduction_language`
    FOREIGN KEY (`language_id` )
    REFERENCES `tranxy`.`language` (`idlanguage` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB
DEFAULT CHARACTER SET = latin1;



SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;

Console generation

By console

In /application run
>start-console.cmd/sh
Fill the 'Data model reverse-engineering' tab
And click on generate

The output goes in /output/trans/JPA2
To build the resulting package execute
>mvn clean package

By command line

Add configuration in /mywork/config
TRANXY-JPA2-1.xml
<!DOCTYPE root>
<generator-config xmlns="http://minuteproject.sf.net/xsd/mp-config" 
xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" 
xs:noNamespaceSchemaLocation="../config/mp-config.xsd">
 <configuration>
  <conventions>
   <target-convention type="enable-updatable-code-feature" />
  </conventions>
  <model name="tranxy" version="1.0" package-root="net.sf.mp.demo">
   <data-model>
    <driver name="mysql" version="5.1.16" groupId="mysql"
     artifactId="mysql-connector-java"></driver>
    <dataSource>
     <driverClassName>org.gjt.mm.mysql.Driver</driverClassName>
     <url>jdbc:mysql://127.0.0.1:3306/tranxy</url>
     <username>root</username>
     <password>mysql</password>
    </dataSource>
    <primaryKeyPolicy oneGlobal="false" >
     <primaryKeyPolicyPattern name="autoincrementPattern"></primaryKeyPolicyPattern>
    </primaryKeyPolicy>
   </data-model>
   <business-model>
    <business-package default="tranxy">
     <condition type="package" startsWith="trans" result="translation"></condition>
    </business-package>
    <enrichment>
     <conventions>
      <!-- manipulate the structure and entities BEFORE manipulating the 
       entities -->
      <column-naming-convention type="apply-strip-column-name-suffix"
       pattern-to-strip="ID" />
      <reference-naming-convention
       type="apply-referenced-alias-when-no-ambiguity" is-to-plurialize="true" />
     </conventions>
    </enrichment>
   </business-model>
  </model>
  <targets>
   <target refname="JPA2" fileName="mp-template-config-JPA2.xml"
    outputdir-root="../../dev/latvianjug/output/JPA2" templatedir-root="../../template/framework/jpa">
    <property name="add-querydsl" value="2.1.2"></property>
    <property name="add-jpa2-implementation" value="hibernate"></property>
    <property name="environment" value="remote"></property>
   </target>
      <target refname="CACHE-LIB" fileName="mp-template-config-CACHE-LIB.xml"
          templatedir-root="../../template/framework/cache">
      </target>
   <target refname="LIB" fileName="mp-template-config-bsla-LIB-features.xml"
    templatedir-root="../../template/framework/bsla">
   </target>
  </targets>
 </configuration>
</generator-config>

Execute by running:
> model-generation.cmd/sh TRANXY-JPA2-1.xml
The ouput goes in /dev/latvianjug/output/JPA2

Make a build:
>mvn clean package

Console vs. Command line generation

The generation made by the command line has more enrichment facilities than just working via the console.
Example: here the configuration add querydsl integration.
The rest of the demo will focus on the configuration enrichment facilities.
Remark:
Ideally the console should move into a IDE plugin manipulating the configuration.

Resulting artefacts

Summary
  • A maven pom project
  • 3 JPA2 entities (one for each table)
Pom artefact
  • provide a jar with name and version number given in configuration
  • has hibernate, querydsl, mysql driver, junit dependencies
JPA2 entities
  • packaged logically entity starting with 'trans' goes to package translation other goes to 'tranxy' package
  • convention 'apply-strip-column-name-suffix' when ending with 'ID'
    • DB naming convention used (the foreign key name is composed of the name of the link entity + '_id' is converted into java by provided name stripped from the 'Id' particule.
  • primary key strategy:
    • Based on auto increment pk when not natural.
JPA2 metamodel
  • each entity is associated to a metamodel java file to build type safe criteria queries.
persistence.xml: 2 are generated.
MinuteProject artefacts are designed to be tested and ready to be deployed!
  • in src/main/resources/META-INF
    • with reference to a JTA datasource
  • in src/test/resources/META-INF
    • with reference to an embedded Connection pool
Global convention
All artefacts have an updatable nature, meaning that you can change the code or add new code and consecutive generation will keep your modifications.

Code

Translation key
/**
 * Copyright (c) minuteproject, minuteproject@gmail.com
 * All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * More information on minuteproject:
 * twitter @minuteproject
 * wiki http://minuteproject.wikispaces.com 
 * blog http://minuteproject.blogspot.net
 * 
*/
/**
 * template reference : 
 * - name : DomainEntityJPA2Annotation
 * - file name : DomainEntityJPA2Annotation.vm
*/
package net.sf.mp.demo.tranxy.domain.translation;

//MP-MANAGED-ADDED-AREA-BEGINNING @import@
//MP-MANAGED-ADDED-AREA-ENDING @import@
import java.sql.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;

import java.io.Serializable;
import javax.persistence.*;
import net.sf.mp.demo.tranxy.domain.tranxy.Traduction;

/**
 *
 * <p>Title: TranslationKey</p>
 *
 * <p>Description: Domain Object describing a TranslationKey entity</p>
 *
 */
@Entity (name="TranslationKey")
@Table (name="translation_key")
@NamedQueries({
  @NamedQuery(name="TranslationKey.findAll", query="SELECT translationKey FROM TranslationKey translationKey")
 ,@NamedQuery(name="TranslationKey.findByKeyName", query="SELECT translationKey FROM TranslationKey translationKey WHERE translationKey.keyName = :keyName")
 ,@NamedQuery(name="TranslationKey.findByKeyNameContaining", query="SELECT translationKey FROM TranslationKey translationKey WHERE translationKey.keyName like :keyName")
 ,@NamedQuery(name="TranslationKey.findByDescription", query="SELECT translationKey FROM TranslationKey translationKey WHERE translationKey.description = :description")
 ,@NamedQuery(name="TranslationKey.findByDescriptionContaining", query="SELECT translationKey FROM TranslationKey translationKey WHERE translationKey.description like :description")
})
public class TranslationKey implements Serializable {
    private static final long serialVersionUID = 1L;
 
    public static final String FIND_ALL = "TranslationKey.findAll";
    public static final String FIND_BY_KEYNAME = "TranslationKey.findByKeyName";
    public static final String FIND_BY_KEYNAME_CONTAINING ="TranslationKey.findByKeyNameContaining";
    public static final String FIND_BY_DESCRIPTION = "TranslationKey.findByDescription";
    public static final String FIND_BY_DESCRIPTION_CONTAINING ="TranslationKey.findByDescriptionContaining";
 
    @Id @Column(name="id" )
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

//MP-MANAGED-ADDED-AREA-BEGINNING @key_name-field-annotation@
//MP-MANAGED-ADDED-AREA-ENDING @key_name-field-annotation@
//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @ATTRIBUTE-key_name@
    @Column(name="key_name",  length=200,  nullable=true,  unique=false)
    private String keyName; 
//MP-MANAGED-UPDATABLE-ENDING

//MP-MANAGED-ADDED-AREA-BEGINNING @description-field-annotation@
//MP-MANAGED-ADDED-AREA-ENDING @description-field-annotation@
//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @ATTRIBUTE-description@
    @Column(name="description",  length=400,  nullable=true,  unique=false)
    private String description; 
//MP-MANAGED-UPDATABLE-ENDING

//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @traductions-field-translation_key@
    @OneToMany (targetEntity=net.sf.mp.demo.tranxy.domain.tranxy.Traduction.class, fetch=FetchType.LAZY, mappedBy="key", cascade=CascadeType.REMOVE)//, cascade=CascadeType.ALL)
    private Set <Traduction> traductions = new HashSet<Traduction>(); 

//MP-MANAGED-UPDATABLE-ENDING
    /**
    * Default constructor
    */
    public TranslationKey() {
    }

 /**
 * All field constructor 
 */
    public TranslationKey(
       Long id,
       String keyName,
       String description) {
       //primary keys
       setId (id);
       //attributes
       setKeyName (keyName);
       setDescription (description);
       //parents
    }

 public TranslationKey flat() {
    return new TranslationKey(
          getId(),
          getKeyName(),
          getDescription()
    );
 }

    public Long getId() {
        return id;
    }
 
    public void setId (Long id) {
        this.id =  id;
    }
    
//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @GETTER-SETTER-key_name@
    public String getKeyName() {
        return keyName;
    }
 
    public void setKeyName (String keyName) {
        this.keyName =  keyName;
    }    
//MP-MANAGED-UPDATABLE-ENDING

//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @GETTER-SETTER-description@
    public String getDescription() {
        return description;
    }
 
    public void setDescription (String description) {
        this.description =  description;
    }    
//MP-MANAGED-UPDATABLE-ENDING



//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @traductions-getter-translation_key@
    public Set<Traduction> getTraductions() {
        if (traductions == null){
            traductions = new HashSet<Traduction>();
        }
        return traductions;
    }

    public void setTraductions (Set<Traduction> traductions) {
        this.traductions = traductions;
    } 
    
    public void addTraductions (Traduction traduction) {
         getTraductions().add(traduction);
    }
    
//MP-MANAGED-UPDATABLE-ENDING


//MP-MANAGED-ADDED-AREA-BEGINNING @implementation@
//MP-MANAGED-ADDED-AREA-ENDING @implementation@

}
Language
/**
 * Copyright (c) minuteproject, minuteproject@gmail.com
 * All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * More information on minuteproject:
 * twitter @minuteproject
 * wiki http://minuteproject.wikispaces.com 
 * blog http://minuteproject.blogspot.net
 * 
*/
/**
 * template reference : 
 * - name : DomainEntityJPA2Annotation
 * - file name : DomainEntityJPA2Annotation.vm
*/
package net.sf.mp.demo.tranxy.domain.tranxy;

//MP-MANAGED-ADDED-AREA-BEGINNING @import@
//MP-MANAGED-ADDED-AREA-ENDING @import@
import java.sql.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;

import java.io.Serializable;
import javax.persistence.*;
import net.sf.mp.demo.tranxy.domain.tranxy.Traduction;

/**
 *
 * <p>Title: Language</p>
 *
 * <p>Description: Domain Object describing a Language entity</p>
 *
 */
@Entity (name="Language")
@Table (name="language")
@NamedQueries({
  @NamedQuery(name="Language.findAll", query="SELECT language FROM Language language")
 ,@NamedQuery(name="Language.findByCode", query="SELECT language FROM Language language WHERE language.code = :code")
 ,@NamedQuery(name="Language.findByCodeContaining", query="SELECT language FROM Language language WHERE language.code like :code")
})
public class Language implements Serializable {
    private static final long serialVersionUID = 1L;
 
    public static final String FIND_ALL = "Language.findAll";
    public static final String FIND_BY_CODE = "Language.findByCode";
    public static final String FIND_BY_CODE_CONTAINING ="Language.findByCodeContaining";
 
    @Id @Column(name="idlanguage" )
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer idlanguage;

//MP-MANAGED-ADDED-AREA-BEGINNING @code-field-annotation@
//MP-MANAGED-ADDED-AREA-ENDING @code-field-annotation@
//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @ATTRIBUTE-code@
    @Column(name="code",  length=45, nullable=false,  unique=false)
    private String code; 
//MP-MANAGED-UPDATABLE-ENDING

//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @traductions-field-language@
    @OneToMany (targetEntity=net.sf.mp.demo.tranxy.domain.tranxy.Traduction.class, fetch=FetchType.LAZY, mappedBy="language", cascade=CascadeType.REMOVE)//, cascade=CascadeType.ALL)
    private Set <Traduction> traductions = new HashSet<Traduction>(); 

//MP-MANAGED-UPDATABLE-ENDING
    /**
    * Default constructor
    */
    public Language() {
    }

 /**
 * All field constructor 
 */
    public Language(
       Integer idlanguage,
       String code) {
       //primary keys
       setIdlanguage (idlanguage);
       //attributes
       setCode (code);
       //parents
    }

 public Language flat() {
    return new Language(
          getIdlanguage(),
          getCode()
    );
 }

    public Integer getIdlanguage() {
        return idlanguage;
    }
 
    public void setIdlanguage (Integer idlanguage) {
        this.idlanguage =  idlanguage;
    }
    
//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @GETTER-SETTER-code@
    public String getCode() {
        return code;
    }
 
    public void setCode (String code) {
        this.code =  code;
    }    
//MP-MANAGED-UPDATABLE-ENDING



//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @traductions-getter-language@
    public Set<Traduction> getTraductions() {
        if (traductions == null){
            traductions = new HashSet<Traduction>();
        }
        return traductions;
    }

    public void setTraductions (Set<Traduction> traductions) {
        this.traductions = traductions;
    } 
    
    public void addTraductions (Traduction traduction) {
         getTraductions().add(traduction);
    }
    
//MP-MANAGED-UPDATABLE-ENDING


//MP-MANAGED-ADDED-AREA-BEGINNING @implementation@
//MP-MANAGED-ADDED-AREA-ENDING @implementation@

}
Tranduction
/**
 * Copyright (c) minuteproject, minuteproject@gmail.com
 * All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * More information on minuteproject:
 * twitter @minuteproject
 * wiki http://minuteproject.wikispaces.com 
 * blog http://minuteproject.blogspot.net
 * 
*/
/**
 * template reference : 
 * - name : DomainEntityJPA2Annotation
 * - file name : DomainEntityJPA2Annotation.vm
*/
package net.sf.mp.demo.tranxy.domain.tranxy;

//MP-MANAGED-ADDED-AREA-BEGINNING @import@
//MP-MANAGED-ADDED-AREA-ENDING @import@
import java.sql.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;

import java.io.Serializable;
import javax.persistence.*;
import net.sf.mp.demo.tranxy.domain.translation.TranslationKey;
import net.sf.mp.demo.tranxy.domain.tranxy.Language;

/**
 *
 * <p>Title: Traduction</p>
 *
 * <p>Description: Domain Object describing a Traduction entity</p>
 *
 */
@Entity (name="Traduction")
@Table (name="traduction")
@NamedQueries({
  @NamedQuery(name="Traduction.findAll", query="SELECT traduction FROM Traduction traduction")
 ,@NamedQuery(name="Traduction.findByTranslation", query="SELECT traduction FROM Traduction traduction WHERE traduction.translation = :translation")
 ,@NamedQuery(name="Traduction.findByTranslationContaining", query="SELECT traduction FROM Traduction traduction WHERE traduction.translation like :translation")
})
public class Traduction implements Serializable {
    private static final long serialVersionUID = 1L;
 
    public static final String FIND_ALL = "Traduction.findAll";
    public static final String FIND_BY_TRANSLATION = "Traduction.findByTranslation";
    public static final String FIND_BY_TRANSLATION_CONTAINING ="Traduction.findByTranslationContaining";
 
    @Id @Column(name="id" )
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

//MP-MANAGED-ADDED-AREA-BEGINNING @translation-field-annotation@
//MP-MANAGED-ADDED-AREA-ENDING @translation-field-annotation@
//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @ATTRIBUTE-translation@
    @Column(name="translation",  length=800,  nullable=true,  unique=false)
    private String translation; 
//MP-MANAGED-UPDATABLE-ENDING

    @ManyToOne (fetch=FetchType.LAZY , optional=false)
    @JoinColumn(name="Key_id", referencedColumnName = "id", nullable=false,  unique=false ) 
    private TranslationKey key;  

    @Column(name="Key_id",  nullable=false,  unique=false, insertable=false, updatable=false)
    private Long key_;

    @ManyToOne (fetch=FetchType.LAZY , optional=false)
    @JoinColumn(name="language_id", referencedColumnName = "idlanguage", nullable=false,  unique=false ) 
    private Language language;  

    @Column(name="language_id",  nullable=false,  unique=false, insertable=false, updatable=false)
    private Integer language_;

    /**
    * Default constructor
    */
    public Traduction() {
    }

 /**
 * All field constructor 
 */
    public Traduction(
       Long id,
       String translation,
       Integer language,
       Long key) {
       //primary keys
       setId (id);
       //attributes
       setTranslation (translation);
       //parents
       this.key = new TranslationKey();
       this.key.setId(key); //ID
       this.language = new Language();
       this.language.setIdlanguage(language); //IDLANGUAGE
    }

 public Traduction flat() {
    return new Traduction(
          getId(),
          getTranslation(),
          getLanguage_(),
          getKey_()
    );
 }

    public Long getId() {
        return id;
    }
 
    public void setId (Long id) {
        this.id =  id;
    }
    
//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @GETTER-SETTER-translation@
    public String getTranslation() {
        return translation;
    }
 
    public void setTranslation (String translation) {
        this.translation =  translation;
    }    
//MP-MANAGED-UPDATABLE-ENDING


    public TranslationKey getKey () {
     return key;
    }
 
    public void setKey (TranslationKey key) {
     this.key = key;
    }

    public Long getKey_() {
        return key_;
    }
 
    public void setKey_ (Long key) {
        this.key_ =  key;
    }
 
    public Language getLanguage () {
     return language;
    }
 
    public void setLanguage (Language language) {
     this.language = language;
    }

    public Integer getLanguage_() {
        return language_;
    }
 
    public void setLanguage_ (Integer language) {
        this.language_ =  language;
    }

//MP-MANAGED-ADDED-AREA-BEGINNING @implementation@
//MP-MANAGED-ADDED-AREA-ENDING @implementation@

}
persistence.xml in test directory
<?xml version="1.0"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
             version="1.0">
<!--MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @PERSISTENCE-UNIT-tranxy@-->
    <persistence-unit name="tranxy" transaction-type="RESOURCE_LOCAL">
<!--MP-MANAGED-UPDATABLE-ENDING-->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <!-- tranxy --> 
        <class>net.sf.mp.demo.tranxy.domain.tranxy.Language</class>
        <class>net.sf.mp.demo.tranxy.domain.tranxy.Traduction</class>
        <!-- translation --> 
        <class>net.sf.mp.demo.tranxy.domain.translation.TranslationKey</class>
        <properties>
            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            <property name="hibernate.connection.driver_class" value="org.gjt.mm.mysql.Driver" />
            <property name="hibernate.connection.url" value="jdbc:mysql://127.0.0.1:3306/tranxy" />
            <property name="hibernate.connection.username" value="root" />
            <property name="hibernate.connection.password" value="mysql" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
        </properties> 
    </persistence-unit>
</persistence>

Persistence.xml in main directory

The persistence.xml provides a configuration with embedded connection pool.
If the user want to use a remote connection pool accessed by JNDI, the user has to put property

lt;property name="environment" value="remote" />
Under the target JPA2 node.

Alter Generated Code and Unit Test

With Minuteproject one shot-generation as 'too-much-often' seen is over!
Be ready for continuous-refactoring of your backend!

Add a validation annotation

In Language class
//MP-MANAGED-ADDED-AREA-BEGINNING @import@
import javax.validation.constraints.*;
//MP-MANAGED-ADDED-AREA-ENDING @import@

...
//MP-MANAGED-ADDED-AREA-BEGINNING @code-field-annotation@
    @Size(min = 2)
//MP-MANAGED-ADDED-AREA-ENDING @code-field-annotation@
//MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @ATTRIBUTE-code@
    @Column(name="code",  length=45, nullable=false,  unique=false)
    private String code; 

Add the imports between the MP-MANAGED-ADDED-AREA-BEGINNING @import@ and MP-MANAGED-ADDED-AREA-ENDING @import@ comments
Add the annotation between MP-MANAGED-ADDED-AREA-BEGINNING @xxxx-field-annotation@ and MP-MANAGED-ADDED-AREA-ENDING @xxxx-field-annotation@ comments

Now further generation will keep your added code.

Unit test

In src/test/java add the TranxyTest class in package mytest
The validation scenario has been inspired by this link.

package mytest;

import javax.persistence.*;
import javax.validation.*;
import javax.validation.constraints.*;

import static junit.framework.Assert.*;

import org.junit.*;

import net.sf.mp.demo.tranxy.domain.tranxy.*;

public class TranxyTest {

 EntityManagerFactory emf = Persistence.createEntityManagerFactory("tranxy");
 EntityManager em = emf.createEntityManager();
 
 @Test
 public void testLanguage() {
  EntityTransaction tx = em.getTransaction(); 
  tx.begin();
  Language language = new Language();
  language.setCode("FR");
  em.persist(language);
  tx.commit();
 }
 
    @Test
    public void testTooSmallLanguage() {

        try {
      EntityTransaction tx = em.getTransaction(); 
      tx.begin();
      Language language = new Language();
      language.setCode("F");
      em.persist(language);
            fail("Expected ConstraintViolationException wasn't thrown.");
            tx.commit();
        } 
        catch (ConstraintViolationException e) {
            assertEquals(1, e.getConstraintViolations().size());
            ConstraintViolation violation = 
                e.getConstraintViolations().iterator().next();
            assertEquals("code", violation.getPropertyPath().toString());
            assertEquals(
                Size.class, 
                violation.getConstraintDescriptor().getAnnotation().annotationType());
        }
    } 
 
 @Before
 public void clean () {
  EntityTransaction tx = em.getTransaction(); 
  tx.begin();
  Query q = em.createQuery("delete Language");
  q.executeUpdate();
  tx.commit();
 }
}


Execute with
>mvn clean package
The 2 tests pass.

Download


The result can be downloaded on google code minuteproject.

Monday, December 19, 2011

Productivity by Example: Liferay reverse-engineered

In this experiment, minuteproject demonstrates that it is possible to use Reverse-Engineering as a development methodology for Big Projects AND this from bootstrap time to the entire lifecycle of a Project.

To illustrate this postulat, I take enterprise-size opensource model (Liferay with150+ entities) upon which I apply minuteproject tracks.
It will quickly appear that performing 'bulk reverse-engineering' have strong limitations. Meanwhile by applying conventions deduced from the 'Reverse-Analysis' discipline, it will be possible to overcome them.
The generation will run releasing in couple of seconds (3 to 10) hundreds of artefacts.
Following Minuteproject philosophy, you'll will have to write 0 (Zero) LOC to get a working result and this can be achieved within 5 min-.
Eventually some code will be written on top and the result will be tested.
The technologies discussed will be:
  • JPA2 with Hibernate implementation
  • Querydsl
  • Ehcache 
  • Fitnesse
But other MP tracks can be applied.
The conclusion resumes the productivity benefit at project and enterprise level of embracing MinuteProject productivity philosophy.

Liferay
Liferay is an opensource portal, this article only focus on the database model.
Liferay Model
Setup 
The model is install on mysql database.
Now you can introspect your model.

Reverse-Analysis crash course
At first sight 150+ tables, 0 views.
Some tables start with QUARTZ, those are likely to be shipped with the Quartz scheduling framework, so they should be excluded from the generation.

At second sight (on mysql installation)
Entity with primary key but no foreign keys. => No relationships?
Mysql schema primary key are not autoincrement.

If reverse-engineering is performed as-is, then the bulk resulting artefacts in JPA2 will contain no relationships (no @OneToMany, @ManyToOne and @ManyToMany), so the resulting value will be quite poor (no graph navigation, query link to be done manually, no type safe benefit of Querydsl or Metamodel...)

Relation detection convention
Since the relationships are not formalised in terms of constraint, does it mean that there is none?
In fact, when having a deeper look inside the tables, some fields have name such as USER_ID and there is a strong chance that they are used for a relationship link with the User_ table primary key.
Let's assume it.
Since in minuteproject configuration the user can perform some enrichment and one of this enrichment consists in defining relationship, it is possible to enrich every table containing the field USER_ID by indicating it acts as a foreign key towards USER_ table.
// add snippet
It's fine, but it might be cumbersome since you'll have to do it more than 40 times and only for the 'towards User' relationships. What about the others?
A wiser way is to apply a convention:
What we need is to be able to detect relationships when those match a pattern.The pattern here is that the 'considered' foreign keys are fields ending with 'id' and whose beginning (column name without ending 'id') correspond to an existing table.
But this was not enough since some table ends with '_'.
So the convention has to take care of those exceptions and the match is done against the a map of those entity alias if the match was not successful via direct binding.
The final configuration of the convention is

<foreign-key-convention type="autodetect-foreign-key-based-on-similarity-and-map" 
column-ending="id" column-starting="" column-body="match-entity">
<property tag="map-entity" name="user_" value="user"/> 
<property tag="map-entity" name="group_" value="group"/>
<property tag="map-entity" name="organization_" value="organization"/>
<property tag="map-entity" name="permission_" value="permission"/>
</foreign-key-convention>

With this convention it is now possible to have 'virtual' foreign keys used by the generator to create @OneToMany, @ManyToOne and @ManyToMany relationships for JPA2 track.

Naming convention
The JEE conventions can be different from those retrieved from the Database structure:
Example: Some column ends with 'id' but in java we might not want that.
Here the convention for that
<column-naming-convention type="apply-strip-column-name-suffix" pattern-to-strip="ID" />

Collection naming convention
While reverse-engineering a problem is to give unambiguous names to list elements. Otherwise in java you have a compilation error. The safest way is to compose the name of this collection relationship (@OneToMany) with the name of the field holding the foreign key, and the name of entity having this FK. It get even worse when dealing with a many2many relationship where the intermediary link table as to be associated.
As a result, you can have very long name which is not quite handy.
Fortunately the convention apply-reference-alias-when-no-ambiguity is there for you!
<reference-naming-convention type="apply-referenced-alias-when-no-ambiguity" is-to-plurialize="true" />
It checks if while reducing the 'unambiguous' default name to 'just' the associated table (plurialize as option) there is no colision.
Remark
With conventions bear in mind that they are applied sequentially and so the order is very important!
Aliasing 
Entity name or column name can be aliased.
The alias will be used for the Java name but the ORM mapping will match the alias name to the correct entity name. 
Caching
To enable caching add a property to the target JPA2
               <property name="add-cache-implementation" value="ehcache"></property>
It is possible to associate caching to entities by two means:
By enrichment at entity level: Each entity marked as having their content-type="master-data" or ="reference-data" will have an associate cache entry
<entity name="country" content-type="reference-data"></entity>
By convention: describe a pattern of entity matching the requirement. Example all the entity ending with '_' could be considered as reference data
<entity-content-type-convention type="apply-content-type-to-entity-ending-with" pattern="_" content-type="reference-data"></entity-content-type-convention>
In this case the entity 'user', 'account', 'classname', 'contact', 'group', 'lock', 'organisation', 'permission', 'release', 'resouce' and 'role' will match the convention and thus have their cache entry in ehcache.xml
MinuteProject input
Input configuration
The analysis is resumed in the mp-config_LR.xml serving as input of MinuteProject
<!DOCTYPE root>
<generator-config>
    <configuration>
        <conventions>
            <target-convention type="enable-updatable-code-feature" />
        </conventions>       
        <model name="liferay" version="1.0" package-root="net.sf.mp.demo">
            <data-model>
                <driver name="mysql" version="5.1.16" groupId="mysql" artifactId="mysql-connector-java"></driver>
                <dataSource>
                    <driverClassName>org.gjt.mm.mysql.Driver</driverClassName>
                    <url>jdbc:mysql://127.0.0.1:3306/lportal</url>
                    <username>root</username>
                    <password>mysql</password>
                </dataSource>
                <primaryKeyPolicy oneGlobal="true">
                    <primaryKeyPolicyPattern name="none"></primaryKeyPolicyPattern>
                </primaryKeyPolicy>
            </data-model>
            <business-model>   
                    <generation-condition>
                    <condition type="exclude" startsWith="QUARTZ"></condition>
                    </generation-condition>
                <business-package default="liferay">
                    <condition type="package" startsWith="social" result="social"></condition>
                    <condition type="package" startsWith="user" result="user"></condition>
                    <condition type="package" startsWith="journal" result="journal"></condition>               
                </business-package>
                <enrichment>
                    <conventions>
                        <column-naming-convention type="apply-strip-column-name-suffix" pattern-to-strip="ID" />
                        <foreign-key-convention type="autodetect-foreign-key-based-on-similarity-and-map"
                            column-ending="id" column-starting="" column-body="match-entity">
                            <property tag="map-entity" name="user_" value="user"/>
                            <property tag="map-entity" name="group_" value="group"/>
                            <property tag="map-entity" name="organization_" value="organization"/>
                            <property tag="map-entity" name="permission_" value="permission"/>
                        </foreign-key-convention>
                        <reference-naming-convention type="apply-referenced-alias-when-no-ambiguity" is-to-plurialize="true" />       
                        <entity-content-type-convention type="apply-content-type-to-entity-ending-with" pattern="_" content-type="reference-data"></entity-content-type-convention>
                    </conventions>
                    <entity name="user_" alias="user"></entity>
                    <entity name="role_" alias="role"></entity>
                    <entity name="group_" alias="group"></entity>
                    <entity name="permission_" alias="permission"></entity>
                    <entity name="organization_" alias="organization"></entity>
                    <entity name="classname_" alias="classname"></entity>
                    <entity name="country" content-type="reference-data"></entity>
                </enrichment>
            </business-model>
        </model>
        <targets>
            <target refname="JPA2"
               fileName="mp-template-config-JPA2.xml"
               outputdir-root="../../dev/liferay/output/JPA2"
               templatedir-root="../../template/framework/jpa">
               <property name="add-querydsl" value="2.1.2"></property>
               <property name="add-jpa2-implementation" value="hibernate"></property>
               <property name="add-cache-implementation" value="ehcache"></property>
               
            </target>                        
            <target refname="LIB" fileName="mp-template-config-bsla-LIB-features.xml"
                templatedir-root="../../template/framework/bsla">
            </target>
    <target refname="CACHE-LIB" 
      fileName="mp-template-config-CACHE-LIB.xml" 
      templatedir-root="../../template/framework/cache">
    </target>            
        </targets>
    </configuration>
</generator-config>
Generation
Adapt the above config with the liferay database connection credentials.
You need version 0.7+ of minuteproject (and check that everything is fine by running the demos)
To generate place the following file mp-config_LR.xml into /mywork/config
Execute model-generation.(sh/cmd) mp-config_LR.xml

MinuteProject output
Generated artefacts
Entities
140+ entities have been created
12 Embeddable Id when composite primary have been found
Relationships
+- 300 OneToMany
+- 300 ManyToOne
+- 20 ManyToMany
Configuration
Entity Associate MetaModel for typesafe query
Maven pom.xml with querydsl integration
persistence.xml for test (local connection pool) and for release (jndi connection pool).
ehcache.xml

And All That in 10s less!
(local instance of mysql)


If you want to see what the resulting code looks like you can download on googlecode (version for minuteproject 0.8).
If you want to see what the Liferay datamodel looks like with relationships check here.


Test
But does it really work?
The sources are generated in /dev/liferay/output/JPA2
Writing a simple unit test in src/test/java which illustrates:
  • that you can write business added value
  • that querydsl for typesafe as an alternative to metamodel (MetaModel entities are also generated) is integrated
  • that ORM, ehcache configuration works
package my.liferay.test;

import java.util.List;

import javax.persistence.*;

import com.mysema.query.jpa.impl.JPAQuery;

import junit.framework.TestCase;
import net.sf.mp.demo.liferay.domain.liferay.Country;
import net.sf.mp.demo.liferay.domain.liferay.QCountry;

public class LiferayJPA2Test extends TestCase{
   
    public void testCountry() {
        // Initializes the Entity manager
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("liferay");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        //use Query dsl for type safe query
        JPAQuery query = new JPAQuery(em);
        QCountry qcountry = QCountry.country;
        List l = query.from(qcountry)
           .where(qcountry.countryid.lt(0))
           .list(qcountry);
        //remove the countries
        for (Country country:l) {
         em.remove(country);
        }
        // create a country
        Country c = new Country();
        // no primary policy key given so associate one manually 
        c.setCountryid(new Long(-100));
        c.setName("NEW-country");
        c.setA2("A2");
        c.setA3("A3");
        c.setActive(new Short("1"));
        c.setNumber("5");

        tx.begin();
        em.persist(c);
        tx.commit();

        em.close();
        emf.close();   
    }
}

Running with maven
Execute mvn package
This will compile and test the code.
In the build, querydsl classes will be generated and compiled
In the test, at run time, ORM and ehcache configuration will be loaded.

The sql statement issue by Hibernate at the end of the test is 
Hibernate:
    insert
    into
        country
        (a2, a3, active_, idd_, name, number_, countryId)
    values
        (?, ?, ?, ?, ?, ?, ?)
So yes, it works!
And package liferayBackEnd-1.0-SNAPSHOT.jar is created.

But what happens if there is a compile time or runtime issue with the generated code?
Do not worry the generated code is updatable which means that you can patch the code but your modification will be kept over successive generations. More information at http://minuteproject.wikispaces.com/Updatable_Generated_Code

Extend
Here the reverse-engineering target just one type of target JPA2 track.
Extend with other MinuteProject tracks
Example:
FitNessize you development: Adding the following snippet into the 'targets' node of the main configuration will generate an entire Fitnesse wiki ready to use for your acceptance testing.
   <target refname="FitNesse" 
      name="default" 
      fileName="mp-template-config-fitnesse.xml" 
      outputdir-root="../../dev/liferay/output/FITNESSE"
      templatedir-root="../../template/framework/fitnesse">
   </target>
For more information see this article
Extend you model
Enrich you model with:
  • views
  • store procedure
  • constraints
Minuteproject will generate relevant artefacts for them.
Conclusions
This article shows how to set reverse-engineering approach as a fast development methodology.
It is far less intrusive and restricted than Domain Driven Development and much faster.
This article also insists on how to apply your own conventions via 'declarative conventions'.

Minuteproject unleashes your productivity!
Things and tedious tasks that where bound to stay for the lifetime of your project can be removed.
At this point it takes more time for a DBA to write and execute an alter statement than to have full backend synchronisation!
Now you can move agile on your backend development.

Conclusion for the LifeRay Dev team:
Altering your backend might be quite long if you want to go to JPA2 track described above. Meanwhile you can use MinuteProject without any intrusivity on other tracks.
MinuteProject FitNesse fixture generated for Liferay and this really help increasing QA!
Try other MinuteProject tracks: for example any DB query can be RESTified (fully REST app generated) within seconds...

More information
More information about Minuteproject 4 JPA2 track can be found here.

Thursday, December 15, 2011

Adding Ehcache to Openxava application

Introduction
This article shows how to quickly enable Ehcache on Openxava applications and thus improving performance.
When viewing an entity and its graph, relationships are loaded. Adding a second-level cache fasten the retrieval of associated elements, since already loaded elements are retrieved from cache and not database.
Eventually this page explains how minuteproject treats this aspect with keeping its promise: write nothing.
As an example, we will take the Lazuly minuteproject showcase.
Openxava-Ehcache integration
In Openxava, you describe your model in the manner of Java annotated POJO.The annotations come from the standard JPA2 ORM and Openxava specific ones.
But nothing prevents you to add others. This is what is done to add caching. There are also couple of configurations to undertake to enable caching.
List of actions
  1. Add ehcache.xml config file at the root of your sources
  2. Modify persistence.xml to include second level cache
  3. Add caching annotation (alongside JPA2)
Remark:
Openxava comes with the ehcache.jar so there is no need to add a dependency.
Detailed actions
Add ehcache.xml
In /persistence place ehcache.xml file
<ehcache>
    <defaultCache
            maxElementsInMemory="1000"
            eternal="false"
            timeToIdleSeconds="300"
            timeToLiveSeconds="300"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="300"
            memoryStoreEvictionPolicy="LRU"
            />
   <cache
    name="your.domain.object"
    maxElementsInMemory="5000"
    eternal="false"
    timeToIdleSeconds="300"
    timeToLiveSeconds="600"
    overflowToDisk="false"
   />
</ehcache>
Modify persistence.xml
Persistence.xml file contains information related to the persitence unit such as connection pool info, class or configuration to load. 'persistence.xml' is located in /persistence/META-INF
We will append properties for L2 cache.
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
            <property name="hibernate.cache.provider_class" value="net.sf.ehcache.hibernate.SingletonEhCacheProvider" />
            <property name="net.sf.ehcache.configurationResourceName" value="/ehcache.xml" />
            <property name="hibernate.cache.use_query_cache" value="true" />
            <property name="hibernate.cache.use_second_level_cache" value="true" />
            <property name="hibernate.generate_statistics" value="true" />   

        </properties>
Add cache annotation
Here the hibernate annotation is used instead of the standard one (Cacheable in fact seems not to work)
Place Cache annotation at class level of your domain object.
@org.hibernate.annotations.Cache(usage = org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE)
Example
Lazuly application
Lazuly is a sample database holding conference information used for MinuteProject showcase purpose.
Minuteproject generates a comprehensive set of artefacts to speedup the release of OX application.
Further information can be found in Minuteproject 4 Openxava Lazuly showcase.
On this part we focus on the artefacts generated for the caching specific.
Minuteproject for the generation base itself on a configuration file, where we define the datamodel to reverse engineer. In this configuration there is an enrichement part where you can add information.
One of this information deals with the type of content is held in an entity. There are 4 possibilities (reference-data, master-data, pseudo-static-data, live-business-data)
If you enrich your entity with the content-type="master-data" or "reference-data" MinuteProject 4 Openxava will generate associated caching.
This is done here for the entity Country.
     <entity name="COUNTRY" content-type="reference-data">
Here are the cache related artefacts

ehcache.xml
<ehcache>

  <!--
    Sets the path to the directory where cache files are created.

    If the path is a Java System Property it is replaced by its value in the
    running VM.

    The following properties are translated:
    * user.home - User's home directory
    * user.dir - User's current working directory
    * java.io.tmpdir - Default temp file path

    Subdirectories can be specified below the property e.g. java.io.tmpdir/one
    -->
<!--MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @ehcache-main-config-conference@-->
    <diskStore path="java.io.tmpdir"/>

 <!--
    Mandatory Default Cache configuration. These settings will be applied to caches
    created programmtically using CacheManager.add(String cacheName)
    -->
    <defaultCache
            maxElementsInMemory="1000"
            eternal="false"
            timeToIdleSeconds="300"
            timeToLiveSeconds="300"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="300"
            memoryStoreEvictionPolicy="LRU"
            />
<!-- The unnamed query cache -->
   <cache
    name="org.hibernate.cache.StandardQueryCache"
    maxElementsInMemory="1000"
    eternal="false"
    timeToLiveSeconds="300"
    overflowToDisk="false"
   />
<!--MP-MANAGED-UPDATABLE-ENDING-->

<!--MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @cache-entity-country-conference@-->
   <cache
    name="net.sf.mp.demo.conference.domain.admin.Country"
    maxElementsInMemory="5000"
    eternal="false"
    timeToIdleSeconds="300"
    timeToLiveSeconds="600"
    overflowToDisk="false"
   />
<!--MP-MANAGED-UPDATABLE-ENDING-->

<!--MP-MANAGED-ADDED-AREA-BEGINNING @custom-cache-definition@-->
<!--MP-MANAGED-ADDED-AREA-ENDING @custom-cache-definition@-->

</ehcache>
Persistence.xml
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
             version="1.0">
             
    <!-- Tomcat + Hypersonic -->
    <persistence-unit name="default">
     <non-jta-data-source>java:comp/env/jdbc/conferenceDS</non-jta-data-source>
     <class>org.openxava.session.GalleryImage</class>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
            <property name="hibernate.cache.provider_class" value="net.sf.ehcache.hibernate.SingletonEhCacheProvider" />
            <property name="net.sf.ehcache.configurationResourceName" value="/ehcache.xml" />
            <property name="hibernate.cache.use_query_cache" value="true" />
            <property name="hibernate.cache.use_second_level_cache" value="true" />
            <property name="hibernate.generate_statistics" value="true" />   
<!--MP-MANAGED-ADDED-AREA-BEGINNING @properties@-->
<!--MP-MANAGED-ADDED-AREA-ENDING @properties@-->
        </properties>
<!--MP-MANAGED-ADDED-AREA-BEGINNING @persistence-unit@-->
<!--MP-MANAGED-ADDED-AREA-ENDING @persistence-unit@-->
    </persistence-unit>       

<!--MP-MANAGED-ADDED-AREA-BEGINNING @persistence@-->
<!--MP-MANAGED-ADDED-AREA-ENDING @persistence@-->

</persistence>
Class annotation
@org.hibernate.annotations.Cache(usage = org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE)
//MP-MANAGED-ADDED-AREA-BEGINNING @class-annotation@
//MP-MANAGED-ADDED-AREA-ENDING @class-annotation@
public class Country {

    @Hidden @Id @Column(name="id" )
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id; 
...
Generated code remark
The generated code has markers inside file extension comment.
Within MP-MANAGED-ADDED-AREA-BEGINNING and  MP-MANAGED-ADDED-AREA-ENDING you can place customized code
Within MP-MANAGED-UPDATABLE-BEGINNING-DISABLE and  MP-MANAGED-UPDATABLE-ENDING you can alter the code. To keep your modifications please change MP-MANAGED-UPDATABLE-BEGINNING-DISABLE into MP-MANAGED-UPDATABLE-BEGINNING-ENABLE.
Updatable code prevent you to lose your customisation over consecutive generations.
For more information on updatable code see Minuteproject updatable code.
Generation
  • Place the following file mp-config-LAZULY-OPENXAVA.xml in /mywork/config
  • on a prompt execute mp-model-generation(.sh/cmd) mp-config-LAZULY-OPENXAVA.xml 
  • the resulting artefacts in /DEV/output/openxava/conference 
To generate use the updated version of mp-config-LAZULY-OPENXAVA.xml
<!DOCTYPE root>
<generator-config>
 <configuration>
  <conventions>
   <target-convention type="enable-updatable-code-feature" />
  </conventions>  
  <model name="conference" version="1.0" package-root="net.sf.mp.demo">
   <data-model>
    <driver name="mysql" version="5.1.16" groupId="mysql" artifactId="mysql-connector-java"></driver>
    <dataSource>
     <driverClassName>org.gjt.mm.mysql.Driver</driverClassName>
     <url>jdbc:mysql://127.0.0.1:3306/conference</url>
     <username>root</username>
     <password>mysql</password>
    </dataSource>
    <!--
     for Oracle and DB2 please set the schema <schema> </schema>
    -->
    <primaryKeyPolicy oneGlobal="true">
     <primaryKeyPolicyPattern name="autoincrementPattern"></primaryKeyPolicyPattern>
    </primaryKeyPolicy>
   </data-model>
   <business-model>
    <!--
     <generation-condition> <condition type="exclude"
     startsWith="DUAL"></condition> </generation-condition>
    -->
    <business-package default="conference">
        <condition type="package" startsWith="STAT" result="statistics"></condition>
        <condition type="package" startsWith="COUNTRY" result="admin"></condition>
        <condition type="package" startsWith="ROLE" result="admin"></condition>    
    </business-package>
    <enrichment>
     <conventions>
      <column-naming-convention type="apply-strip-column-name-suffix"
       pattern-to-strip="_ID" />
      <reference-naming-convention
       type="apply-referenced-alias-when-no-ambiguity" is-to-plurialize="true" />
     </conventions>

     <entity name="COUNTRY" content-type="reference-data">
      <semantic-reference>
       <sql-path path="NAME" />
      </semantic-reference>
     </entity>
     <entity name="CONFERENCE_MEMBER">
      <semantic-reference>
       <sql-path path="FIRST_NAME" />
       <sql-path path="LAST_NAME" />
      </semantic-reference>
      <field name="STATUS">
       <property tag="checkconstraint" alias="conference_member_status">
        <property name="PENDING" value="PENDING" />
        <property name="ACTIVE" value="ACTIVE" />
       </property>
      </field>
      <field name="EMAIL">
       <stereotype stereotype="EMAIL" />
      </field>
     </entity>
     <entity name="SPEAKER">
      <field name="BIO">
       <stereotype stereotype="HTML_TEXT" />
      </field>
      <field name="PHOTO">
       <stereotype stereotype="PHOTO" />
      </field>
      <field name="WEB_SITE_URL">
       <stereotype stereotype="WEBURL" />
      </field>
     </entity>
     <entity name="PRESENTATION">
      <field name="STATUS">
       <property tag="checkconstraint" alias="presentation_status">
        <property name="PROPOSAL" value="PROPOSAL" />
        <property name="ACTIVE" value="ACTIVE" />
       </property>
      </field>
     </entity>
     <entity name="SPONSOR">
      <field name="STATUS">
       <property tag="checkconstraint" alias="sponsor_status">
        <property name="PENDING" value="PENDING" />
        <property name="ACTIVE" value="ACTIVE" />
       </property>
      </field>
      <field name="PRIVILEGE_TYPE">
       <property tag="checkconstraint" alias="sponsor_privilege">
        <property name="GOLDEN" value="Golden" />
        <property name="SILVER" value="Silver" />
        <property name="BRONZE" value="Bronze" />
       </property>
      </field>
     </entity>
     <!-- views -->
     <entity name="stat_mb_per_ctry_conf" alias="MEMBER_PER_COUNTRY_AND_CONFERENCE">
      <virtual-primary-key isRealPrimaryKey="true">
       <property name="virtualPrimaryKey" value="ID" />
      </virtual-primary-key>
     </entity>
     <entity name="stat_mb_by_role" alias="MEMBER_PER_ROLE_COUNTRY_AND_CONFERENCE">
      <virtual-primary-key isRealPrimaryKey="true">
       <property name="virtualPrimaryKey" value="id" />
      </virtual-primary-key>
      <field name="stat_mb_per_ctry_conf_ID" linkToTargetEntity="stat_mb_per_ctry_conf"
       linkToTargetField="id"></field>
     </entity>
    </enrichment>
   </business-model>
  </model>
  <targets>
   <!-- openxava -->
   <target refname="OpenXava" name="OpenXava"
    fileName="mp-template-config-openxava-last-features.xml"
    outputdir-root="../../DEV/output/openxava/conference"
    templatedir-root="../../template/framework/openxava">
   </target>

   <target refname="JPA2-LIB" fileName="mp-template-config-JPA2-LIB.xml"
    templatedir-root="../../template/framework/jpa">
   </target>
   
   <target refname="BSLA-LIB" fileName="mp-template-config-bsla-LIB-features.xml"
    templatedir-root="../../template/framework/bsla">
   </target>

   <target refname="CACHE-LIB" 
      fileName="mp-template-config-CACHE-LIB.xml" 
      templatedir-root="../../template/framework/cache">
   </target>
               
  </targets>
 </configuration>
</generator-config>

Test
To ensure that the caching is working properly:
  • Enable hibernate logging. Add the following snippet as extra properties in persistence.xml.
            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
  • navigate to an entity that reference country (example Address)
  • When you view the detail of this entity you will notice that there is a load of the associated entity 'country'
  • But the second time you access to the details of this entity (or another entity referencing the same country instance), the country is not loaded twice from the database.