OPENJPA-2141 fix and test for lazy embedded fields

git-svn-id: https://svn.apache.org/repos/asf/openjpa/trunk@1293250 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Jeremy Bauer 2012-02-24 13:53:01 +00:00
parent fdda697583
commit d03434b557
11 changed files with 609 additions and 4 deletions

View File

@ -1087,10 +1087,6 @@ public class JDBCStoreManager implements StoreManager, JDBCStore {
if (processed != eres)
res.putEager(fms[i], processed);
} else {
boolean lazyEmbeddable = fms[i].getValueMapping().isEmbedded() &&
fms[i].getEmbeddedMetaData() != null &&
fetch.requiresFetch(fms[i]) == FetchConfiguration.FETCH_NONE;
if (!lazyEmbeddable)
fms[i].load(sm, this, fetch.traverseJDBC(fms[i]), res);
}
} finally {

View File

@ -43,6 +43,7 @@ import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.Embeddable;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.meta.JavaSQLTypes;
import org.apache.openjpa.jdbc.meta.Joinable;
import org.apache.openjpa.jdbc.meta.RelationId;
import org.apache.openjpa.jdbc.meta.ValueMappingInfo;
import org.apache.openjpa.jdbc.schema.Column;
@ -62,7 +63,9 @@ import org.apache.openjpa.kernel.StateManagerImpl;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FetchGroup;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.ValueMetaData;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.MetaDataException;
@ -406,6 +409,14 @@ public class EmbedFieldStrategy
sm.storeObject(field.getIndex(), null);
return;
}
// handling of lazy embeddables. if the embedded field is not part of any
// fetch group and the result does not contain any embeddable columns,
// do not load the embeddable.
if (fetch.requiresFetch(field) == FetchConfiguration.FETCH_NONE &&
!containsEmbeddedResult(fetch, res)) {
return;
}
//### note: without a null indicator column, the above indicatesNull()
//### call will always return false, meaning we always have to assume
@ -426,6 +437,52 @@ public class EmbedFieldStrategy
}
}
/*
* finds an eager fetch field and searches for it in the result.
* if the result does not contain it, assume that it contains no embeddable
* column data. this is a fairly safe assumption given that the entire
* embeddable was marked lazy.
*/
private boolean containsEmbeddedResult(FetchConfiguration fetch, Result res) {
FieldMapping[] fields = field.getEmbeddedMapping().getFieldMappings();
for (int i = 0; i < fields.length; i++) {
boolean load = (fetch.requiresFetch(fields[i]) == FetchConfiguration.FETCH_LOAD);
if (load) {
// check the first eager fetch field
return checkResult(fields[i],res);
}
}
// if all fields are lazy and in the default fetch group, populate the embeddable
// so its attributes can be loaded when accessed.
return fetch.hasFetchGroup(FetchGroup.NAME_DEFAULT);
}
private boolean checkResult(FieldMapping fm, Result res) {
if (fm.getStrategy() instanceof Joinable) {
Joinable strat = (Joinable)fm.getStrategy();
Column[] cols = strat.getColumns();
for (Column col : cols) {
try {
if (res.contains(col)) {
return true;
}
} catch (Exception e) {
return false;
}
}
}
// if the field is a collection, also check for an eager result which could result from
// a non-lazy relationship in the embeddable
int type = fm.getTypeCode();
if ((type == JavaTypes.ARRAY ||
type == JavaTypes.COLLECTION ||
type == JavaTypes.MAP)
&& res.getEager(fm) != null) {
return true;
}
return false;
}
private boolean loadFields(OpenJPAStateManager em, JDBCStore store,
JDBCFetchConfiguration fetch, Result res)
throws SQLException {

View File

@ -0,0 +1,51 @@
/*
* 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.embed.lazy;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.FetchType;
@Embeddable
public class BeverageHolder {
@Basic(fetch=FetchType.LAZY)
@Column(name="BH_DIAMETER")
private int diameter;
@Column(name="BH_DEPTH")
private int depth;
public void setDiameter(int diameter) {
this.diameter = diameter;
}
public int getDiameter() {
return diameter;
}
public void setDepth(int depth) {
this.depth = depth;
}
public int getDepth() {
return depth;
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.embed.lazy;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.FetchType;
@Embeddable
public class Guy {
@Basic(fetch=FetchType.LAZY)
@Column(name="GUY_HEIGHT")
private int height;
@Basic(fetch=FetchType.LAZY)
@Column(name="GUY_WEIGHT")
private int weight;
@Column(name="GUY_NAME")
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setHeight(int height) {
this.height = height;
}
public int getHeight() {
return height;
}
public void setWeight(int weight) {
this.weight = weight;
}
public int getWeight() {
return weight;
}
}

View File

@ -0,0 +1,80 @@
/*
* 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.embed.lazy;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.Table;
import org.apache.openjpa.persistence.Persistent;
@Entity
@Table(name="REC_TABLE")
public class Recliner {
@EmbeddedId
private ReclinerId id;
@Enumerated(EnumType.STRING)
@Column(name="REC_STYLE")
private Style style;
@Embedded // Lazy fetch set via xml mapping
private Guy guy;
@Persistent(fetch=FetchType.LAZY, embedded=true)
private BeverageHolder holder;
public void setId(ReclinerId id) {
this.id = id;
}
public ReclinerId getId() {
return id;
}
public void setStyle(Style style) {
this.style = style;
}
public Style getStyle() {
return style;
}
public void setGuy(Guy guy) {
this.guy = guy;
}
public Guy getGuy() {
return guy;
}
public void setHolder(BeverageHolder holder) {
this.holder = holder;
}
public BeverageHolder getHolder() {
return holder;
}
}

View File

@ -0,0 +1,62 @@
/*
* 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.embed.lazy;
import javax.persistence.Column;
import javax.persistence.Embeddable;
@Embeddable
public class ReclinerId {
private int id;
private String color;
public void setId(int id) {
this.id = id;
}
@Column(name="RECID_ID")
public int getId() {
return id;
}
public void setColor(String color) {
this.color = color;
}
@Column(name="RECID_COLOR")
public String getColor() {
return color;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ReclinerId) {
ReclinerId rid = (ReclinerId)obj;
return rid.id == id &&
rid.color.equals(color);
}
return false;
}
@Override
public int hashCode() {
return id ^ color.hashCode();
}
}

View File

@ -0,0 +1,25 @@
/*
* 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.embed.lazy;
public enum Style {
WESTERN,
RETRO,
MODERN
}

View File

@ -0,0 +1,197 @@
/*
* 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.embed.lazy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import javax.persistence.EntityManager;
import org.apache.openjpa.lib.jdbc.AbstractJDBCListener;
import org.apache.openjpa.lib.jdbc.JDBCEvent;
import org.apache.openjpa.lib.jdbc.JDBCListener;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI;
import org.apache.openjpa.persistence.OpenJPAPersistence;
import org.apache.openjpa.persistence.test.AbstractPersistenceTestCase;
public class TestLazyEmbeddable extends AbstractPersistenceTestCase {
protected List<String> _sql = new ArrayList<String>();
/*
* Verifies an entity with annotated (@Persistent) lazy embeddable and xml-tagged
* lazy embeddable (openjpa:persistent) with a mix of eager and lazy fields are lazily
* loaded (or not) as expected.
*/
public void testLazyEmbeddableFields() throws Exception {
_sql.clear();
HashMap<String, Object> props = new HashMap<String, Object>();
props.put("openjpa.jdbc.JDBCListeners",
new JDBCListener[] { new SQLListener() });
OpenJPAEntityManagerFactorySPI emf1 =
(OpenJPAEntityManagerFactorySPI)OpenJPAPersistence.
createEntityManagerFactory("LazyEmbedPU",
"org/apache/openjpa/persistence/embed/lazy/" +
"embed-lazy-persistence.xml", props);
try {
EntityManager em = emf1.createEntityManager();
Recliner rec = new Recliner();
ReclinerId recId = new ReclinerId();
recId.setColor("Camouflage");
recId.setId(new Random().nextInt());
rec.setId(recId);
rec.setStyle(Style.RETRO);
Guy guy = new Guy();
guy.setName("Tom");
guy.setHeight(76);
guy.setWeight(275);
rec.setGuy(guy);
BeverageHolder bh = new BeverageHolder();
bh.setDepth(2);
bh.setDiameter(3);
rec.setHolder(bh);
em.getTransaction().begin();
em.persist(rec);
em.getTransaction().commit();
em.clear();
_sql.clear();
Recliner r2 = em.find(Recliner.class, recId);
assertNotNull("Find returned null object", r2);
assertTrue(selectContains("REC_TABLE", _sql, "REC_STYLE", "RECID_ID", "RECID_COLOR"));
assertFalse(selectContains("REC_TABLE", _sql, "GUY_HEIGHT", "GUY_WEIGHT", "GUY_NAME",
"BH_DIAMETER", "BH_DEPTH"));
em.detach(r2);
// Lazy embeds should be null after detach.
assertNull("Embedded field guy is null before getter is called", r2.getGuy());
assertNull("Embedded field holder is null before getter is called", r2.getHolder());
// verify lazy embeds will load on access post-detach and merge
r2 = em.merge(r2);
verifyLazyLoading(r2);
em.clear();
_sql.clear();
// verify lazy embeds will load on access after find
r2 = em.find(Recliner.class, recId);
assertNotNull("Find returned null object", r2);
assertTrue(selectContains("REC_TABLE", _sql, "REC_STYLE", "RECID_ID", "RECID_COLOR"));
assertFalse(selectContains("REC_TABLE", _sql, "GUY_HEIGHT", "GUY_WEIGHT", "GUY_NAME",
"BH_DIAMETER", "BH_DEPTH"));
verifyLazyLoading(r2);
} finally {
cleanupEMF(emf1);
}
}
private void verifyLazyLoading(Recliner r2) {
_sql.clear();
Guy g = r2.getGuy();
assertNotNull("Guy is not null", g);
assertTrue(selectContains("REC_TABLE", _sql, "GUY_NAME"));
assertFalse(selectContains("REC_TABLE", _sql, "GUY_HEIGHT", "GUY_WEIGHT"));
_sql.clear();
g.getHeight();
assertTrue(selectContains("REC_TABLE", _sql, "GUY_HEIGHT"));
assertFalse(selectContains("REC_TABLE", _sql, "GUY_NAME", "GUY_WEIGHT", "BH_DIAMETER",
"BH_DEPTH"));
_sql.clear();
BeverageHolder holder = r2.getHolder();
assertNotNull("Holder is not null", holder);
assertTrue(selectContains("REC_TABLE", _sql, "BH_DEPTH"));
assertFalse(selectContains("REC_TABLE", _sql, "BH_DIAMETER"));
_sql.clear();
holder.getDiameter();
assertTrue(selectContains("REC_TABLE", _sql, "BH_DIAMETER"));
assertFalse(selectContains("REC_TABLE", _sql, "BH_DEPTH"));
}
private boolean selectContains(String table, List<String> sql, String...cols) {
boolean foundSelect = false;
for (String s: sql) {
String stmt = s.toUpperCase();
if (!stmt.startsWith("SELECT") && !stmt.contains(table)) {
continue;
}
foundSelect = true;
for (String col : cols) {
String ucol = col.toUpperCase();
if (!stmt.contains(ucol)) {
return false;
}
}
}
return foundSelect;
}
private static String toString(List<String> list) {
StringBuffer buf = new StringBuffer();
for (String s : list)
buf.append(s).append("\r\n");
return buf.toString();
}
/**
* Closes a specific entity manager factory and cleans up
* associated tables.
*/
private void cleanupEMF(OpenJPAEntityManagerFactorySPI emf1)
throws Exception {
if (emf1 == null)
return;
try {
clear(emf1);
} catch (Exception e) {
// if a test failed, swallow any exceptions that happen
// during tear-down, as these just mask the original problem.
if (testResult.wasSuccessful())
throw e;
} finally {
closeEMF(emf1);
}
}
public class SQLListener
extends AbstractJDBCListener {
@Override
public void beforeExecuteStatement(JDBCEvent event) {
if (event.getSQL() != null && _sql != null) {
_sql.add(event.getSQL());
}
}
}
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<entity-mappings xmlns="http://www.apache.org/openjpa/ns/orm/extendable"
xmlns:openjpa="http://www.apache.org/openjpa/ns/orm"
xmlns:orm="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="2.0">
<openjpa:openjpa-version version="1.0"/>
<entity class="org.apache.openjpa.persistence.embed.lazy.Recliner">
<attributes>
<openjpa:attributes>
<!-- embeddable should not get fetched along with entity -->
<openjpa:persistent name="guy" fetch="LAZY"/>
</openjpa:attributes>
</attributes>
</entity>
</entity-mappings>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="2.0">
<persistence-unit name="LazyEmbedPU">
<mapping-file>org/apache/openjpa/persistence/embed/lazy/embed-lazy-orm.xml</mapping-file>
<class>org.apache.openjpa.persistence.embed.lazy.ReclinerId</class>
<class>org.apache.openjpa.persistence.embed.lazy.Recliner</class>
<class>org.apache.openjpa.persistence.embed.lazy.Guy</class>
<class>org.apache.openjpa.persistence.embed.lazy.BeverageHolder</class>
<properties>
<property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema"/>
</properties>
</persistence-unit>
</persistence>

View File

@ -2542,6 +2542,11 @@ public class XMLPersistenceMetaDataParser
parseCommonExtendedAttributes(fmd, attrs);
parseTypeAttr(fmd, attrs);
// TODO - handle attributes
String val = attrs.getValue("fetch");
if (val != null) {
fmd.setInDefaultFetchGroup("EAGER".equals(val));
}
switch (fmd.getDeclaredTypeCode()) {
case JavaTypes.ARRAY:
if (fmd.getDeclaredType() == byte[].class