Hi Matthew, <disclosure>I haven't tried the example you submitted. </disclosure>
1. If I had to guess I'd say that it's because the interface (BuildingMaterial) doesn't have the @OneToMany annotation, and as a result it's not a valid target for the relationship. Adding the annotatation (and switching to property access instead of field access) might help here. If you can't move to property access and add the annotation then I'm not sure how you could use an interface.. Using a MappedSuperclass for BuildingMaterial instead of an interface (could be an abstract MappedSuperclass though). 2. I'd have to look into @JoinColumn, I suspect it would result in similar behavior though (I'm prepared to be wrong here - haven't used JoinColumn much). Hope this helps -mike On Fri, Oct 23, 2009 at 12:54 PM, Matthew Adams <[email protected]>wrote: > Hi all, > > NB: All source for this example is attached and is pasted at the end > of this email, in case the attachment doesn't survive the mailing > list. I've also entered the following JIRA to track this issue: > https://issues.apache.org/jira/browse/OPENJPA-1361 > > Synopsis: bidirectional one-to-many relationship where the one side > references the objects on the many side using a concrete class > reference and the many side references the object on the one side > using an interface reference. When specifying the mappedBy property > of the @OneToMany annotation, OpenJPA throws the error: > > Collection field "example.model.Concrete.pebbles" declares that it is > mapped by "example.model.Pebble.buildingMaterial", but this is not a > valid inverse relation. > > When I remove the mappedBy property, everything works fine, except > that OpenJPA creates an extra join table (Concrete_Pebble) where it > shouldn't, IMHO. I have two questions, an answer to either one of > which will suffice: > > 1. Why can't I use mappedBy in this scenario? > 2. How do I annotate things with @JoinColumn or similar that will > allow me to not have a join table and only the reference from Pebble > back to its BuildingMaterial? > > Thanks, > Matthew > ==================== > package example.model; > > import java.util.HashSet; > import java.util.Set; > > import javax.persistence.CascadeType; > import javax.persistence.Column; > 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.Version; > > @Entity > public class Concrete implements BuildingMaterial { > > @Id > @GeneratedValue(strategy = GenerationType.AUTO) > @Column(name = "id") > private Long assignedId; > > @Version > @Column(name = "version") > private long assignedVersion; > > @OneToMany(cascade = { CascadeType.MERGE, CascadeType.REFRESH, > CascadeType.PERSIST }, fetch = FetchType.LAZY) > public Set<Pebble> pebbles = new HashSet<Pebble>(); > > public Long getId() { > return assignedId; > } > > public long getVersion() { > return assignedVersion; > } > > public Discriminator getDiscriminator() { > return Discriminator.CONCRETE; > } > > public void add(Pebble pebble) { > this.pebbles.add(pebble); > pebble.buildingMaterial = this; > } > > public Set<Pebble> getPebbles() { > return new HashSet<Pebble>(pebbles); > } > } > =========================== > package example.model; > > import javax.persistence.CascadeType; > import javax.persistence.Column; > import javax.persistence.Entity; > import javax.persistence.FetchType; > import javax.persistence.GeneratedValue; > import javax.persistence.GenerationType; > import javax.persistence.Id; > import javax.persistence.ManyToOne; > import javax.persistence.Version; > > import org.apache.openjpa.persistence.jdbc.Strategy; > > @Entity > public class Pebble implements PersistentlyIdentifiable { > > @Id > @GeneratedValue(strategy = GenerationType.AUTO) > @Column(name = "id") > private Long assignedId; > > @Version > @Column(name = "version") > private long assignedVersion; > > @ManyToOne(cascade = { CascadeType.MERGE, CascadeType.REFRESH, > CascadeType.PERSIST }, fetch = FetchType.LAZY) > @Strategy("example.openjpa.PersistentInterfaceValueHandler") > protected BuildingMaterial buildingMaterial; > > public Long getId() { > return assignedId; > } > > public long getVersion() { > return assignedVersion; > } > > public void setBuildingMaterial(BuildingMaterial buildingMaterial) { > buildingMaterial.add(this); > } > > public Discriminator getDiscriminator() { > return Discriminator.PEBBLE; > } > > public BuildingMaterial getBuildingMaterial() { > return buildingMaterial; > } > } > ============================ > package example.model; > > public interface PersistentlyIdentifiable { > Comparable<?> getId(); > > long getVersion(); > > Discriminator getDiscriminator(); > } > ============================ > package example.model; > > import java.util.Set; > > public interface BuildingMaterial extends PersistentlyIdentifiable { > > void add(Pebble pebble); > > Set<Pebble> getPebbles(); > } > ============================ > package example.model; > > import java.util.HashMap; > import java.util.Map; > > /** > * Enum used to store discriminators in the database for polymorphic > references. > */ > public enum Discriminator { > > CONCRETE(Concrete.class), PEBBLE(Pebble.class); > > private static class X { > public static final Map<String, Discriminator> > discriminatorsByClassName = new HashMap<String, Discriminator>(); > > public static void storeByClassName(Discriminator > discriminator) { > synchronized (discriminatorsByClassName) { > > discriminatorsByClassName.put(discriminator.getClassName(), > discriminator); > } > } > } > > private Discriminator(Class<?> clazz) { > this.className = clazz.getName(); > X.storeByClassName(this); > } > > private String className; > > public String getClassName() { > return className; > } > > public static Discriminator getDiscriminatorEnum(String className) { > return X.discriminatorsByClassName.get(className); > } > > public static String getDiscriminatorName(Class<?> clazz) { > Discriminator d = getDiscriminatorEnum(clazz.getName()); > if (d == null) { > throw new IllegalArgumentException("Class " + > clazz.getName() > + " does not have a corresponding > Discriminator"); > } > return d.name(); > } > > public static Class<?> getDiscriminatorClassFor(String name) > throws ClassNotFoundException { > > return getDiscriminatorClassFor(name, Discriminator.class > .getClassLoader()); > } > > public static Class<?> getDiscriminatorClassFor(String name, > ClassLoader loader) throws ClassNotFoundException { > > return > Class.forName(Discriminator.valueOf(name).getClassName(), true, > loader); > } > } > =========================== > package example.openjpa; > > import java.sql.SQLException; > > import org.apache.openjpa.jdbc.kernel.JDBCFetchConfiguration; > import org.apache.openjpa.jdbc.kernel.JDBCStore; > import org.apache.openjpa.jdbc.meta.ValueMapping; > import org.apache.openjpa.jdbc.meta.strats.AbstractValueHandler; > import org.apache.openjpa.jdbc.schema.Column; > import org.apache.openjpa.jdbc.schema.ColumnIO; > import org.apache.openjpa.kernel.OpenJPAStateManager; > import org.apache.openjpa.kernel.StoreContext; > import org.apache.openjpa.meta.JavaTypes; > import org.apache.openjpa.util.StoreException; > import org.slf4j.Logger; > import org.slf4j.LoggerFactory; > > import example.model.Discriminator; > import example.model.PersistentlyIdentifiable; > > @SuppressWarnings("serial") > public class PersistentInterfaceValueHandler extends AbstractValueHandler { > > Logger log = > LoggerFactory.getLogger(PersistentInterfaceValueHandler.class); > > @Override > public Column[] map(ValueMapping valueMapping, String fieldName, > ColumnIO columnIo, boolean adapt) { > Column discriminatorColumn = new Column(); > discriminatorColumn.setName(fieldName.toUpperCase() + > "_DISCRIMINATOR"); > discriminatorColumn.setJavaType(JavaTypes.STRING); > > Column idColumn = new Column(); > idColumn.setName(fieldName.toUpperCase() + "_ID"); > idColumn.setJavaType(JavaTypes.LONG); > > return new Column[] { discriminatorColumn, idColumn }; > } > > public boolean isVersionable(ValueMapping vm) { > return true; > } > > public boolean objectValueRequiresLoad(ValueMapping vm) { > return true; > } > > public Object toDataStoreValue(ValueMapping vm, Object val, > JDBCStore store) { > if (val == null) { > return new Object[] { null, null }; > } > return new Object[] { deriveDiscriminator(val.getClass()), > ((PersistentlyIdentifiable) val).getId() }; > } > > public String deriveDiscriminator(Class<?> clazz) { > if (log.isDebugEnabled()) { > log.debug("Getting discriminator for class: [" + > clazz.getName() > + "]"); > } > String discriminator = > Discriminator.getDiscriminatorName(clazz); > if (log.isDebugEnabled()) { > log.debug("Discriminator for class: [" + > clazz.getName() + "]: [" > + discriminator + "]"); > } > return discriminator; > } > > public Class<?> deriveClass(String name, ClassLoader loader) > throws ClassNotFoundException { > if (log.isDebugEnabled()) { > log.debug("Getting class for discriminator: [" + > name + "]"); > } > Class<?> clazz = > Discriminator.getDiscriminatorClassFor(name, loader); > if (log.isDebugEnabled()) { > log.debug("Class for discriminator: [" + name + "]: > [" > + clazz.getName() + "]"); > } > return clazz; > } > > public Object toObjectValue(ValueMapping vm, Object val, > OpenJPAStateManager sm, JDBCStore store, > JDBCFetchConfiguration fetch) throws SQLException { > > if (val == null) { > return null; > } > Object[] values = (Object[]) val; > StoreContext ctx = store.getContext(); > ClassLoader loader = store.getConfiguration() > > .getClassResolverInstance().getClassLoader(vm.getType(), > ctx.getClassLoader()); > Class<?> cls = null; > String clsName = (String) values[0]; > Long oidStr = (Long) values[1]; > if (clsName == null) { > return null; > } > try { > cls = deriveClass(clsName, loader); > } catch (ClassNotFoundException cnfe) { > throw new StoreException(cnfe); > } > Object oid = ctx.newObjectId(cls, oidStr); > return store.find(oid, vm, fetch); > } > } > ======================== > package example.test.integration; > > import static org.junit.Assert.assertEquals; > import static org.junit.Assert.assertNotNull; > > import java.util.Set; > > import org.junit.Test; > import org.springframework.transaction.annotation.Transactional; > > import example.model.BuildingMaterial; > import example.model.Concrete; > import example.model.Pebble; > > public class ConcretePebbleTest extends AbstractJpaTest { > > @Test > @Transactional > public void testConcrete() { > > BuildingMaterial bm = new Concrete(); > em.persist(bm); > > Pebble p = new Pebble(); > bm.add(p); > > em.flush(); > > Object bmid = bm.getId(); > Object pid = p.getId(); > > em.clear(); > > // navigate from BuildingMaterials side > bm = em.find(bm.getClass(), bmid); > Set<Pebble> pebbles = null; > assertNotNull(pebbles = bm.getPebbles()); > assertEquals(1, pebbles.size()); > > em.clear(); > > // navigate from Pebble side > p = em.find(Pebble.class, pid); > assertNotNull(bm = p.getBuildingMaterial()); > } > } > ======================= > package example.test.integration; > > import javax.annotation.PostConstruct; > import javax.persistence.EntityManager; > import javax.persistence.EntityManagerFactory; > import javax.persistence.PersistenceContext; > > import org.junit.runner.RunWith; > import org.springframework.beans.factory.annotation.Autowired; > import org.springframework.test.context.ContextConfiguration; > import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; > > @ContextConfiguration(locations={"classpath:applicationContext.xml"}) > @RunWith(SpringJUnit4ClassRunner.class) > abstract public class AbstractJpaTest { > > @Autowired > protected EntityManagerFactory emf; > > @PersistenceContext > protected EntityManager em; > > @PostConstruct > public void afterPropertiesSet() throws Exception { > emf.createEntityManager(); > } > } > ======================== > package example.test.unit; > > import static org.junit.Assert.assertEquals; > > import org.junit.Test; > > import example.model.Concrete; > import example.model.Discriminator; > > public class DiscriminatorTest { > > @Test > public void testDiscriminator() throws ClassNotFoundException { > Discriminator d = Discriminator.CONCRETE; > > assertEquals(Discriminator.getDiscriminatorName(Concrete.class), d > .name()); > > assertEquals(Discriminator.getDiscriminatorClassFor(d.name > ()), > Concrete.class); > } > } > =========================== > # log4j.properties > log4j.rootLogger=ERROR, A1 > log4j.appender.A1=org.apache.log4j.ConsoleAppender > log4j.appender.A1.layout=org.apache.log4j.PatternLayout > log4j.appender.A1.layout.ConversionPattern=%d{HH:mm:ss.SSS} [%t] %-5p > %c %x - %m%n > > log4j.logger.example=debug > =========================== > # test.properties > test.database=my-integration-test > =========================== > <!-- applicationContext.xml --> > <?xml version="1.0" > encoding="UTF-8"?> > > <beans xmlns="http://www.springframework.org/schema/beans" > xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" > xmlns:aop="http://www.springframework.org/schema/aop" > xmlns:tx="http://www.springframework.org/schema/tx" > xmlns:context="http://www.springframework.org/schema/context" > xsi:schemaLocation="http://www.springframework.org/schema/beans > http://www.springframework.org/schema/beans/spring-beans-2.5.xsd > http://www.springframework.org/schema/aop > http://www.springframework.org/schema/aop/spring-aop-2.5.xsd > http://www.springframework.org/schema/tx > http://www.springframework.org/schema/tx/spring-tx-2.5.xsd > > http://www.springframework.org/schema/context > http://www.springframework.org/schema/context/spring-context-2.5.xsd"> > > <bean > > > class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> > <property name="location" value="classpath:test.properties" > /> > </bean> > > <bean id="HsqldbDataSource" class="org.hsqldb.jdbc.jdbcDataSource"> > <property name="user" value="sa" /> > <property name="database" > value="jdbc:hsqldb:mem:${test.database}" /> > </bean> > > <bean id="entityManagerFactory" > > class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> > <property name="persistenceUnitName" value="case-sandbox" /> > <property name="jpaProperties"> > <value> > openjpa.AutoDetach=commit > openjpa.DetachState=fgs > openjpa.QueryCache=false > openjpa.jdbc.SubclassFetchMode=none > openjpa.jdbc.EagerFetchMode=none > > openjpa.jdbc.SynchronizeMappings=buildSchema(ForeignKeys=true) > </value> > </property> > <property name="jpaVendorAdapter"> > <bean > class="org.springframework.orm.jpa.vendor.OpenJpaVendorAdapter"> > <property name="showSql" > value="${test.showSql}" /> > <property name="generateDdl" value="false" > /> > </bean> > </property> > <property name="dataSource" ref="HsqldbDataSource" /> > </bean> > <bean > > > class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" > /> > > <bean id="transactionManager" > class="org.springframework.orm.jpa.JpaTransactionManager"> > <property name="entityManagerFactory" > ref="entityManagerFactory" /> > <property name="jpaDialect"> > <bean > class="org.springframework.orm.jpa.vendor.OpenJpaDialect" /> > </property> > <property name="dataSource" ref="HsqldbDataSource" /> > </bean> > <tx:annotation-driven transaction-manager="transactionManager" > proxy-target-class="true" /> > > </beans> > ============================= > <!-- orm.xml --> > <?xml version="1.0" encoding="UTF-8" ?> > <entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" > xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" > xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm > http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" > version="1.0"> > > <persistence-unit-metadata> > <persistence-unit-defaults> > <access>FIELD</access> > <cascade-persist/> > </persistence-unit-defaults> > </persistence-unit-metadata> > > </entity-mappings> > ================================ > <!-- persistence.xml --> > <?xml version="1.0" > encoding="UTF-8"?> > > <persistence xmlns="http://java.sun.com/xml/ns/persistence" > version="1.0"> > <persistence-unit name="case-sandbox"> > <provider> > org.apache.openjpa.persistence.PersistenceProviderImpl > </provider> > </persistence-unit> > </persistence> > =============================== > <!-- pom.xml --> > <?xml version="1.0" > encoding="UTF-8"?> > > <project xmlns="http://maven.apache.org/POM/4.0.0" > xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" > xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 > http://maven.apache.org/maven-v4_0_0.xsd"> > <modelVersion>4.0.0</modelVersion> > <groupId>org.mytest</groupId> > <artifactId>case-sandbox</artifactId> > <packaging>jar</packaging> > <version>1.0.0-SNAPSHOT</version> > <name>Sandbox</name> > <properties> > <surefire.debug.suspend>n</surefire.debug.suspend> > <test.showSql>true</test.showSql> > <spring.version>2.5.6</spring.version> > <slf4j.version>1.5.6</slf4j.version> > <jpa.spec.version>1.0</jpa.spec.version> > <openjpa.version>1.2.1</openjpa.version> > <hsql.version>1.8.0.1</hsql.version> > </properties> > <dependencies> > <dependency> > <groupId>org.slf4j</groupId> > <artifactId>slf4j-log4j12</artifactId> > <version>${slf4j.version}</version> > </dependency> > <dependency> > <groupId>org.slf4j</groupId> > <artifactId>slf4j-api</artifactId> > <version>${slf4j.version}</version> > </dependency> > <dependency> > <groupId>javax.persistence</groupId> > <artifactId>persistence-api</artifactId> > <version>${jpa.spec.version}</version> > </dependency> > <dependency> > <groupId>org.apache.openjpa</groupId> > <artifactId>openjpa</artifactId> > <version>${openjpa.version}</version> > </dependency> > <dependency> > <groupId>hsqldb</groupId> > <artifactId>hsqldb</artifactId> > <version>${hsql.version}</version> > </dependency> > <dependency> > <groupId>org.springframework</groupId> > <artifactId>spring</artifactId> > <version>${spring.version}</version> > </dependency> > <dependency> > <groupId>junit</groupId> > <artifactId>junit</artifactId> > <version>4.6</version> > </dependency> > <dependency> > <groupId>org.springframework</groupId> > <artifactId>spring-test</artifactId> > <version>${spring.version}</version> > <exclusions> > <exclusion> > <groupId>junit</groupId> > <artifactId>junit</artifactId> > </exclusion> > </exclusions> > </dependency> > <dependency> > <groupId>cglib</groupId> > <artifactId>cglib-nodep</artifactId> > <version>2.1_3</version> > </dependency> > <!-- > <dependency> > <groupId>org.jmock</groupId> > <artifactId>jmock-junit4</artifactId> > <scope>test</scope> > </dependency> > --> > </dependencies> > <build> > <testResources> > <testResource> > <filtering>true</filtering> > <directory>src/test/resources</directory> > <includes> > <include>**/*</include> > </includes> > </testResource> > </testResources> > <plugins> > <plugin> > <groupId>org.apache.maven.plugins</groupId> > <artifactId>maven-jar-plugin</artifactId> > <version>2.2</version> > <executions> > <execution> > <goals> > <goal>test-jar</goal> > </goals> > </execution> > </executions> > </plugin> > <plugin> > <groupId>org.apache.maven.plugins</groupId> > > <artifactId>maven-compiler-plugin</artifactId> > <configuration> > <source>1.6</source> > <target>1.6</target> > </configuration> > </plugin> > <plugin> > <groupId>org.apache.maven.plugins</groupId> > <artifactId>maven-antrun-plugin</artifactId> > <version>1.1</version> > <executions> > <execution> > <id>1</id> > > <phase>process-classes</phase> > <configuration> > <tasks> > <path > id="cp"> > > <path refid="maven.compile.classpath" /> > </path> > > <pathconvert > pathsep=" " property="arguments" > > dirsep="."> > <map > from="${basedir}/target/classes/" to="" /> > <map > from="${basedir}\target\classes\" to="" /> > > <mapper> > > <chainedmapper> > > <globmapper from="*.class" to="*" /> > > </chainedmapper> > > </mapper> > > <path id="model.path"> > > <fileset dir="${basedir}/target/classes/"> > > <include name="**/*.class" /> > > <exclude name="**/annotations/**/*.class" /> > > </fileset> > > </path> > > </pathconvert> > <java > classname="org.apache.openjpa.enhance.PCEnhancer" > > classpathref="cp" dir="target/classes" fork="true"> > <arg > line="${arguments} -p persistence.xml#case-sandbox" /> > </java> > </tasks> > </configuration> > <goals> > <goal>run</goal> > </goals> > </execution> > <execution> > <id>2</id> > <configuration> > <tasks> > <path > id="cp"> > > <path refid="maven.compile.classpath" /> > </path> > > <java classname="org.apache.openjpa.jdbc.meta.MappingTool" > > classpathref="cp" dir="target/classes" fork="true"> > > > <arg line="-p persistence.xml#case-sandbox" /> > > <arg line="-connectionDriverName org.hsqldb.jdbc.jdbcDataSource" /> > > <arg line="-connectionURL jdbc:hsqldb:mem:dummy" /> > > <arg line="-schemaAction build" /> > > <arg line="-sql ../test.sql" /> > > </java> > > </tasks> > </configuration> > <phase>compile</phase> > <goals> > <goal>run</goal> > </goals> > </execution> > > </executions> > </plugin> > <plugin> > <groupId>org.apache.maven.plugins</groupId> > > <artifactId>maven-surefire-plugin</artifactId> > <version>2.3</version> > <configuration> > > > <argLine>-Xrunjdwp:transport=dt_socket,address=9998,server=y,suspend=${surefire.debug.suspend} > -Xmx512m</argLine> > <includes> > > <include>**/*Test.java</include> > </includes> > <excludes> > > <exclude>**/integration/**</exclude> > </excludes> > </configuration> > <executions> > <execution> > <id>integration-tests</id> > > <phase>integration-test</phase> > <goals> > <goal>test</goal> > </goals> > <configuration> > <skip>false</skip> > <excludes> > > <exclude>**/unit/**</exclude> > </excludes> > <includes> > > <include>**/integration/**/*Test.java</include> > </includes> > </configuration> > </execution> > </executions> > </plugin> > </plugins> > </build> > </project> > ================================ >
