Table of Contents

Object/relational mapping
Entity mapping
Property mapping
Reference mapping
Collection mapping
Embedded reference mapping
Type conversion
Property conversion
Multiple column conversion
Reference conversion
Unique Constraints
Other dialects
Reference guide: Model | View | Tabular data | Object/relational mapping | Controllers | Application | Customizing

Object/relational mapping

Object relational mapping allows you to declare in which tables and columns of your database the entity data will be stored.
Object/relational tools allow you to work with objects instead of tables and columns, and to generate automatically the SQL code to read and update the database. In this way you do not need direct access to the SQL database. Of course you have to define precisely how to map your classes to your tables, and this work is done using JPA mapping annotations.
The OpenXava entities are JPA entities, therefore the object/relational mapping in OpenXava is done by means of the Java Persistence API (JPA). This chapter shows the more basic mapping techniques and some special cases. If you want to learn more about JPA you can look at the documentation of Hibernate Annotations (the JPA implementation used by OpenXava by default), or whatever JPA manual you want.

Entity mapping

The @Table annotation specifies the primary table for the annotated entity. Additional tables may be specified using @SecondaryTable or @SecondaryTables annotation.
If no @Table annotation is specified for an entity class, the default values apply.
Example:
@Entity
@Table(name="CUST", schema="XAVATEST")
public class Customer {
 

Property mapping

The @Column annotation is used to specify a mapped column for a persistent property or field. If no @Column annotation is specified, the default values are applied.
A simple example:
@Column(name="DESC", length=512)
private String description;
 
An example annotating the getter:
@Column(name="DESC", nullable=false, length=512)
public String getDescription() { return description; }
 
Other examples:
@Column(name="DESC",
 columnDefinition="CLOB NOT NULL",
 table="EMP_DETAIL")
@Lob
private String description;
 
@Column(name="ORDER_COST", updatable=false, precision=12, scale=2)
private BigDecimal cost;
 

Reference mapping

The @JoinColumn annotation is used to specify a mapped column for a reference.
Example:
@ManyToOne
@JoinColumn(name="CUST_ID")
private Customer customer;
If you need to define a mapping for the composite foreign keys use @JoinColumns. This annotation groups @JoinColumn annotations for the same reference.
When the @JoinColumns annotation is used, both the name and the referencedColumnName elements must be specified in each such @JoinColumn annotation.
Example:
@ManyToOne
@JoinColumns({
 @JoinColumn(name="INV_YEAR", referencedColumnName="YEAR"),
 @JoinColumn(name="INV_NUMBER", referencedColumnName="NUMBER")
})
private Invoice invoice;

Collection mapping

When you use @OneToMany for a collection the mapping depends of the reference used in the other part of the association, that is, usually it's not needed to do anything. But if you are using @ManyToMany, maybe it's useful to declare the @JoinTable, as following:
@ManyToMany
@JoinTable(name="CUSTOMER_STATE",
 joinColumns=@JoinColumn(name="CUSTOMER"),
 inverseJoinColumns=@JoinColumn(name="STATE")
)
private Collection<State> states;
If @JoinTable is missing the default values apply.

When you use @ElementCollection (new in v5.0) for a collection you can use @CollectionTable and @AttributeOverrides, as following:
@ElementCollection
@CollectionTable(name="HOMES") // Uses default join column name
@AttributeOverrides({
    @AttributeOverride(name="street",
        column=@Column(name="HOME_STREET")),
    @AttributeOverride(name="city",
        column=@Column(name="HOME_CITY")),
    @AttributeOverride(name="state",
        column=@Column(name="HOME_STATE"))
})
private Collection<Address> vacationHomes;
If @CollectionTable and @AttributeOverrides are missing the default values apply.

Embedded reference mapping

An embedded reference contains data that in the relational model are stored in the same table as the main entity. For example, if you have an embeddable Address associated to a Customer, the address data is stored in the same data table as the customer data. How can you map this case with JPA?
Just using @AttributeOverrides annotations, in this way:
@Embedded
@AttributeOverrides({
 @AttributeOverride(name="street", column=@Column("ADDR_STREET")),
 @AttributeOverride(name="zip", column=@Column("ADDR_ZIP"))
 @AttributeOverride(name="city", column=@Column("ADDR_CITY")),
 @AttributeOverride(name="country", column=@Column("ADDR_COUNTRY"))
})
private Address address;
If you do not use @AttributeOverrides default values are assumed.

Type conversion

The type conversion between Java and the relational database is a work for the JPA implementation (OpenXava uses Hibernate by default). Usually, the default type conversion is good for most cases, but if you work with legacy database perhaps you need to use the tips here.
Given that OpenXava uses the type conversion facility provided by Hibernate you can learn more on Hibernate documentation.

Property conversion

When the type of a Java property and the type of its corresponding column in DB do not match you need to write a Hibernate Type in order to do your custom type conversion.
For example, if you have a property of type String [], and you want to store its value concatenating it in a single table column of VARCHAR type. Then you must declare the conversion for your property in this way:
@Type(type="org.openxava.test.types.RegionsType")
private String [] regions;
The conversion logic in RegionsType is:
package org.openxava.test.types;
 
import java.io.*;
import java.sql.*;
 
import org.apache.commons.logging.*;
import org.hibernate.*;
import org.hibernate.engine.spi.*; // Since OpenXava 5.3 that uses Hibernate 4.3
import org.hibernate.usertype.*;
import org.openxava.util.*;
 
/**
 *
 * @author Javier Paniza
 */
public class RegionsType implements UserType { // 1
 
    public int[] sqlTypes() {
        return new int[] { Types.VARCHAR };
    }
 
    public Class returnedClass() {
        return String[].class;
    }
 
    public boolean equals(Object obj1, Object obj2) throws HibernateException {
        return Is.equal(obj1, obj2);
    }
 
    public int hashCode(Object obj) throws HibernateException {
        return obj.hashCode();
    }
 
    // SessionImplementer argument since OpenXava 5.3 that uses Hibernate 4.3
    public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor implementor, Object owner) // 2
        throws HibernateException, SQLException
    {
        Object o = resultSet.getObject(names[0]);
        if (o == null) return new String[0];
        String dbValue = (String) o;
        String [] javaValue = new String [dbValue.length()];
        for (int i = 0; i < javaValue.length; i++) {
            javaValue[i] = String.valueOf(dbValue.charAt(i));
        }
        return javaValue;
    }
 
    // SessionImplementer argument since OpenXava 5.3 that uses Hibernate 4.3
    public void nullSafeSet(PreparedStatement ps, Object value, int index, SessionImplementor implementor) // 3
        throws HibernateException, SQLException
    {
        if (value == null) {
            ps.setString(index, "");
            return;
        }
        String [] javaValue = (String []) value;
        StringBuffer dbValue = new StringBuffer();
        for (int i = 0; i < javaValue.length; i++) {
            dbValue.append(javaValue[i]);
        }
        ps.setString(index, dbValue.toString());
    }
 
    public Object deepCopy(Object obj) throws HibernateException {
        return obj == null?null:((String []) obj).clone();
    }
 
    public boolean isMutable() {
        return true;
    }
 
    public Serializable disassemble(Object obj) throws HibernateException {
        return (Serializable) obj;
    }
 
    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return cached;
    }
 
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
 
}
The type converter has to implement org.hibernate.usertype.UserType (1). The main methods are nullSafeGet (2) for read from database and to convert to Java, and nullSafeSet (3) for writing the Java value into database.
OpenXava has generic Hibernate type converters in the org.openxava.types package ready to use. One of them is EnumLetterType, that allows to map properties of enum type. For example, if you have a property like this:
private Distance distance;
public enum Distance { LOCAL, NATIONAL, INTERNATIONAL };
In this Java property 'LOCAL' is 1, 'NATIONAL' is 2 and 'INTERNATIONAL' is 3 when the property is stored in database. But what happens, if in the database a single letter ('L', 'N' or 'I') is stored? In this case you can use EnumLetterType in this way:
@Type(type="org.openxava.types.EnumLetterType",
    parameters={
        @Parameter(name="letters", value="LNI"),
        @Parameter(name="enumType", value="org.openxava.test.model.Delivery$Distance")
    }
)
private Distance distance;
public enum Distance { LOCAL, NATIONAL, INTERNATIONAL };
As you put 'LNI' as a value to letters, the type converter matches the 'L' to 1, the 'N' to 2 and the 'I' to 3. You also see how type converters are configurable using its properties and this makes the converters more reusable.

Multiple column conversion

With CompositeUserType you can map several table columns to a single Java property. This is useful if you have properties of custom class that have itself several attributes to store. Also it is used when you have to deal with legacy database schemes.
A typical example is the generic converter Date3Type, that allows to store in the database 3 columns and in Java a single property of type java.util.Date.
@Type(type="org.openxava.types.Date3Type")
@Columns(columns = {
    @Column(name="YEARDELIVERY"),
    @Column(name="MONTHDELIVERY"),
    @Column(name="DAYDELIVERY")
})
private java.util.Date deliveryDate;
 
DAYDELIVERY, MONTHDELIVERY and YEARDELIVERY are 3 columns in database that store the delivery date. And here Date3Type:
package org.openxava.types;
 
import java.io.*;
import java.sql.*;
 
import org.hibernate.*;
import org.hibernate.engine.*; // Until OpenXava 5.2.x
import org.hibernate.engine.spi.*; // Since OpenXava 5.3 that uses Hibernate 4.3
import org.hibernate.type.*;
import org.hibernate.usertype.*;
import org.openxava.util.*;
 
/**
 * In java a <tt>java.util.Date</tt> and in database 3 columns of
 * integer type. <p>
 *
 * @author Javier Paniza
 */
public class Date3Type implements CompositeUserType { // 1
 
    public String[] getPropertyNames() {
        return new String[] { "year", "month", "day" };
    }
 
    public Type[] getPropertyTypes() {
        // return new Type[] { Hibernate.INTEGER, Hibernate.INTEGER, Hibernate.INTEGER }; // Before OpenXava 5.3/Hibernate 4.3
        return new Type[] { IntegerType.INSTANCE, IntegerType.INSTANCE, IntegerType.INSTANCE }; // Since OpenXava 5.3/Hibernate 4.3
    }
 
    public Object getPropertyValue(Object component, int property) throws HibernateException { // 2
        java.util.Date date = (java.util.Date) component;
        switch (property) {
            case 0:
                return Dates.getYear(date);
            case 1:
                return Dates.getMonth(date);
            case 2:
                return Dates.getDay(date);
        }
        throw new HibernateException(XavaResources.getString("date3_type_only_3_properties"));
    }
 
    public void setPropertyValue(Object component, int property, Object value)
        throws HibernateException // 3
    {
        java.util.Date date = (java.util.Date) component;
        int intValue = value == null?0:((Number) value).intValue();
        switch (property) {
            case 0:
                Dates.setYear(date, intValue);
            case 1:
                Dates.setMonth(date, intValue);
            case 2:
                Dates.setDay(date, intValue);
        }
        throw new HibernateException(XavaResources.getString("date3_type_only_3_properties"));
    }
 
    public Class returnedClass() {
        return java.util.Date.class;
    }
 
    public boolean equals(Object x, Object y) throws HibernateException {
        if (x==y) return true;
        if (x==null || y==null) return false;
        return !Dates.isDifferentDay((java.util.Date) x, (java.util.Date) y);
    }
 
    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }
 
    public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner)
        throws HibernateException, SQLException // 4
    {
        /* Before OpenXava 5.3/Hibernate 4.3
        Number year = (Number) Hibernate.INTEGER.nullSafeGet( rs, names[0] );
        Number month = (Number) Hibernate.INTEGER.nullSafeGet( rs, names[1] );
        Number day = (Number) Hibernate.INTEGER.nullSafeGet( rs, names[2] );
        */
        // Since OpenXava 5.3/Hibernate 4.3
        Number year = (Number) IntegerType.INSTANCE.nullSafeGet( rs, names[0], session, owner);
        Number month = (Number) IntegerType.INSTANCE.nullSafeGet( rs, names[1], session, owner );
        Number day = (Number) IntegerType.INSTANCE.nullSafeGet( rs, names[2], session, owner );
 
        int iyear = year == null?0:year.intValue();
        int imonth = month == null?0:month.intValue();
        int iday = day == null?0:day.intValue();
 
        return Dates.create(iday, imonth, iyear);
    }
 
    public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session)
        throws HibernateException, SQLException // 5
    {
        java.util.Date d = (java.util.Date) value;
        /* Before OpenXava 5.3/Hibernate 4.3
        Hibernate.INTEGER.nullSafeSet(st, Dates.getYear(d), index);
        Hibernate.INTEGER.nullSafeSet(st, Dates.getMonth(d), index + 1);
        Hibernate.INTEGER.nullSafeSet(st, Dates.getDay(d), index + 2);
        */
        // Since OpenXava 5.3/Hibernate 4.3
        IntegerType.INSTANCE.nullSafeSet(st, Dates.getYear(d), index, session);
        IntegerType.INSTANCE.nullSafeSet(st, Dates.getMonth(d), index + 1, session);
        IntegerType.INSTANCE.nullSafeSet(st, Dates.getDay(d), index + 2, session);
    }
 
    public Object deepCopy(Object value) throws HibernateException {
        java.util.Date d = (java.util.Date) value;
        if (value == null) return null;
        return (java.util.Date) d.clone();
    }
 
    public boolean isMutable() {
        return true;
    }
 
    public Serializable disassemble(Object value, SessionImplementor session)
        throws HibernateException
    {
        return (Serializable) deepCopy(value);
    }
 
    public Object assemble(Serializable cached, SessionImplementor session, Object owner)
        throws HibernateException
    {
        return deepCopy(cached);
    }
 
    public Object replace(Object original, Object target, SessionImplementor session, Object owner)
        throws HibernateException
    {
        return deepCopy(original);
    }
 
}
As you see the type converter implements CompositeUserType (1). The key methods are getPropertyValue (2) and setPropertyValue (3) to get and to set values in the properties of the object of the composite type, and nullSafeGet (4) and nullSafeSet (5) for reading and storing this object from and to database.

Reference conversion

Reference conversion is not supported directly by Hibernate. But in some very rare circumstances maybe you need to do conversion in the reference. In this section we explain how to do it.
For example, you may have a reference to driver licence using two columns, DRIVINGLICENCE_LEVEL and DRIVINGLICENCE_TYPE, and the DRIVINGLICENCE_TYPE column does not admit null, but it's possible that the object can have no reference to driving lincence in which case the column DRIVINGLICENCE_TYPE hold an empty string. This is not a normal case if you design the database using foreign keys, but if the database was designed by a RPG programmer, for example, this was done in this way, because RPG programmer are not used to cope with nulls.
That is, you need a conversion for DRIVINGLICENCE_TYPE, for transform null to empty string. This can be achieve with a code like this:
// We apply conversion (null into an empty String) to DRIVINGLICENCE_TYPE column
// In order to do it, we create drivingLicence_level and drivingLicence_type
// We make JoinColumns not insertable nor updatable, we modify the get/setDrivingLincence methods
// and we create a drivingLicenceConversion() method.
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumns({ // 1
 @JoinColumn(name="DRIVINGLICENCE_LEVEL", referencedColumnName="LEVEL",
 insertable=false, updatable=false),
 @JoinColumn(name="DRIVINGLICENCE_TYPE", referencedColumnName="TYPE",
 insertable=false, updatable=false)
})
private DrivingLicence drivingLicence;
private Integer drivingLicence_level; // 2
private String drivingLicence_type; // 2
 
public DrivingLicence getDrivingLicence() { // 3
 // In this way because the column for type of driving lincence does not admit null
 try {
 if (drivingLicence != null) drivingLicence.toString(); // to force load
 return drivingLicence;
 }
 catch (EntityNotFoundException ex) {
 return null;
 }
}
 
public void setDrivingLicence(DrivingLicence licence) { // 4
 // In this way because the column for type of driving lincence does not admit null
 this.drivingLicence = licence;
 this.drivingLicence_level = licence==null?null:licence.getLevel();
 this.drivingLicence_type = licence==null?null:licence.getType();
}
 
@PrePersist @PreUpdate
private void drivingLicenceConversion() { // 5
 if (this.drivingLicence_type == null) this.drivingLicence_type = "";
}
 
First, you have to use @JoinColumns with insertable=false and updatable=false on all @JoinColumn (1), in this way the reference is readed from database, but it is not write. Also define plain properties for storing the foreign key of the reference (2).
Now you must write a getter, getDrivingLicence() (3), for returning null when the reference is not found; and a setter, setDrivingLicence() (4), for assigning the key of the reference to the correspoding plain properties.
Finally, you have to write a callback method, drivingLincenceConversion() (5), to do the conversion work. This method will be automatically executed on create and update.
This example shows how it's possible to wrap legacy databases simply using a little of programming and some basic resources from JPA.

Unique Constraints

Since v4.9 openxava lets you to customize messages of declared constraints in the element uniqueConstraints of @Table and @SecondaryTable, as well as @Column(unique = true). To do this, keep in mind some preliminary considerations regarding Hibernate (The JPA implementation used by default by openxava).
Example:
package org.openxava.test.model;
 
@Entity
@SecondaryTable(
    name="APPLICATIONUSER_INFO",
    uniqueConstraints={
        @UniqueConstraint(name="not_repeat_user_info", columnNames={"name", "birthdate", "sex"})
    }
)
public class ApplicationUser extends Identifiable {
 
    @Required
    @Column(length=8, unique=true) //not_repeat_nic
    private String nic;
 
    @Column(length=40, table="APPLICATIONUSER_INFO")
    private String name;
 
    @Column(length=40, table="APPLICATIONUSER_INFO")
    private Date birthdate;
 
    @Column(table="APPLICATIONUSER_INFO")
    @Enumerated(EnumType.STRING)
    private Sex sex;
    public enum Sex { MALE, FEMALE }
   ...
}
create table APPLICATIONUSER_INFO (
    birthdate datetime,
    name varchar(40),
    sex varchar(255),
    id varchar(32) not null,
    primary key (id), unique (name, birthdate, sex)
)
create table ApplicationUser (
    id varchar(32) not null,
    nic varchar(8) unique,
    primary key (id)
)
alter table APPLICATIONUSER_INFO
    add index FK375C9572BA846971 (id),
    add constraint FK375C9572BA846971 foreign key (id) references ApplicationUser (id)
As can be seen Hibernate has mapped the structure of our ApplicationUser class has even created the constraint unique(name, birthdate, sex) and nic varchar (8) unique but did not assign declared name ("no_repeat_user_info") in @UniqueConstraint nor there is an element in @Column that allows us name the constraint unique=true, letting the database engine assigns default names.

Therefore, if We want to generate a custom message to the above constraints, We do:
create table APPLICATIONUSER_INFO (
   birthdate datetime,
   name varchar(40),
   sex varchar(255),
   id varchar(32) not null,
   primary key (id),
   unique key `not_repeat_user_info` (name, birthdate, sex)
)
create table ApplicationUser (
   id varchar(32) not null,
   nic varchar(8),
   primary key (id),
   unique key `not_repeat_nic` (nic)
)
package dialect;
 
import java.sql.*;
import org.hibernate.dialect.*;
import org.hibernate.exception.*;
 
public class XMySQL5Dialect extends MySQL5Dialect {
 
    public XMySQL5Dialect(){}
 
    private static ViolatedConstraintNameExtracter EXTRACTER = new TemplatedViolatedConstraintNameExtracter() {
        public String extractConstraintName(SQLException sqle) {
            try {
                int sqlState = Integer.valueOf( JDBCExceptionHelper.extractSqlState(sqle)).intValue();
                switch (sqlState) {
                    case 23000: return extractUsingTemplate("for key '","'", sqle.getMessage());
                    default: return null;
                }
            } catch (NumberFormatException nfe) {
                return null;
            }
        }
    };
 
    @Override
    public ViolatedConstraintNameExtracter getViolatedConstraintNameExtracter() {
        return EXTRACTER;
    }
}
...
<property name="hibernate.dialect"value="dialect.XMySQL5Dialect"/>
...

Other dialects

import java.sql.*;
 
import org.hibernate.dialect.*;
import org.hibernate.exception.*;
 
public class XPostgreSQLDialect extends PostgreSQLDialect {
 
  public XPostgreSQLDialect(){}
 
  private static ViolatedConstraintNameExtracter EXTRACTER = new TemplatedViolatedConstraintNameExtracter() {
    public String extractConstraintName(SQLException sqle) {
        try {
            int sqlState = Integer.valueOf( JDBCExceptionHelper.extractSqlState(sqle)).intValue();
            switch (sqlState) {
                // CHECK VIOLATION
                case 23514: return extractUsingTemplate("violates check constraint \"","\"", sqle.getMessage());
                // UNIQUE VIOLATION
                case 23505:
                    if (sqle.getMessage().indexOf("violates unique constraint \"") > -1)
                        return extractUsingTemplate("violates unique constraint \"","\"", sqle.getMessage());
                    else if (sqle.getNextException() != null )
                        return extractConstraintName(sqle.getNextException());
                    else
                        return "UNIQUE_CONSTRAINT_VIOLATION_UNKNOWN";
                // FOREIGN KEY VIOLATION
                case 23503: return extractUsingTemplate("violates foreign key constraint \"","\"", sqle.getMessage());
                // NOT NULL VIOLATION
                case 23502: return extractUsingTemplate("null value in column \"","\" violates not-null constraint", sqle.getMessage());
                // RESTRICT VIOLATION
                case 23001: return null;
                // ALL OTHER
                default: return null;
                }
            } catch (NumberFormatException nfe) {
                return null;
            }
        }
    };
 
  @Override
  public ViolatedConstraintNameExtracter getViolatedConstraintNameExtracter() {
            return EXTRACTER;
  }
}
import java.sql.*;
import org.hibernate.dialect.*;
import org.hibernate.exception.*;
 
public class XSQLServerDialect extends SQLServer2008Dialect {
 
   public XSQLServerDialect() {}
 
   private static ViolatedConstraintNameExtracter EXTRACTER = new TemplatedViolatedConstraintNameExtracter() {
    public String extractConstraintName(SQLException sqle) {
        try {
            int sqlState = Integer.valueOf(JDBCExceptionHelper.extractSqlState(sqle)).intValue();
            switch (sqlState) {
                case 23000:
                    return extractUsingTemplate("UNIQUE KEY '", "'", sqle.getMessage());
                default:
                    return null;
            }
        } catch (NumberFormatException nfe) {
            return null;
        }
    }
    };
 
    @Override
     public ViolatedConstraintNameExtracter getViolatedConstraintNameExtracter() {
     return EXTRACTER;
     }
}