You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2017/08/11 10:46:34 UTC

cayenne git commit: CAY-2346 Field-based data object with Map-based storage fallback

Repository: cayenne
Updated Branches:
  refs/heads/master 7b762e9a3 -> 7f278e001


CAY-2346 Field-based data object with Map-based storage fallback


Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/7f278e00
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/7f278e00
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/7f278e00

Branch: refs/heads/master
Commit: 7f278e001c68a8588658e0071b418c07b304c66c
Parents: 7b762e9
Author: Nikita Timofeev <st...@gmail.com>
Authored: Fri Aug 11 13:13:26 2017 +0300
Committer: Nikita Timofeev <st...@gmail.com>
Committed: Fri Aug 11 13:13:26 2017 +0300

----------------------------------------------------------------------
 .../java/org/apache/cayenne/BaseDataObject.java |  22 +-
 .../org/apache/cayenne/CayenneDataObject.java   |  12 +-
 .../org/apache/cayenne/HybridDataObject.java    | 130 ++++++++++++
 .../org/apache/cayenne/HybridDataObjectIT.java  | 207 +++++++++++++++++++
 .../cayenne/testdo/hybrid/HybridEntity1.java    |   9 +
 .../cayenne/testdo/hybrid/HybridEntity2.java    |   9 +
 .../testdo/hybrid/auto/_HybridEntity1.java      | 129 ++++++++++++
 .../testdo/hybrid/auto/_HybridEntity2.java      | 123 +++++++++++
 .../cayenne/unit/di/server/CayenneProjects.java |   1 +
 .../cayenne/unit/di/server/SchemaBuilder.java   |   2 +-
 .../resources/cayenne-hybrid-data-object.xml    |   8 +
 .../test/resources/hybrid-data-object.map.xml   |  37 ++++
 12 files changed, 682 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/7f278e00/cayenne-server/src/main/java/org/apache/cayenne/BaseDataObject.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/BaseDataObject.java b/cayenne-server/src/main/java/org/apache/cayenne/BaseDataObject.java
index 17447c7..a12b1bb 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/BaseDataObject.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/BaseDataObject.java
@@ -43,14 +43,26 @@ import org.apache.cayenne.validation.ValidationFailure;
 import org.apache.cayenne.validation.ValidationResult;
 
 /**
- * Base implementation of DataObject,
+ * Base implementation of {@link DataObject},
  * have no assumption about how data is actually stored.
- *
- * Two variants are currently supported:
- * - field based storage, e.g. each entity class will directly define fields to store data
- * - Map based storage, e.g. values will be stored in general Map (see {@link CayenneDataObject})
+ * <p>
+ * Three variants are currently supported:
+ * <ul>
+ *  <li> field based storage, e.g. each entity class will directly define fields to store data
+ *  <li> {@link Map} based storage, e.g. values will be stored in general Map ({@link CayenneDataObject})
+ *  <li> mixed fields and generic Map to store runtime attributes ({@link HybridDataObject})
+ * </ul>
+ * <p>
+ * This class can be used directly as superclass for field-based data objects.
+ * <p>
+ * To create own implementation of {@link DataObject} with custom field storage logic it is enough
+ * to implement {@link #readPropertyDirectly(String)} and {@link #writePropertyDirectly(String, Object)} methods
+ * and serialization support if needed (helper methods {@link #writeState(ObjectOutputStream)}
+ * and {@link #readState(ObjectInputStream)} are provided).
  *
  * @see CayenneDataObject
+ * @see HybridDataObject
+ *
  * @since 4.1
  */
 public abstract class BaseDataObject extends PersistentObject implements DataObject, Validating {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/7f278e00/cayenne-server/src/main/java/org/apache/cayenne/CayenneDataObject.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/CayenneDataObject.java b/cayenne-server/src/main/java/org/apache/cayenne/CayenneDataObject.java
index 41e18ff..3cad849 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/CayenneDataObject.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/CayenneDataObject.java
@@ -30,8 +30,18 @@ import java.util.Iterator;
 import java.util.Map;
 
 /**
- * Implementation of DataObject that uses {@link Map} to store object fields.
+ * Implementation of {@link DataObject} that uses {@link Map} to store object fields.
+ * <p>
  * This implementation was pre 4.1 default.
+ * <p>
+ * Since <b>4.1</b> it is recommended to use {@link BaseDataObject} as superclass (and it is actually default now),
+ * as it has better performance and lower memory consumption (<b>much</b> lower for small objects).
+ * <p>
+ * You may need to use this class only if you have some generic attributes created at runtime (also
+ * consider {@link HybridDataObject} in this case) or if any compatibility issues arise.
+ *
+ * @see BaseDataObject
+ * @see HybridDataObject
  */
 public class CayenneDataObject extends BaseDataObject {
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/7f278e00/cayenne-server/src/main/java/org/apache/cayenne/HybridDataObject.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/HybridDataObject.java b/cayenne-server/src/main/java/org/apache/cayenne/HybridDataObject.java
new file mode 100644
index 0000000..a998c76
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/HybridDataObject.java
@@ -0,0 +1,130 @@
+/*****************************************************************
+ *   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.cayenne;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.cayenne.reflect.PropertyUtils;
+
+/**
+ *
+ * This data object like {@link CayenneDataObject} uses {@link Map} to store generic attributes,
+ * only difference is that this Map will be created lazily at first write, thus reducing memory penalty if possible.
+ * <p>
+ * This class can be used as superclass for objects that have attributes created at runtime.
+ * If generic runtime attributes will be used always it may be a good idea to use {@link CayenneDataObject} instead.
+ * If you don't create attributes at runtime it is better to use {@link BaseDataObject} class.
+ * <p>
+ * Map creation is not thread safe, as DataObject in general not thread safe by it's own.
+ *
+ * @see BaseDataObject
+ * @see CayenneDataObject
+ *
+ * @since 4.1
+ */
+public class HybridDataObject extends BaseDataObject {
+
+    private static final long serialVersionUID = 1945209973678806566L;
+
+    protected Map<String, Object> values;
+
+    @Override
+    Object readSimpleProperty(String property) {
+
+        // side effect - resolves HOLLOW object
+        Object object = readProperty(property);
+
+        // if a null value is returned, there is still a chance to
+        // find a non-persistent property via reflection
+        if (object == null && values != null && !values.containsKey(property)) {
+            object = PropertyUtils.getProperty(this, property);
+        }
+
+        return object;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(values == null) {
+            return null;
+        }
+        return values.get(propName);
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(values == null) {
+            values = new HashMap<>();
+        }
+        values.put(propName, val);
+    }
+
+    @Override
+    protected void appendProperties(StringBuffer buffer) {
+        buffer.append('[');
+        if(values == null) {
+            buffer.append(']');
+            return;
+        }
+
+        Iterator<Map.Entry<String, Object>> it = values.entrySet().iterator();
+        while (it.hasNext()) {
+            Map.Entry<String, Object> entry = it.next();
+
+            buffer.append(entry.getKey()).append("=>");
+            Object value = entry.getValue();
+
+            if (value instanceof Persistent) {
+                buffer.append('{').append(((Persistent) value).getObjectId()).append('}');
+            } else if (value instanceof Collection) {
+                buffer.append("(..)");
+            } else if (value instanceof Fault) {
+                buffer.append('?');
+            } else {
+                buffer.append(value);
+            }
+
+            if (it.hasNext()) {
+                buffer.append("; ");
+            }
+        }
+
+        buffer.append(']');
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        values = (Map<String, Object>) in.readObject();
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+        out.writeObject(values);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/7f278e00/cayenne-server/src/test/java/org/apache/cayenne/HybridDataObjectIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/HybridDataObjectIT.java b/cayenne-server/src/test/java/org/apache/cayenne/HybridDataObjectIT.java
new file mode 100644
index 0000000..e25751e
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/HybridDataObjectIT.java
@@ -0,0 +1,207 @@
+/*****************************************************************
+ *   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.cayenne;
+
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.testdo.hybrid.HybridEntity1;
+import org.apache.cayenne.testdo.hybrid.HybridEntity2;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.apache.cayenne.util.Util;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+/**
+ * @since 4.1
+ */
+@UseServerRuntime(CayenneProjects.HYBRID_DATA_OBJECT_PROJECT)
+public class HybridDataObjectIT extends ServerCase {
+
+    @Inject
+    private DataContext context;
+
+    @Inject
+    private ServerRuntime runtime;
+
+    @Test
+    public void testCreateNew() {
+        HybridEntity1 entity1 = context.newObject(HybridEntity1.class);
+        HybridEntity2 entity2 = context.newObject(HybridEntity2.class);
+        context.commitChanges();
+
+        assertNull(entity1.values);
+        assertNull(entity2.values);
+
+        HybridEntity1 selectEntity1 = ObjectSelect.query(HybridEntity1.class).selectOne(context);
+        assertEquals(0, selectEntity1.getIntField());
+        assertEquals(null, selectEntity1.getStrField());
+
+        HybridEntity2 selectEntity2 = ObjectSelect.query(HybridEntity2.class).selectOne(context);
+        assertEquals(0, selectEntity2.getIntField());
+        assertEquals(null, selectEntity2.getStrField());
+    }
+
+    @Test
+    public void testSetFieldAttributes() {
+        HybridEntity1 entity1 = context.newObject(HybridEntity1.class);
+        entity1.setIntField(123);
+        entity1.setStrField("abc");
+
+        HybridEntity2 entity2 = context.newObject(HybridEntity2.class);
+        entity2.setIntField(321);
+        entity2.setStrField("cba");
+        entity2.setHybridEntity1(entity1);
+
+        assertNull(entity1.values);
+        assertNull(entity2.values);
+
+        context.commitChanges();
+
+        HybridEntity1 selectEntity1 = ObjectSelect.query(HybridEntity1.class).selectOne(context);
+        assertEquals(123, selectEntity1.getIntField());
+        assertEquals("abc", selectEntity1.getStrField());
+
+        HybridEntity2 selectEntity2 = ObjectSelect.query(HybridEntity2.class).selectOne(context);
+        assertEquals(321, selectEntity2.getIntField());
+        assertEquals("cba", selectEntity2.getStrField());
+        assertEquals(selectEntity1, selectEntity2.getHybridEntity1());
+    }
+
+    @Test
+    public void testSetDynamicDbAttributes() {
+        // add attributes that in DbEntity but not mapped yet
+        addRuntimeAttribute(HybridEntity1.class, "FLOAT_FIELD", "double");
+        addRuntimeAttribute(HybridEntity2.class, "BOOLEAN_FIELD", "boolean");
+
+        try {
+            HybridEntity1 entity1 = context.newObject(HybridEntity1.class);
+            entity1.writeProperty("FLOAT_FIELD", 3.14);
+
+            HybridEntity2 entity2 = context.newObject(HybridEntity2.class);
+            entity2.writeProperty("BOOLEAN_FIELD", true);
+
+            assertNotNull(entity1.values);
+            assertNotNull(entity2.values);
+
+            context.commitChanges();
+
+            entity1.writeProperty("FLOAT_FIELD", 2.17);
+            entity2.writeProperty("BOOLEAN_FIELD", false);
+
+            // attributes should be merged with context cache
+            HybridEntity1 selectEntity1 = ObjectSelect.query(HybridEntity1.class).selectOne(context);
+            assertEquals(2.17, selectEntity1.readProperty("FLOAT_FIELD"));
+
+            HybridEntity2 selectEntity2 = ObjectSelect.query(HybridEntity2.class).selectOne(context);
+            assertEquals(false, selectEntity2.readProperty("BOOLEAN_FIELD"));
+
+            // attributes should be read from DB
+            ObjectContext cleanContext = runtime.newContext();
+            HybridEntity1 selectCleanEntity1 = ObjectSelect.query(HybridEntity1.class).selectOne(cleanContext);
+            assertEquals(3.14, selectCleanEntity1.readProperty("FLOAT_FIELD"));
+
+            HybridEntity2 selectCleanEntity2 = ObjectSelect.query(HybridEntity2.class).selectOne(cleanContext);
+            assertEquals(true, selectCleanEntity2.readProperty("BOOLEAN_FIELD"));
+        } finally {
+            removeRuntimeAttribute(HybridEntity1.class, "FLOAT_FIELD");
+            removeRuntimeAttribute(HybridEntity2.class, "BOOLEAN_FIELD");
+        }
+    }
+
+    @Test
+    public void testSetDynamicNonDbAttributes() {
+        // test write arbitrary data into object
+        HybridEntity1 entity1 = context.newObject(HybridEntity1.class);
+        entity1.writeProperty("CUSTOM_NON_DB_ATTRIBUTE", 42L);
+        assertEquals(42L, entity1.readProperty("CUSTOM_NON_DB_ATTRIBUTE"));
+        assertNotNull(entity1.values);
+
+        context.commitChanges();
+
+        entity1.writeProperty("CUSTOM_NON_DB_ATTRIBUTE", 12L);
+
+        HybridEntity1 selectEntity1 = ObjectSelect.query(HybridEntity1.class).selectOne(context);
+        // this will be restored from context cache
+        assertEquals(12L, selectEntity1.readProperty("CUSTOM_NON_DB_ATTRIBUTE"));
+
+        ObjectContext cleanContext = runtime.newContext();
+
+        HybridEntity1 selectCleanEntity1 = ObjectSelect.query(HybridEntity1.class).selectOne(cleanContext);
+        // this will be read from db only
+        assertEquals(null, selectCleanEntity1.readProperty("CUSTOM_NON_DB_ATTRIBUTE"));
+    }
+
+    @Test
+    public void testSerialization() throws Exception {
+
+        HybridEntity1 entity1 = new HybridEntity1();
+        entity1.setIntField(123);
+        entity1.setStrField("abc");
+        entity1.writeProperty("CUSTOM_PROPERTY", 3.14);
+
+        HybridEntity1 clonedEntity1 = Util.cloneViaSerialization(entity1);
+
+        assertEquals(123, clonedEntity1.getIntField());
+        assertEquals("abc", clonedEntity1.getStrField());
+        assertEquals(3.14, clonedEntity1.readProperty("CUSTOM_PROPERTY"));
+    }
+
+    @Test
+    public void testDirectPropertyWrite() throws Exception {
+        HybridEntity1 entity1 = new HybridEntity1();
+
+        HybridEntity2 entity2 = new HybridEntity2();
+        entity2.writePropertyDirectly("intField", 123);
+        entity2.writePropertyDirectly("strField", "abc");
+        assertNull(entity2.values);
+
+        entity2.writePropertyDirectly("CUSTOM_PROPERTY", 3.14);
+        entity2.writePropertyDirectly("hybridEntity1", entity1);
+        assertNotNull(entity2.values);
+
+        assertEquals(123, entity2.readPropertyDirectly("intField"));
+        assertEquals("abc", entity2.readPropertyDirectly("strField"));
+        assertEquals(3.14, entity2.readPropertyDirectly("CUSTOM_PROPERTY"));
+        assertEquals(entity1, entity2.readPropertyDirectly("hybridEntity1"));
+    }
+
+    private void addRuntimeAttribute(Class<?> entityClass, String attributeName, String attributeType) {
+        ObjEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(entityClass);
+        ObjAttribute attribute = new ObjAttribute();
+        attribute.setName(attributeName);
+        attribute.setDbAttributePath(attributeName);
+        attribute.setType(attributeType);
+        entity.addAttribute(attribute);
+    }
+
+    private void removeRuntimeAttribute(Class<?> entityClass, String attributeName) {
+        ObjEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(entityClass);
+        entity.removeAttribute(attributeName);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/7f278e00/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/HybridEntity1.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/HybridEntity1.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/HybridEntity1.java
new file mode 100644
index 0000000..e7767fe
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/HybridEntity1.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.hybrid;
+
+import org.apache.cayenne.testdo.hybrid.auto._HybridEntity1;
+
+public class HybridEntity1 extends _HybridEntity1 {
+
+    private static final long serialVersionUID = 1L; 
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/7f278e00/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/HybridEntity2.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/HybridEntity2.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/HybridEntity2.java
new file mode 100644
index 0000000..b2cf3f4
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/HybridEntity2.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.hybrid;
+
+import org.apache.cayenne.testdo.hybrid.auto._HybridEntity2;
+
+public class HybridEntity2 extends _HybridEntity2 {
+
+    private static final long serialVersionUID = 1L; 
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/7f278e00/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/auto/_HybridEntity1.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/auto/_HybridEntity1.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/auto/_HybridEntity1.java
new file mode 100644
index 0000000..82aa0e8
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/auto/_HybridEntity1.java
@@ -0,0 +1,129 @@
+package org.apache.cayenne.testdo.hybrid.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.List;
+
+import org.apache.cayenne.HybridDataObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.testdo.hybrid.HybridEntity2;
+
+/**
+ * Class _HybridEntity1 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _HybridEntity1 extends HybridDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final Property<Integer> INT_FIELD = Property.create("intField", Integer.class);
+    public static final Property<String> STR_FIELD = Property.create("strField", String.class);
+    public static final Property<List<HybridEntity2>> HYBRID_ENTITIES2 = Property.create("hybridEntities2", List.class);
+
+    protected int intField;
+    protected String strField;
+
+    protected Object hybridEntities2;
+
+    public void setIntField(int intField) {
+        beforePropertyWrite("intField", this.intField, intField);
+        this.intField = intField;
+    }
+
+    public int getIntField() {
+        beforePropertyRead("intField");
+        return this.intField;
+    }
+
+    public void setStrField(String strField) {
+        beforePropertyWrite("strField", this.strField, strField);
+        this.strField = strField;
+    }
+
+    public String getStrField() {
+        beforePropertyRead("strField");
+        return this.strField;
+    }
+
+    public void addToHybridEntities2(HybridEntity2 obj) {
+        addToManyTarget("hybridEntities2", obj, true);
+    }
+
+    public void removeFromHybridEntities2(HybridEntity2 obj) {
+        removeToManyTarget("hybridEntities2", obj, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<HybridEntity2> getHybridEntities2() {
+        return (List<HybridEntity2>)readProperty("hybridEntities2");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "intField":
+                return this.intField;
+            case "strField":
+                return this.strField;
+            case "hybridEntities2":
+                return this.hybridEntities2;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "intField":
+                this.intField = val == null ? 0 : (Integer)val;
+                break;
+            case "strField":
+                this.strField = (String)val;
+                break;
+            case "hybridEntities2":
+                this.hybridEntities2 = val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+        out.writeInt(this.intField);
+        out.writeObject(this.strField);
+        out.writeObject(this.hybridEntities2);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.intField = in.readInt();
+        this.strField = (String)in.readObject();
+        this.hybridEntities2 = in.readObject();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/7f278e00/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/auto/_HybridEntity2.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/auto/_HybridEntity2.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/auto/_HybridEntity2.java
new file mode 100644
index 0000000..c923af3
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/hybrid/auto/_HybridEntity2.java
@@ -0,0 +1,123 @@
+package org.apache.cayenne.testdo.hybrid.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.HybridDataObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.testdo.hybrid.HybridEntity1;
+
+/**
+ * Class _HybridEntity2 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _HybridEntity2 extends HybridDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final Property<Integer> INT_FIELD = Property.create("intField", Integer.class);
+    public static final Property<String> STR_FIELD = Property.create("strField", String.class);
+    public static final Property<HybridEntity1> HYBRID_ENTITY1 = Property.create("hybridEntity1", HybridEntity1.class);
+
+    protected int intField;
+    protected String strField;
+
+    protected Object hybridEntity1;
+
+    public void setIntField(int intField) {
+        beforePropertyWrite("intField", this.intField, intField);
+        this.intField = intField;
+    }
+
+    public int getIntField() {
+        beforePropertyRead("intField");
+        return this.intField;
+    }
+
+    public void setStrField(String strField) {
+        beforePropertyWrite("strField", this.strField, strField);
+        this.strField = strField;
+    }
+
+    public String getStrField() {
+        beforePropertyRead("strField");
+        return this.strField;
+    }
+
+    public void setHybridEntity1(HybridEntity1 hybridEntity1) {
+        setToOneTarget("hybridEntity1", hybridEntity1, true);
+    }
+
+    public HybridEntity1 getHybridEntity1() {
+        return (HybridEntity1)readProperty("hybridEntity1");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "intField":
+                return this.intField;
+            case "strField":
+                return this.strField;
+            case "hybridEntity1":
+                return this.hybridEntity1;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "intField":
+                this.intField = val == null ? 0 : (Integer)val;
+                break;
+            case "strField":
+                this.strField = (String)val;
+                break;
+            case "hybridEntity1":
+                this.hybridEntity1 = val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+        out.writeInt(this.intField);
+        out.writeObject(this.strField);
+        out.writeObject(this.hybridEntity1);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.intField = in.readInt();
+        this.strField = (String)in.readObject();
+        this.hybridEntity1 = in.readObject();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/7f278e00/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
index 0cd6372..10615ab 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
@@ -80,4 +80,5 @@ public class CayenneProjects {
     public static final String UUID_PROJECT = "cayenne-uuid.xml";
     public static final String CUSTOM_NAME_PROJECT = "custom-name-file.xml";
     public static final String WEIGHTED_SORT_PROJECT = "cayenne-weighted-sort.xml";
+    public static final String HYBRID_DATA_OBJECT_PROJECT = "cayenne-hybrid-data-object.xml";
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/7f278e00/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
index 5101bd5..f217052 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
@@ -82,7 +82,7 @@ public class SchemaBuilder {
 			"table-primitives.map.xml", "generic.map.xml", "map-db1.map.xml", "map-db2.map.xml", "embeddable.map.xml",
 			"qualified.map.xml", "quoted-identifiers.map.xml", "inheritance-single-table1.map.xml",
 			"inheritance-vertical.map.xml", "oneway-rels.map.xml", "unsupported-distinct-types.map.xml",
-			"array-type.map.xml", "cay-2032.map.xml", "weighted-sort.map.xml" };
+			"array-type.map.xml", "cay-2032.map.xml", "weighted-sort.map.xml", "hybrid-data-object.map.xml" };
 
 	// hardcoded dependent entities that should be excluded
 	// if LOBs are not supported

http://git-wip-us.apache.org/repos/asf/cayenne/blob/7f278e00/cayenne-server/src/test/resources/cayenne-hybrid-data-object.xml
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/resources/cayenne-hybrid-data-object.xml b/cayenne-server/src/test/resources/cayenne-hybrid-data-object.xml
new file mode 100644
index 0000000..4fc8784
--- /dev/null
+++ b/cayenne-server/src/test/resources/cayenne-hybrid-data-object.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain xmlns="http://cayenne.apache.org/schema/10/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/domain http://cayenne.apache.org/schema/10/domain.xsd"
+	 project-version="10">
+	<property name="cayenne.DataDomain.sharedCache" value="false"/>
+	<map name="hybrid-data-object"/>
+</domain>

http://git-wip-us.apache.org/repos/asf/cayenne/blob/7f278e00/cayenne-server/src/test/resources/hybrid-data-object.map.xml
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/resources/hybrid-data-object.map.xml b/cayenne-server/src/test/resources/hybrid-data-object.map.xml
new file mode 100644
index 0000000..441bf60
--- /dev/null
+++ b/cayenne-server/src/test/resources/hybrid-data-object.map.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-map xmlns="http://cayenne.apache.org/schema/10/modelMap"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap http://cayenne.apache.org/schema/10/modelMap.xsd"
+	 project-version="10">
+	<property name="defaultPackage" value="org.apache.cayenne.testdo.hybrid"/>
+	<property name="defaultSuperclass" value="org.apache.cayenne.HybridDataObject"/>
+	<db-entity name="HYBRID_ENTITY_1">
+		<db-attribute name="FLOAT_FIELD" type="DOUBLE"/>
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="INT_FIELD" type="INTEGER" isMandatory="true"/>
+		<db-attribute name="STR_FIELD" type="VARCHAR" length="255"/>
+	</db-entity>
+	<db-entity name="HYBRID_ENTITY_2">
+		<db-attribute name="BOOLEAN_FIELD" type="BOOLEAN"/>
+		<db-attribute name="HYBRID_ENTITY_1_ID" type="INTEGER"/>
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="INT_FIELD" type="INTEGER" isMandatory="true"/>
+		<db-attribute name="STR_FIELD" type="VARCHAR" length="255"/>
+	</db-entity>
+	<obj-entity name="HybridEntity1" className="org.apache.cayenne.testdo.hybrid.HybridEntity1" dbEntityName="HYBRID_ENTITY_1" superClassName="org.apache.cayenne.HybridDataObject">
+		<obj-attribute name="intField" type="int" db-attribute-path="INT_FIELD"/>
+		<obj-attribute name="strField" type="java.lang.String" db-attribute-path="STR_FIELD"/>
+	</obj-entity>
+	<obj-entity name="HybridEntity2" className="org.apache.cayenne.testdo.hybrid.HybridEntity2" dbEntityName="HYBRID_ENTITY_2" superClassName="org.apache.cayenne.HybridDataObject">
+		<obj-attribute name="intField" type="int" db-attribute-path="INT_FIELD"/>
+		<obj-attribute name="strField" type="java.lang.String" db-attribute-path="STR_FIELD"/>
+	</obj-entity>
+	<db-relationship name="hybridEntities2" source="HYBRID_ENTITY_1" target="HYBRID_ENTITY_2" toMany="true">
+		<db-attribute-pair source="ID" target="HYBRID_ENTITY_1_ID"/>
+	</db-relationship>
+	<db-relationship name="hybridEntity1" source="HYBRID_ENTITY_2" target="HYBRID_ENTITY_1">
+		<db-attribute-pair source="HYBRID_ENTITY_1_ID" target="ID"/>
+	</db-relationship>
+	<obj-relationship name="hybridEntities2" source="HybridEntity1" target="HybridEntity2" deleteRule="Deny" db-relationship-path="hybridEntities2"/>
+	<obj-relationship name="hybridEntity1" source="HybridEntity2" target="HybridEntity1" deleteRule="Nullify" db-relationship-path="hybridEntity1"/>
+</data-map>