| /* 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()+"]"); |
| } |
| |
| |
| } |