blob: d0b32a8267aa24d097819648dd07c9475122229b [file] [log] [blame]
/* Copyright 2004-2005 the original author or authors.
*
* 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.
*/
package org.codehaus.groovy.grails.orm.hibernate.cfg;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.groovy.grails.commons.GrailsDomainClass;
import org.codehaus.groovy.grails.commons.GrailsDomainClassProperty;
import org.hibernate.FetchMode;
import org.hibernate.MappingException;
import org.hibernate.cfg.Mappings;
import org.hibernate.cfg.SecondPass;
import org.hibernate.id.PersistentIdentifierGenerator;
import org.hibernate.mapping.*;
import org.hibernate.mapping.Collection;
import org.hibernate.util.StringHelper;
import java.util.*;
import java.util.Map;
import java.util.Set;
/**
* Handles the binding Grails domain classes and properties to the Hibernate runtime meta model.
* Based on the HbmBinder code in Hibernate core and influenced by AnnotationsBinder.
*
* @author Graeme Rocher
* @since 06-Jul-2005
*/
public final class GrailsDomainBinder {
private static final String FOREIGN_KEY_SUFFIX = "_ID";
private static final Log LOG = LogFactory.getLog( GrailsDomainBinder.class );
/**
* A Collection type, for the moment only Set is supported
*
* @author Graeme
*
*/
abstract static class CollectionType {
private Class clazz;
public abstract Collection create(GrailsDomainClassProperty property, PersistentClass owner,
Mappings mappings) throws MappingException;
CollectionType(Class clazz) {
this.clazz = clazz;
}
public String toString() {
return clazz.getName();
}
private static CollectionType SET = new CollectionType(Set.class) {
public Collection create(GrailsDomainClassProperty property, PersistentClass owner, Mappings mappings) throws MappingException {
org.hibernate.mapping.Set coll = new org.hibernate.mapping.Set(owner);
coll.setCollectionTable(owner.getTable());
bindCollection( property, coll, owner, mappings );
return coll;
}
};
private static final Map INSTANCES = new HashMap();
static {
INSTANCES.put( SET.toString(), SET );
}
public static CollectionType collectionTypeForClass(Class clazz) {
return (CollectionType)INSTANCES.get( clazz.getName() );
}
}
/**
* Second pass class for grails relationships. This is required as all
* persistent classes need to be loaded in the first pass and then relationships
* established in the second pass compile
*
* @author Graeme
*
*/
static class GrailsCollectionSecondPass implements SecondPass {
private static final long serialVersionUID = -5540526942092611348L;
private GrailsDomainClassProperty property;
private Mappings mappings;
private Collection collection;
public GrailsCollectionSecondPass(GrailsDomainClassProperty property, Mappings mappings, Collection coll) {
this.property = property;
this.mappings = mappings;
this.collection = coll;
}
public void doSecondPass(Map persistentClasses, Map inheritedMetas) throws MappingException {
bindCollectionSecondPass( this.property, mappings, persistentClasses, collection,inheritedMetas );
}
}
private static void bindCollectionSecondPass(GrailsDomainClassProperty property, Mappings mappings, Map persistentClasses, Collection collection, Map inheritedMetas) {
PersistentClass associatedClass = null;
// Configure one-to-many
if(collection.isOneToMany()) {
OneToMany oneToMany = (OneToMany)collection.getElement();
String associatedClassName = oneToMany.getReferencedEntityName();
associatedClass = (PersistentClass)persistentClasses.get(associatedClassName);
// if there is no persistent class for the association throw
// exception
if(associatedClass == null) {
throw new MappingException( "Association references unmapped class: " + oneToMany.getReferencedEntityName() );
}
oneToMany.setAssociatedClass( associatedClass );
collection.setCollectionTable( associatedClass.getTable() );
collection.setLazy(true);
LOG.info( "Mapping collection: "
+ collection.getRole()
+ " -> "
+ collection.getCollectionTable().getName() );
}
// setup the primary key references
KeyValue keyValue;
String propertyRef = collection.getReferencedPropertyName();
// this is to support mapping by a property
if(propertyRef == null) {
keyValue = collection.getOwner().getIdentifier();
}
else {
keyValue = (KeyValue)collection.getOwner().getProperty( propertyRef ).getValue();
}
DependantValue key = new DependantValue(collection.getCollectionTable(), keyValue);
key.setTypeName(null);
//
if(property.isBidirectional()) {
GrailsDomainClassProperty otherSide = property.getOtherSide();
if(otherSide.isManyToOne()) {
collection.setInverse(true);
Iterator mappedByColumns = associatedClass.getProperty( otherSide.getName() ).getValue().getColumnIterator();
while(mappedByColumns.hasNext()) {
Column column = (Column)mappedByColumns.next();
linkValueUsingAColumnCopy(otherSide,column,key);
}
}
}
else {
bindSimpleValue( property,key,mappings );
}
collection.setKey( key );
// make required and non-updateable
key.setNullable(false);
key.setUpdateable(false);
// if we have a many-to-many
if(property.isManyToMany()) {
ManyToOne element = new ManyToOne( collection.getCollectionTable() );
collection.setElement(element);
bindManyToOne(property,element, mappings);
}
else if ( property.isOneToMany() && !property.isBidirectional() ) {
// for non-inverse one-to-many, with a not-null fk, add a backref!
OneToMany oneToMany = (OneToMany) collection.getElement();
String entityName = ( oneToMany ).getReferencedEntityName();
PersistentClass referenced = mappings.getClass( entityName );
Backref prop = new Backref();
prop.setName( '_' + property.getName() + "Backref" );
prop.setUpdateable( true );
prop.setInsertable( true );
prop.setCollectionRole( collection.getRole() );
prop.setValue( collection.getKey() );
prop.setOptional( property.isOptional() );
referenced.addProperty( prop );
}
}
private static void linkValueUsingAColumnCopy(GrailsDomainClassProperty prop, Column column, DependantValue key) {
Column mappingColumn = new Column();
mappingColumn.setName(column.getName());
mappingColumn.setLength(column.getLength());
mappingColumn.setNullable(!prop.isOptional());
mappingColumn.setSqlType(column.getSqlType());
mappingColumn.setValue(key);
key.addColumn( mappingColumn );
key.getTable().addColumn( mappingColumn );
}
/**
* First pass to bind collection to Hibernate metamodel, sets up second pass
*
* @param property The GrailsDomainClassProperty instance
* @param collection The collection
* @param owner The owning persistent class
* @param mappings The Hibernate mappings instance
*/
private static void bindCollection(GrailsDomainClassProperty property, Collection collection, PersistentClass owner, Mappings mappings) {
// set role
collection.setRole( StringHelper.qualify( property.getDomainClass().getFullName() , property.getName() ) );
// TODO: add code to configure optimistic locking
// TODO: configure fetch strategy
collection.setFetchMode( FetchMode.DEFAULT );
// if its a one-to-many mapping
if(property.isOneToMany()) {
OneToMany oneToMany = new OneToMany( collection.getOwner() );
collection.setElement( oneToMany );
bindOneToMany( property, oneToMany, mappings );
}
else {
String tableName = property.getReferencedDomainClass().getName();
Table table = mappings.addTable(
mappings.getSchemaName(),
mappings.getCatalogName(),
tableName,
null,
false
);
collection.setCollectionTable(table);
}
// setup second pass
mappings.addSecondPass( new GrailsCollectionSecondPass(property, mappings, collection) );
}
/**
* Binds a Grails domain class to the Hibernate runtime meta model
* @param domainClass The domain class to bind
* @param mappings The existing mappings
* @throws MappingException Thrown if the domain class uses inheritance which is not supported
*/
public static void bindClass(GrailsDomainClass domainClass, Mappings mappings)
throws MappingException {
if(domainClass.getClazz().getSuperclass() == java.lang.Object.class) {
bindRoot(domainClass, mappings);
}
else {
throw new MappingException("Grails domain classes do not support inheritance");
}
}
/**
* Binds the specified persistant class to the runtime model based on the
* properties defined in the domain class
* @param domainClass The Grails domain class
* @param persistentClass The persistant class
* @param mappings Existing mappings
*/
private static void bindClass(GrailsDomainClass domainClass, PersistentClass persistentClass, Mappings mappings) {
// set lazy loading for now
persistentClass.setLazy(true);
persistentClass.setEntityName(domainClass.getFullName());
persistentClass.setProxyInterfaceName( domainClass.getFullName() );
persistentClass.setClassName(domainClass.getFullName());
// set dynamic insert to false
persistentClass.setDynamicInsert(false);
// set dynamic update to false
persistentClass.setDynamicUpdate(false);
// set select before update to false
persistentClass.setSelectBeforeUpdate(false);
// add import to mappings
if ( mappings.isAutoImport() && persistentClass.getEntityName().indexOf( '.' ) > 0 ) {
mappings.addImport( persistentClass.getEntityName(), StringHelper.unqualify( persistentClass
.getEntityName() ) );
}
}
/**
* Binds a root class (one with no super classes) to the runtime meta model
* based on the supplied Grails domain class
*
* @param domainClass The Grails domain class
* @param mappings The Hibernate Mappings object
*/
public static void bindRoot(GrailsDomainClass domainClass, Mappings mappings) {
RootClass root = new RootClass();
bindClass(domainClass, root, mappings);
bindRootPersistentClassCommonValues(domainClass, root, mappings);
mappings.addClass(root);
}
/**
* Binds a persistent classes to the table representation and binds the class properties
*
* @param domainClass
* @param root
* @param mappings
*/
private static void bindRootPersistentClassCommonValues(GrailsDomainClass domainClass, RootClass root, Mappings mappings) {
// get the schema and catalog names from the configuration
String schema = mappings.getSchemaName();
String catalog = mappings.getCatalogName();
// create the table
Table table = mappings.addTable(
schema,
catalog,
domainClass.getTableName(),
null,
false
);
root.setTable(table);
LOG.info( "[GrailsDomainBinder] Mapping Grails domain class: " + domainClass.getFullName() + " -> " + root.getTable().getName() );
bindSimpleId( domainClass.getIdentifier(), root, mappings );
bindVersion( domainClass.getVersion(), root, mappings );
root.createPrimaryKey();
createClassProperties(domainClass,root,mappings);
}
/**
* Creates and binds the properties for the specified Grails domain class and PersistantClass
* and binds them to the Hibernate runtime meta model
*
* @param domainClass The Grails domain class
* @param persistentClass The Hibernate PersistentClass instance
* @param mappings The Hibernate Mappings instance
*/
protected static void createClassProperties(GrailsDomainClass domainClass, PersistentClass persistentClass, Mappings mappings) {
GrailsDomainClassProperty[] persistantProperties = domainClass.getPersistantProperties();
Table table = persistentClass.getTable();
for(int i = 0; i < persistantProperties.length;i++) {
GrailsDomainClassProperty currentGrailsProp = persistantProperties[i];
// TODO: Implement support for many from many relationships
if(currentGrailsProp.isManyToMany())
continue;
/* if(currentGrailsProp.isManyToOne() && currentGrailsProp.isBidirectional() ) {
GrailsDomainClassProperty otherSide = currentGrailsProp.getOtherSide();
if(otherSide.isOneToMany())
table = null;
}*/
if(LOG.isTraceEnabled())
LOG.trace("[GrailsDomainBinder] Binding persistent property [" + currentGrailsProp.getName() + "]");
Value value = null;
// see if its a collection type
CollectionType collectionType = CollectionType.collectionTypeForClass( currentGrailsProp.getType() );
if(collectionType != null) {
// create collection
Collection collection = collectionType.create(
currentGrailsProp,
persistentClass,
mappings
);
mappings.addCollection(collection);
value = collection;
}
// work out what type of relationship it is and bind value
else if ( currentGrailsProp.isManyToOne() ) {
value = new ManyToOne( table );
bindManyToOne( currentGrailsProp, (ManyToOne) value, mappings );
}
else if ( currentGrailsProp.isOneToOne()) {
//value = new OneToOne( table, persistentClass );
//bindOneToOne( currentGrailsProp, (OneToOne)value, mappings );
value = new ManyToOne( table );
bindManyToOne( currentGrailsProp, (ManyToOne) value, mappings );
}
else {
value = new SimpleValue( table );
bindSimpleValue( persistantProperties[i], (SimpleValue) value, mappings );
}
if(value != null) {
Property property = createProperty( value, persistentClass, persistantProperties[i], mappings );
persistentClass.addProperty( property );
}
}
}
/**
* Creates a persistant class property based on the GrailDomainClassProperty instance
*
* @param value
* @param persistentClass
* @param mappings
*/
private static Property createProperty(Value value, PersistentClass persistentClass, GrailsDomainClassProperty grailsProperty, Mappings mappings) {
// set type
value.setTypeUsingReflection( persistentClass.getClassName(), grailsProperty.getName() );
// if it is a ManyToOne or OneToOne relationship
if ( value instanceof ToOne ) {
ToOne toOne = (ToOne) value;
String propertyRef = toOne.getReferencedPropertyName();
if ( propertyRef != null ) {
// TODO: Hmm this method has package visibility. Why?
//mappings.addUniquePropertyReference( toOne.getReferencedEntityName(), propertyRef );
}
}
else if( value instanceof Collection ) {
//Collection collection = (Collection)value;
//String propertyRef = collection.getReferencedPropertyName();
}
if(value.getTable() != null)
value.createForeignKey();
Property prop = new Property();
prop.setValue( value );
bindProperty( grailsProperty, prop, mappings );
return prop;
}
/**
* @param property
* @param oneToOne
* @param mappings
*/
/* private static void bindOneToOne(GrailsDomainClassProperty property, OneToOne oneToOne, Mappings mappings) {
// bind value
bindSimpleValue(property, oneToOne, mappings );
// set foreign key type
oneToOne.setForeignKeyType( ForeignKeyDirection.FOREIGN_KEY_TO_PARENT );
oneToOne.setForeignKeyName( property.getFieldName() + FOREIGN_KEY_SUFFIX );
// TODO configure fetch settings
oneToOne.setFetchMode( FetchMode.DEFAULT );
// TODO configure lazy loading
oneToOne.setLazy(true);
oneToOne.setPropertyName( property.getName() );
oneToOne.setReferencedEntityName( property.getType().getName() );
}*/
/**
* @param currentGrailsProp
* @param one
* @param mappings
*/
private static void bindOneToMany(GrailsDomainClassProperty currentGrailsProp, OneToMany one, Mappings mappings) {
one.setReferencedEntityName( currentGrailsProp.getReferencedPropertyType().getName() );
}
/**
* Binds a many-to-one relationship to the
* @param property
* @param manyToOne
* @param mappings
*/
private static void bindManyToOne(GrailsDomainClassProperty property, ManyToOne manyToOne, Mappings mappings) {
// TODO configure fetching
manyToOne.setFetchMode(FetchMode.DEFAULT);
// TODO configure lazy loading
manyToOne.setLazy(true);
// bind column
bindSimpleValue(property,manyToOne,mappings);
// set referenced entity
manyToOne.setReferencedEntityName( property.getReferencedPropertyType().getName() );
manyToOne.setIgnoreNotFound(true);
}
/**
* @param version
* @param mappings
*/
private static void bindVersion(GrailsDomainClassProperty version, RootClass entity, Mappings mappings) {
SimpleValue val = new SimpleValue( entity.getTable() );
bindSimpleValue( version, val, mappings);
if ( !val.isTypeSpecified() ) {
val.setTypeName( "version".equals( version.getName() ) ? "integer" : "timestamp" );
}
Property prop = new Property();
prop.setValue( val );
bindProperty( version, prop, mappings );
val.setNullValue( "undefined" );
entity.setVersion( prop );
entity.addProperty( prop );
}
/**
* @param identifier
* @param entity
* @param mappings
*/
private static void bindSimpleId(GrailsDomainClassProperty identifier, RootClass entity, Mappings mappings) {
// create the id value
SimpleValue id = new SimpleValue(entity.getTable());
// set identifier on entity
entity.setIdentifier( id );
// configure generator strategy
id.setIdentifierGeneratorStrategy( "native" );
Properties params = new Properties();
if ( mappings.getSchemaName() != null ) {
params.setProperty( PersistentIdentifierGenerator.SCHEMA, mappings.getSchemaName() );
}
if ( mappings.getCatalogName() != null ) {
params.setProperty( PersistentIdentifierGenerator.CATALOG, mappings.getCatalogName() );
}
id.setIdentifierGeneratorProperties(params);
// bind value
bindSimpleValue(identifier, id, mappings );
// create property
Property prop = new Property();
prop.setValue(id);
// bind property
bindProperty( identifier, prop, mappings );
// set identifier property
entity.setIdentifierProperty( prop );
id.getTable().setIdentifierValue( id );
}
/**
* @param grailsProperty
* @param prop
* @param mappings
*/
private static void bindProperty(GrailsDomainClassProperty grailsProperty, Property prop, Mappings mappings) {
// set the property name
prop.setName( grailsProperty.getName() );
prop.setInsertable(true);
prop.setUpdateable(true);
prop.setPropertyAccessorName( mappings.getDefaultAccess() );
prop.setOptional( grailsProperty.isOptional() );
// set to cascade all for the moment
if(grailsProperty.isAssociation()) {
if(grailsProperty.isOneToMany()) {
prop.setCascade("all");
}
else if(grailsProperty.isManyToOne() || grailsProperty.isOneToOne()) {
GrailsDomainClass domainClass = grailsProperty.getDomainClass();
if(domainClass.isOwningClass(grailsProperty.getType())) {
prop.setCascade("save-update");
}
else {
GrailsDomainClassProperty otherSide = grailsProperty.getOtherSide();
if(otherSide != null && otherSide.isOneToMany()) {
prop.setCascade("save-update");
}
else {
prop.setCascade("all");
}
}
}
}
if(LOG.isTraceEnabled())
LOG.trace( "[GrailsDomainBinder] Set cascading strategy on property ["+grailsProperty.getName()+"] to ["+prop.getCascade()+"]" );
// lazy to true
prop.setLazy(true);
}
/**
w * Binds a simple value to the Hibernate metamodel. A simple value is
* any type within the Hibernate type system
*
* @param grailsProp The grails domain class property
* @param simpleValue The simple value to bind
* @param mappings The Hibernate mappings instance
*/
private static void bindSimpleValue(GrailsDomainClassProperty grailsProp, SimpleValue simpleValue,Mappings mappings) {
// set type
simpleValue.setTypeName(grailsProp.getType().getName());
Table table = simpleValue.getTable();
Column column = new Column();
if(grailsProp.isManyToOne())
column.setNullable(false);
column.setValue(simpleValue);
bindColumn(grailsProp, column);
if(table != null) table.addColumn(column);
simpleValue.addColumn(column);
}
/**
* Binds a Column instance to the Hibernate meta model
* @param grailsProp The Grails domain class property
* @param column The column to bind
*/
private static void bindColumn(GrailsDomainClassProperty grailsProp, Column column) {
if(grailsProp.isAssociation()) {
if(grailsProp.isOneToMany()) {
column.setName( grailsProp.getDomainClass().getTableName() + FOREIGN_KEY_SUFFIX );
}
else if(grailsProp.isManyToOne()) {
column.setName( grailsProp.getReferencedDomainClass().getTableName() + FOREIGN_KEY_SUFFIX );
}
else {
column.setName( grailsProp.getFieldName() + FOREIGN_KEY_SUFFIX );
}
column.setNullable(true);
} else {
column.setNullable(grailsProp.isOptional());
column.setName(grailsProp.getFieldName());
}
if(LOG.isTraceEnabled())
LOG.trace("[GrailsDomainBinder] bound property [" + grailsProp + "] to column name ["+column.getName()+"]");
}
}