diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/MappingInfo.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/MappingInfo.java index a0937361d..ef884924a 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/MappingInfo.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/MappingInfo.java @@ -71,6 +71,7 @@ public abstract class MappingInfo private boolean _canIdx = true; private boolean _canUnq = true; private boolean _canFK = true; + private boolean _implicitRelation = false; private int _join = JOIN_NONE; private ColumnIO _io = null; @@ -130,6 +131,30 @@ public abstract class MappingInfo _canIdx = indexable; } + /** + * Affirms if this instance represents an implicit relation. For example, a + * relation expressed as the value of primary key of the related class and + * not as object reference. + * + * @since 1.3.0 + */ + public boolean isImplicitRelation() { + return _implicitRelation; + } + + /** + * Sets a marker to imply a logical relation that can not have any physical + * manifest in the database. For example, a relation expressed as the value + * of primary key of the related class and not as object reference. + * Populated from @ForeignKey(implicit=true) annotation. + * The mutator can only transit from false to true but not vice versa. + * + * @since 1.3.0 + */ + public void setImplicitRelation(boolean flag) { + _implicitRelation |= flag; + } + /** * Raw foreign key information. */ @@ -280,7 +305,7 @@ public abstract class MappingInfo else _canFK = info.canForeignKey(); } - + _implicitRelation = info.isImplicitRelation(); List cols = getColumns(); List icols = info.getColumns(); if (!icols.isEmpty() && (cols.isEmpty() @@ -386,10 +411,11 @@ public abstract class MappingInfo } /** - * Assert that the user did not try to place a foreign key on this mapping. + * Assert that the user did not try to place a foreign key on this mapping + * or placed an implicit foreign key. */ public void assertNoForeignKey(MetaDataContext context, boolean die) { - if (_fk == null) + if (_fk == null || isImplicitRelation()) return; Message msg = _loc.get("unexpected-fk", context); @@ -610,6 +636,7 @@ public abstract class MappingInfo String defStr = tmplate.getDefaultString(); boolean autoAssign = tmplate.isAutoAssigned(); boolean relationId = tmplate.isRelationId(); + boolean implicitRelation = tmplate.isImplicitRelation(); String targetField = tmplate.getTargetField(); if (given != null) { // use given type if provided, but warn if it isn't compatible with @@ -640,6 +667,8 @@ public abstract class MappingInfo autoAssign = true; if (given.isRelationId()) relationId = true; + if (given.isImplicitRelation()) + implicitRelation = true; } // default char column size if original type is char (test original @@ -684,6 +713,7 @@ public abstract class MappingInfo } col.setAutoAssigned(autoAssign); col.setRelationId(relationId); + col.setImplicitRelation(implicitRelation); col.setTargetField(targetField); // we need this for runtime, and the dynamic schema factory might diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/HandlerStrategies.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/HandlerStrategies.java index 12f04c375..6c69604a3 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/HandlerStrategies.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/HandlerStrategies.java @@ -62,6 +62,9 @@ public class HandlerStrategies { if (cols.length > 0 && cols[0].getTable() == null) { cols = vinfo.getColumns(vm, name, cols, vm.getFieldMapping().getTable(), adapt); + if (vinfo.isImplicitRelation()) + for (int i = 0; i < cols.length; i++) + cols[i].setImplicitRelation(true); ColumnIO mappedIO = vinfo.getColumnIO(); vm.setColumns(cols); vm.setColumnIO(mappedIO); @@ -138,7 +141,7 @@ public class HandlerStrategies { } /** - * Set a value into a row, taking care not to override column defualts + * Set a value into a row, taking care not to override column defaults * with nulls unless the user wants us to. */ private static void set(Row row, Column col, Object val, diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/PrimitiveFieldStrategy.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/PrimitiveFieldStrategy.java index f2e8ec6f1..f6ce8455f 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/PrimitiveFieldStrategy.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/PrimitiveFieldStrategy.java @@ -84,7 +84,9 @@ public class PrimitiveFieldStrategy new Column[]{ tmpCol }, field.getTable(), adapt); if (field.getValueStrategy() == ValueStrategies.AUTOASSIGN) cols[0].setAutoAssigned(true); - + if (vinfo.isImplicitRelation()) + for (int i = 0; i < cols.length; i++) + cols[i].setImplicitRelation(true); field.setColumns(cols); field.setColumnIO(vinfo.getColumnIO()); field.mapConstraints(field.getName(), adapt); diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/schema/Column.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/schema/Column.java index 45dedec18..c965518ed 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/schema/Column.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/schema/Column.java @@ -65,6 +65,7 @@ public class Column private Boolean _notNull = null; private boolean _autoAssign = false; private boolean _rel = false; + private boolean _implicitRelation = false; private String _target = null; private String _targetField = null; private int _flags = 0; @@ -712,6 +713,8 @@ public class Column setAutoAssigned(from.isAutoAssigned()); if (!isRelationId()) setRelationId(from.isRelationId()); + if (!isImplicitRelation()) + setImplicitRelation(from.isRelationId()); if (getTarget() == null) setTarget(from.getTarget()); if (getTargetField() == null) @@ -746,4 +749,28 @@ public class Column public void setComment(String comment) { _comment = comment; } + + /** + * Affirms if this instance represents an implicit relation. For example, a + * relation expressed as the value of primary key of the related class and + * not as object reference. + * + * @since 1.3.0 + */ + public boolean isImplicitRelation() { + return _implicitRelation; + } + + /** + * Sets a marker to imply a logical relation that can not have any physical + * manifest in the database. For example, a relation expressed as the value + * of primary key of the related class and not as object reference. + * Populated from @ForeignKey(implicit=true) annotation. + * The mutator can only transit from false to true but not vice versa. + * + * @since 1.3.0 + */ + public void setImplicitRelation(boolean flag) { + _implicitRelation |= flag; + } } diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/PrimaryRow.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/PrimaryRow.java index 3a0be1ba7..968ff5ad5 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/PrimaryRow.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/PrimaryRow.java @@ -331,20 +331,40 @@ public class PrimaryRow boolean overrideDefault) throws SQLException { // make sure we're not setting two different values + // unless the given column is an implicit relationship and value + // changes from logical default to non-default Object prev = getSet(col); if (prev != null) { if (prev == NULL) prev = null; if (!rowValueEquals(prev, val)) { - throw new InvalidStateException(_loc.get("diff-values", - new Object[]{ col.getFullName(), - (prev == null) ? null : prev.getClass(), prev, - (val == null) ? null : val.getClass(), val })). - setFatal(true); + if (allowsUpdate(col, prev, val)) { + super.setObject(col, val, metaType, overrideDefault); + } else if (!isDefaultValue(val)) { + throw new InvalidStateException(_loc.get("diff-values", + new Object[]{ col.getFullName(), + (prev == null) ? null : prev.getClass(), prev, + (val == null) ? null : val.getClass(), val })). + setFatal(true); + } } } super.setObject(col, val, metaType, overrideDefault); } + + /** + * Allow the given column value to be updated only if old or current value + * is a default value or was not set and the column is not a primary key. + */ + boolean allowsUpdate(Column col, Object old, Object cur) { + return !col.isPrimaryKey() && col.isImplicitRelation() + && (isDefaultValue(old)); + } + + boolean isDefaultValue(Object val) { + return val == null || val == NULL + || (val instanceof Number && ((Number)val).longValue() == 0); + } /** * Return true if the two values should be considered equal. diff --git a/openjpa-persistence-jdbc/src/main/java/org/apache/openjpa/persistence/jdbc/AnnotationPersistenceMappingParser.java b/openjpa-persistence-jdbc/src/main/java/org/apache/openjpa/persistence/jdbc/AnnotationPersistenceMappingParser.java index dd3030ecf..1d679c190 100644 --- a/openjpa-persistence-jdbc/src/main/java/org/apache/openjpa/persistence/jdbc/AnnotationPersistenceMappingParser.java +++ b/openjpa-persistence-jdbc/src/main/java/org/apache/openjpa/persistence/jdbc/AnnotationPersistenceMappingParser.java @@ -682,8 +682,13 @@ public class AnnotationPersistenceMappingParser * Parse the given foreign key. */ private void parseForeignKey(MappingInfo info, ForeignKey fk) { - parseForeignKey(info, fk.name(), fk.enabled(), fk.deferred(), - fk.deleteAction(), fk.updateAction()); + if (!fk.implicit()) { + parseForeignKey(info, fk.name(), fk.enabled(), fk.deferred(), + fk.deleteAction(), fk.updateAction()); + } else { + info.setImplicitRelation(true); + assertDefault(fk); + } } /** @@ -706,6 +711,20 @@ public class AnnotationPersistenceMappingParser fk.setUpdateAction(toForeignKeyAction(updateAction)); info.setForeignKey(fk); } + + void assertDefault(ForeignKey fk) { + boolean isDefault = StringUtils.isEmpty(fk.name()) + && fk.enabled() + && !fk.deferred() + && fk.deleteAction() == ForeignKeyAction.RESTRICT + && fk.updateAction() == ForeignKeyAction.RESTRICT + && fk.columnNames().length == 0 + && fk.specified(); + if (!isDefault) + throw new UserException(_loc.get("implicit-non-default-fk", _cls, + getSourceFile()).getMessage()); + } + /** * Convert our FK action enum to an internal OpenJPA action. diff --git a/openjpa-persistence-jdbc/src/main/java/org/apache/openjpa/persistence/jdbc/ForeignKey.java b/openjpa-persistence-jdbc/src/main/java/org/apache/openjpa/persistence/jdbc/ForeignKey.java index 5cc2d674c..1a76aa6ad 100644 --- a/openjpa-persistence-jdbc/src/main/java/org/apache/openjpa/persistence/jdbc/ForeignKey.java +++ b/openjpa-persistence-jdbc/src/main/java/org/apache/openjpa/persistence/jdbc/ForeignKey.java @@ -47,4 +47,6 @@ public @interface ForeignKey { String[] columnNames() default {}; boolean specified() default true; + + boolean implicit() default false; } diff --git a/openjpa-persistence-jdbc/src/main/resources/org/apache/openjpa/persistence/jdbc/localizer.properties b/openjpa-persistence-jdbc/src/main/resources/org/apache/openjpa/persistence/jdbc/localizer.properties index 468a0a627..584b814b9 100644 --- a/openjpa-persistence-jdbc/src/main/resources/org/apache/openjpa/persistence/jdbc/localizer.properties +++ b/openjpa-persistence-jdbc/src/main/resources/org/apache/openjpa/persistence/jdbc/localizer.properties @@ -56,4 +56,7 @@ unique-many-on-seq-unsupported: More than one unique constraints is specified \ discriminator-on-abstract-class: A discriminator value has been specified for \ the abstract class "{0}". The discriminator will never be used and may be \ safely removed. - \ No newline at end of file +implicit-non-default-fk: While parsing "{0}" from "{1}", found a @ForeignKey \ + with implicit attribute set to true but one or more other attributes of \ + ForeignKey is set to their non-default value. You can not specify any \ + non-default value for an implicit ForeignKey. \ No newline at end of file diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/Child.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/Child.java new file mode 100644 index 000000000..84dd787b4 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/Child.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.openjpa.persistence.jdbc.mapping.bidi; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.openjpa.meta.ValueStrategies; +import org.apache.openjpa.persistence.jdbc.ForeignKey; + +/** + * Child in a logically bidirectional but actually unidirectional parent-child + * relationship where Child holds reference to Parent via primary key and not + * via object reference. + * + * @author Pinaki Poddar + * + */ +@Entity +@Table(name="CHILD_693") +public class Child { + @Id + private long id; + + private String name; + + @Column(name="FK_PARENT_SEQ_ID", nullable=true) + @ForeignKey(implicit=true) + private long seqParentId; + + @Column(name="FK_PARENT_AUTO_ID", nullable=true) + @ForeignKey(implicit=true) + private long autoParentId; + + @Column(name="FK_PARENT_APP_ID", nullable=true) + @ForeignKey(implicit=true) + private long appParentId; + + public Child() { + + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getSeqParentId() { + return seqParentId; + } + + void setSeqParentId(long parentId) { + this.seqParentId = parentId; + } + + public long getAutoParentId() { + return autoParentId; + } + + void setAutoParentId(long parentId) { + this.autoParentId = parentId; + } + public long getAppParentId() { + return appParentId; + } + + void setAppParentId(long parentId) { + this.appParentId = parentId; + } + + public long getParentIdType(int idType) { + switch (idType) { + case ValueStrategies.NONE : return getAppParentId(); + case ValueStrategies.AUTOASSIGN : return getAutoParentId(); + case ValueStrategies.SEQUENCE : return getSeqParentId(); + default : + throw new IllegalArgumentException("No parent with id strategy " + + idType); + } + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/IParent.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/IParent.java new file mode 100644 index 000000000..77abe7db6 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/IParent.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.openjpa.persistence.jdbc.mapping.bidi; + +import java.util.Collection; + +public interface IParent { + public long getId(); + public void setId(long id); + public String getName(); + public void setName(String name); + public Collection getChildren(); + public void addChild(Child child); +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/ParentWithAppIdentity.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/ParentWithAppIdentity.java new file mode 100644 index 000000000..5bb0afd87 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/ParentWithAppIdentity.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.openjpa.persistence.jdbc.mapping.bidi; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.PostPersist; +import javax.persistence.PostUpdate; +import javax.persistence.PreUpdate; +import javax.persistence.Table; +import javax.persistence.Transient; + +import org.apache.openjpa.persistence.jdbc.ElementJoinColumn; + +/** + * Parent in a logically bidirectional but actually unidirectional parent-child + * relationship where Child holds reference to Parent via primary key and not + * via object reference. + * Parent identity is assigned by the application. Hence, Parent sets the + * children's reference to Parent whenever its identity is set by the + * application or a new child is added. + * + * @author Pinaki Poddar + * + */ +@Entity +public class ParentWithAppIdentity implements IParent { + @Id + private long id; + + private String name; + + /** + * This field is not mapped by the child. The child's table will + * hold an implicit foreign key linking to the primary key of this + * Parent's table. + */ + @OneToMany(cascade = CascadeType.ALL, fetch=FetchType.LAZY) + @ElementJoinColumn(name="FK_PARENT_APP_ID", referencedAttributeName="id") + private Set children; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + postIdSet(); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Collection getChildren() { + return children; + } + + public void addChild(Child child) { + if (children == null) + children = new HashSet(); + children.add(child); + child.setAppParentId(this.id); + } + + public boolean removeChild(Child child) { + return children != null && children.remove(child); + } + + /** + * This method will be called when application has assigned identity + * to this instance. + */ + public void postIdSet() { + if (children == null) + return; + for (Child child : children) { + child.setAppParentId(this.getId()); + } + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/ParentWithAutoIdentity.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/ParentWithAutoIdentity.java new file mode 100644 index 000000000..61112a0a4 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/ParentWithAutoIdentity.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.openjpa.persistence.jdbc.mapping.bidi; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.PostPersist; +import javax.persistence.PostUpdate; +import javax.persistence.PreUpdate; +import javax.persistence.Table; +import javax.persistence.Transient; + +import org.apache.openjpa.persistence.jdbc.ElementJoinColumn; + +/** + * Parent in a logically bidirectional but actually unidirectional parent-child + * relationship where Child holds reference to Parent via primary key and not + * via object reference. + * Also identity for Parent is generated by the persistence provider. Hence, + * Parent sets the children's reference to Parent in PostPersist callback. + * + * @author Pinaki Poddar + * + */ +@Entity +public class ParentWithAutoIdentity implements IParent { + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + private long id; + + private String name; + + /** + * This field is not mapped by the child. The child's table will + * hold an implicit foreign key linking to the primary key of this + * Parent's table. + */ + @OneToMany(cascade = CascadeType.ALL, fetch=FetchType.LAZY) + @ElementJoinColumn(name="FK_PARENT_AUTO_ID", referencedAttributeName="id") + private Set children; + + public long getId() { + return id; + } + + public void setId(long id) { + throw new RuntimeException(getClass() + ".setId() is not to be " + + "invoked directly. This class is using AUTO Generation Starategy"); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Collection getChildren() { + return children; + } + + public void addChild(Child child) { + if (children == null) + children = new HashSet(); + children.add(child); + } + + public boolean removeChild(Child child) { + return children != null && children.remove(child); + } + + /** + * This method will be called back after database has assigned identity + * to this instance. + */ + @PreUpdate + @PostPersist + public void postPersist() { + if (children == null) + return; + for (Child child : children) { + child.setAutoParentId(this.getId()); + } + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/ParentWithSequenceIdentity.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/ParentWithSequenceIdentity.java new file mode 100644 index 000000000..57051568f --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/ParentWithSequenceIdentity.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.openjpa.persistence.jdbc.mapping.bidi; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.PostPersist; +import javax.persistence.PostUpdate; +import javax.persistence.PreUpdate; +import javax.persistence.Table; +import javax.persistence.Transient; + +import org.apache.openjpa.persistence.jdbc.ElementJoinColumn; + +/** + * Parent in a logically bidirectional but actually unidirectional parent-child + * relationship where Child holds reference to Parent via primary key and not + * via object reference. + * Also database assigns identity for Parent. Hence, Parent sets the children's + * reference to Parent in PostPersist callback. + * + * @author Pinaki Poddar + * + */ +@Entity +public class ParentWithSequenceIdentity implements IParent { + @Id + @GeneratedValue(strategy=GenerationType.SEQUENCE) + private long id; + + private String name; + + /** + * This field is not mapped by the child. The child's table will + * hold an implicit foreign key linking to the primary key of this + * Parent's table. + */ + @OneToMany(cascade = CascadeType.ALL, fetch=FetchType.LAZY) + @ElementJoinColumn(name="FK_PARENT_SEQ_ID", referencedAttributeName="id") + private Set children; + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public void setId(long id) { + throw new RuntimeException(getClass() + ".setId() is not to be " + + "invoked directly. This class is using SEQ Generation Starategy"); + } + + public void setName(String name) { + this.name = name; + } + + public Collection getChildren() { + return children; + } + + public void addChild(Child child) { + if (children == null) + children = new HashSet(); + children.add(child); + } + + public boolean removeChild(Child child) { + return children != null && children.remove(child); + } + + /** + * This method will be called back after database has assigned identity + * to this instance. + */ + @PreUpdate + @PostPersist + public void postPersist() { + if (children == null) + return; + for (Child child : children) { + child.setSeqParentId(this.getId()); + } + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/TestOneSidedParentChildWithImplicitForeignKey.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/TestOneSidedParentChildWithImplicitForeignKey.java new file mode 100644 index 000000000..b695de400 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/mapping/bidi/TestOneSidedParentChildWithImplicitForeignKey.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.openjpa.persistence.jdbc.mapping.bidi; + +import javax.persistence.EntityManager; + +import org.apache.openjpa.meta.ClassMetaData; +import org.apache.openjpa.meta.FieldMetaData; +import org.apache.openjpa.meta.MetaDataRepository; +import org.apache.openjpa.meta.ValueStrategies; +import org.apache.openjpa.persistence.test.SingleEMFTestCase; + +/** + * Tests behavior of Parent-Child mapping under following conditions a) Parent + * has many references to Child b) Child refers to Parent by Parent's identity + * and not by object reference c) Parent's identity is assigned by the database + * d) PostPersist callback in Parent sets the children's reference to Parent + * + * The use case was originally reported in + * OpenJPA User Forum + * + * @author Pinaki Poddar + */ +public class TestOneSidedParentChildWithImplicitForeignKey extends + SingleEMFTestCase { + private EntityManager em; + private static Class[] PARENT_ID_TYPES = { + ParentWithAppIdentity.class, // ValueStrategies.NONE = 0 + ParentWithSequenceIdentity.class, // ValueStrategies.SEQUENCE = 2 + ParentWithAutoIdentity.class, // ValueStrategies.AUTOASSIGN = 3 + }; + private static int[] VALUE_STRATEGIES = { + ValueStrategies.NONE, + ValueStrategies.SEQUENCE, + ValueStrategies.AUTOASSIGN }; + + private static long[] PARENT_IDS = new long[PARENT_ID_TYPES.length]; + + private static long PARENT_ID_COUNTER = System.currentTimeMillis(); + private static long CHILD_ID_COUNTER = System.currentTimeMillis(); + private static int CHILD_COUNT = 3; + + public void setUp() { + setUp(DROP_TABLES, ParentWithAppIdentity.class, + ParentWithSequenceIdentity.class, ParentWithAutoIdentity.class, + Child.class); + em = emf.createEntityManager(); + createData(CHILD_COUNT); + } + + public void xtestStrategies() { + MetaDataRepository repos = emf.getConfiguration() + .getMetaDataRepositoryInstance(); + for (int i = 0; i < VALUE_STRATEGIES.length; i++) { + ClassMetaData meta = repos.getMetaData(PARENT_ID_TYPES[i], null, + true); + FieldMetaData fmd = meta.getPrimaryKeyFields()[0]; + assertEquals(fmd + " strategy is " + fmd.getValueStrategy(), + VALUE_STRATEGIES[i], fmd.getValueStrategy()); + } + } + + void createData(int nChild) { + em.getTransaction().begin(); + + Child[] children = new Child[CHILD_COUNT]; + for (int j = 0; j < CHILD_COUNT; j++) { + Child child = new Child(); + child.setId(CHILD_ID_COUNTER++); + child.setName("Child" + j); + children[j] = child; + } + + for (int i = 0; i < PARENT_ID_TYPES.length; i++) { + IParent parent = newParent(i); + if (VALUE_STRATEGIES[i] == ValueStrategies.NONE) + parent.setId(++PARENT_ID_COUNTER); + for (int j = 0; j < CHILD_COUNT; j++) { + parent.addChild(children[j]); + } + em.persist(parent); + em.flush(); + PARENT_IDS[i] = parent.getId(); + } + + em.getTransaction().commit(); + } + + public void testPersist() { + em.getTransaction().begin(); + + for (int i = 0; i < PARENT_ID_TYPES.length; i++) { + IParent parent = findParent(i); + assertFalse(parent.getId() == 0); + assertFalse(parent.getChildren().isEmpty()); + assertEquals(CHILD_COUNT, parent.getChildren().size()); + for (Child child : parent.getChildren()) { + assertFalse(child.getParentIdType(VALUE_STRATEGIES[i]) == 0); + assertTrue(child.getParentIdType(VALUE_STRATEGIES[i]) == parent + .getId()); + } + } + em.getTransaction().commit(); + } + + public void testUpdate() { + em.getTransaction().begin(); + + Child newChild = new Child(); + newChild.setId(CHILD_ID_COUNTER++); + newChild.setName("New Child"); + for (int i = 0; i < PARENT_ID_TYPES.length; i++) { + IParent parent = findParent(i); + parent.addChild(newChild); + em.merge(parent); + } + em.flush(); + em.getTransaction().commit(); + em.clear(); + + em.getTransaction().begin(); + for (int i = 0; i < PARENT_ID_TYPES.length; i++) { + IParent parent = findParent(i); + assertFalse(parent.getId() == 0); + assertFalse(parent.getChildren().isEmpty()); + assertEquals(CHILD_COUNT + 1, parent.getChildren().size()); + for (Child child : parent.getChildren()) { + assertFalse(child.getParentIdType(VALUE_STRATEGIES[i]) == 0); + assertTrue(child.getParentIdType(VALUE_STRATEGIES[i]) == parent + .getId()); + } + } + em.getTransaction().commit(); + } + + public void tearDown() { + + } + + public IParent newParent(int parentType) { + try { + IParent parent = (IParent)PARENT_ID_TYPES[parentType].newInstance(); + if (VALUE_STRATEGIES[parentType] == ValueStrategies.NONE) + parent.setId(++PARENT_ID_COUNTER); + parent.setName(PARENT_ID_TYPES[parentType].getSimpleName()); + return parent; + } catch (Exception e) { + fail(e.toString()); + } + return null; + } + + public IParent findParent(int parentType) { + return (IParent) em.find(PARENT_ID_TYPES[parentType], + PARENT_IDS[parentType]); + } +} \ No newline at end of file diff --git a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/AnnotationPersistenceMetaDataParser.java b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/AnnotationPersistenceMetaDataParser.java index b310a3c74..2074400ea 100644 --- a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/AnnotationPersistenceMetaDataParser.java +++ b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/AnnotationPersistenceMetaDataParser.java @@ -188,7 +188,7 @@ public class AnnotationPersistenceMetaDataParser private final Map _pkgs = new HashMap(); // the class we were invoked to parse - private Class _cls = null; + protected Class _cls = null; private File _file = null; /**