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.







No comments:

Post a Comment