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 07:54:47 UTC

[10/18] cayenne git commit: CAY-2330 Field based data objects

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/cayenne-cgen/src/main/resources/templates/v4_1/superclass.vm
----------------------------------------------------------------------
diff --git a/cayenne-cgen/src/main/resources/templates/v4_1/superclass.vm b/cayenne-cgen/src/main/resources/templates/v4_1/superclass.vm
new file mode 100644
index 0000000..860e4af
--- /dev/null
+++ b/cayenne-cgen/src/main/resources/templates/v4_1/superclass.vm
@@ -0,0 +1,292 @@
+##   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.
+##
+##Terminology:
+##	Base class - super superclass of entity, ie, org.apache.cayenne.CayenneDataObject or MyBaseClass
+##  Super class - superclass of entity, ie,  org.apache.cayenne.art.auto._Artist
+##	Sub class - class of entity, ie, org.apache.cayenne.art.Artist
+##
+##  Classes available in template
+##    object (duplicated as 'objEntity') - the ObjEntity class: See org.apache.cayenne.map.ObjectEntity
+##    stringUtils - class for string "helper" functions: See org.apache.cayenne.gen.StringUtils
+##    entityUtils - class for entity "helper" functions: See org.apache.cayenne.gen.EntityUtils
+##    importUtils - class for import statement management: See org.apache.cayenne.gen.ImportUtils
+##    superClassName
+##    superPackageName
+##    subClassName
+##    subPackageName
+##    baseClassName
+##    basePackageName
+##
+###################
+## Create import ##
+###################
+${importUtils.setPackage($superPackageName)}##
+${importUtils.addReservedType("${superPackageName}.${superClassName}")}##
+${importUtils.addType("${basePackageName}.${baseClassName}")}##
+${importUtils.addType("java.io.IOException")}##
+${importUtils.addType("java.io.ObjectInputStream")}##
+${importUtils.addType("java.io.ObjectOutputStream")}##
+#if((${object.DeclaredAttributes} && !${object.DeclaredAttributes.isEmpty()}) || (${object.DeclaredRelationships} && !${object.DeclaredRelationships.isEmpty()}))
+${importUtils.addType('org.apache.cayenne.exp.Property')}##
+#end
+#foreach( $attr in ${object.DeclaredAttributes} )
+$importUtils.addType(${attr.Type})##
+#end
+#foreach( $rel in ${object.DeclaredRelationships} )
+$importUtils.addType(${rel.TargetEntity.ClassName})##
+#if(${rel.CollectionType}) 
+$importUtils.addType(${rel.CollectionType})##
+#end
+#end
+${importUtils.generate()}
+
+/**
+ * Class ${superClassName} 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 ${superClassName} extends ${baseClassName} {
+
+    private static final long serialVersionUID = 1L; 
+
+###########################
+## Create property names ##
+###########################
+#if( $createPropertyNames )
+#foreach( $attr in ${object.DeclaredAttributes} )
+    public static final String ${stringUtils.capitalizedAsConstant($attr.Name)}_PROPERTY = "${attr.Name}";
+#end
+#foreach( $rel in ${object.DeclaredRelationships} )
+    public static final String ${stringUtils.capitalizedAsConstant($rel.Name)}_PROPERTY = "${rel.Name}";
+#end
+
+#end
+#if( $object.DbEntity )
+#foreach( $idAttr in ${object.DbEntity.PrimaryKeys} )
+    public static final String ${stringUtils.capitalizedAsConstant($idAttr.Name)}_PK_COLUMN = "${idAttr.Name}";
+#end
+#end
+
+#######################
+## Create Properties ##
+#######################
+#foreach( $attr in ${object.DeclaredAttributes} )
+    #set ( $type = "$importUtils.formatJavaType(${attr.Type}, false)")
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($attr.Name)} = Property.create("${attr.Name}", ${stringUtils.stripGeneric($type)}.class);
+#end
+#foreach( $rel in ${object.DeclaredRelationships} )
+#if( $rel.ToMany )
+#if ( ${rel.CollectionType} == "java.util.Map")
+    #set( $type = "$importUtils.formatJavaType($rel.CollectionType)<$importUtils.formatJavaType($entityUtils.getMapKeyType($rel)), $importUtils.formatJavaType($rel.TargetEntity.ClassName)>" )
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = Property.create("${rel.Name}", ${stringUtils.stripGeneric($type)}.class);
+#else
+    #set( $type = "$importUtils.formatJavaType($rel.CollectionType)<$importUtils.formatJavaType($rel.TargetEntity.ClassName)>" )
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = Property.create("${rel.Name}", ${stringUtils.stripGeneric($type)}.class);
+#end
+#else
+    #set( $type = "$importUtils.formatJavaType(${rel.TargetEntity.ClassName})" )
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = Property.create("${rel.Name}", ${stringUtils.stripGeneric($type)}.class);
+#end
+#end
+
+###################
+## Create Fields ##
+###################
+#foreach( $attr in ${object.DeclaredAttributes} )
+#set ( $type = "$importUtils.formatJavaType(${attr.Type})")
+    protected $type ${attr.Name};
+#end
+
+#foreach( $rel in ${object.DeclaredRelationships} )
+    protected Object ${rel.Name};
+#end
+
+#########################################################
+## Create attributes and relationships set/get methods ##
+#########################################################
+#foreach( $attr in ${object.DeclaredAttributes} )
+#set ( $name = "$stringUtils.formatVariableName(${attr.Name})")
+##
+## setter
+#if ("true" != "${object.isReadOnly()}")
+    public void set${stringUtils.capitalized($attr.Name)}($importUtils.formatJavaType(${attr.Type}) $name) {
+        beforePropertyWrite("${attr.Name}", this.$name, $name);
+        this.$name = $name;
+    }
+
+#end
+##
+## getter
+#if ( $importUtils.isBoolean(${attr.Type}) )
+	public boolean is${stringUtils.capitalized($attr.Name)}() {
+#else
+    public $importUtils.formatJavaType(${attr.Type}) get${stringUtils.capitalized($attr.Name)}() {
+#end
+        beforePropertyRead("${attr.Name}");
+        return $stringUtils.formatVariableName(${attr.Name});
+    }
+
+#end
+##
+## Create list add/remove/get methods
+#foreach( $rel in ${object.DeclaredRelationships} )
+#if( $rel.ToMany )
+#if ( ! $rel.ReadOnly )
+    public void addTo${stringUtils.capitalized($rel.Name)}($importUtils.formatJavaType(${rel.TargetEntity.ClassName}) obj) {
+        addToManyTarget("${rel.Name}", obj, true);
+    }
+
+    public void removeFrom${stringUtils.capitalized($rel.Name)}($importUtils.formatJavaType(${rel.TargetEntity.ClassName}) obj) {
+        removeToManyTarget("${rel.Name}", obj, true);
+    }
+
+#end
+    @SuppressWarnings("unchecked")
+#if ( ${rel.CollectionType} == "java.util.Map")
+    public $importUtils.formatJavaType($rel.CollectionType)<$importUtils.formatJavaType($entityUtils.getMapKeyType($rel)), $importUtils.formatJavaType($rel.TargetEntity.ClassName)> get${stringUtils.capitalized($rel.Name)}() {
+        return ($importUtils.formatJavaType($rel.CollectionType)<$importUtils.formatJavaType($entityUtils.getMapKeyType($rel)), $importUtils.formatJavaType($rel.TargetEntity.ClassName)>)readProperty("${rel.Name}");
+    }
+#else
+    public $importUtils.formatJavaType($rel.CollectionType)<$importUtils.formatJavaType($rel.TargetEntity.ClassName)> get${stringUtils.capitalized($rel.Name)}() {
+        return ($importUtils.formatJavaType($rel.CollectionType)<$importUtils.formatJavaType($rel.TargetEntity.ClassName)>)readProperty("${rel.Name}");
+    }
+#end
+
+#else
+#if ( !${object.isReadOnly()} && !$rel.ReadOnly )
+    public void set${stringUtils.capitalized($rel.Name)}($importUtils.formatJavaType(${rel.TargetEntity.ClassName}) $stringUtils.formatVariableName(${rel.name})) {
+        setToOneTarget("${rel.Name}", $stringUtils.formatVariableName(${rel.name}), true);
+    }
+
+#end
+    public $importUtils.formatJavaType(${rel.TargetEntity.ClassName}) get${stringUtils.capitalized($rel.Name)}() {
+        return ($importUtils.formatJavaType(${rel.TargetEntity.ClassName}))readProperty("${rel.Name}");
+    }
+
+#end
+#end
+#############################
+## Create callback methods ##
+#############################
+#foreach($cbname in ${entityUtils.callbackNames})
+protected abstract void ${cbname}();
+
+#end
+###########################################################
+## Create writePropertyDirect/readPropertyDirect methods ##
+###########################################################
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+#foreach( $attr in ${object.DeclaredAttributes} )
+#set ( $name = "$stringUtils.formatVariableName(${attr.Name})")
+            case "${attr.Name}":
+                return this.${name};
+#end
+#foreach( $rel in ${object.DeclaredRelationships} )
+            case "${rel.Name}":
+                return this.$stringUtils.formatVariableName(${rel.name});
+#end
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+#foreach( $attr in ${object.DeclaredAttributes} )
+#set ( $type = "$importUtils.formatJavaType(${attr.Type}, false)")
+#set ( $realType = "$importUtils.formatJavaType(${attr.Type})")
+#set ( $name = "$stringUtils.formatVariableName(${attr.Name})")
+            case "${attr.Name}":
+#if ( $importUtils.isBoolean($realType) )
+                this.${name} = val == null ? false : ($type)val;
+#elseif ($importUtils.isPrimitive($realType))
+                this.${name} = val == null ? 0 : ($type)val;
+#else
+                this.${name} = ($type)val;
+#end
+                break;
+#end
+#foreach( $rel in ${object.DeclaredRelationships} )
+            case "${rel.Name}":
+                this.$stringUtils.formatVariableName(${rel.name}) = val;
+                break;
+#end
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+##################################
+## Create serialization support ##
+##################################
+    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);
+#foreach( $attr in ${object.DeclaredAttributes} )
+#set ( $name = "$stringUtils.formatVariableName(${attr.Name})")
+#set ( $type = "$importUtils.formatJavaType(${attr.Type})")
+#if($importUtils.isPrimitive($type))
+        out.write${stringUtils.capitalized($type)}($name);
+#else
+        out.writeObject(${name});
+#end
+#end
+#foreach( $rel in ${object.DeclaredRelationships} )
+        out.writeObject(${stringUtils.formatVariableName($rel.Name)});
+#end
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+#foreach( $attr in ${object.DeclaredAttributes} )
+#set ( $name = "$stringUtils.formatVariableName(${attr.Name})")
+#set ( $type = "$importUtils.formatJavaType(${attr.Type})")
+#if($importUtils.isPrimitive($type))
+        $name = in.read${stringUtils.capitalized($type)}();
+#else
+        $name = ($type)in.readObject();
+#end
+#end
+#foreach( $rel in ${object.DeclaredRelationships} )
+#set ( $name = "${stringUtils.formatVariableName($rel.Name)}")
+        $name = in.readObject();
+#end
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
----------------------------------------------------------------------
diff --git a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
index c6e1885..2eaffa6 100644
--- a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
+++ b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
@@ -46,7 +46,7 @@ public class ClassGenerationActionTest {
 
 	@Before
 	public void setUp() throws Exception {
-		this.writers = new ArrayList<StringWriter>(3);
+		this.writers = new ArrayList<>(3);
 		this.action = new ClassGenerationAction() {
 
 			@Override
@@ -79,7 +79,7 @@ public class ClassGenerationActionTest {
 
 		String superclass = generated.get(0);
 		assertTrue(superclass, superclass.contains("package org.example.auto;"));
-		assertTrue(superclass, superclass.contains("import org.apache.cayenne.CayenneDataObject;"));
+		assertTrue(superclass, superclass.contains("import org.apache.cayenne.BaseDataObject;"));
 
 		String subclass = generated.get(1);
 		assertTrue(subclass, subclass.contains("package org.example;"));
@@ -147,18 +147,16 @@ public class ClassGenerationActionTest {
 		String superclass = generated.get(0);
 
 		assertTrue(superclass, superclass.contains("public void setID(int ID)"));
-		assertTrue(superclass, superclass.contains("writeProperty(\"ID\", ID);"));
+		assertTrue(superclass, superclass.contains("this.ID = ID;"));
 
 		assertTrue(superclass, superclass.contains("public int getID()"));
-		assertTrue(superclass, superclass.contains("Object value = readProperty(\"ID\");"));
-		assertTrue(superclass, superclass.contains("return (value != null) ? (Integer) value : 0;"));
+		assertTrue(superclass, superclass.contains("return this.ID;"));
 
 		assertTrue(superclass, superclass.contains("public void setName(char name)"));
-		assertTrue(superclass, superclass.contains("writeProperty(\"name\", name);"));
+		assertTrue(superclass, superclass.contains("this.name = name;"));
 
 		assertTrue(superclass, superclass.contains("public char getName()"));
-		assertTrue(superclass, superclass.contains("Object value = readProperty(\"name\");"));
-		assertTrue(superclass, superclass.contains("return (value != null) ? (Character) value : 0;"));
+		assertTrue(superclass, superclass.contains("return this.name;"));
 
 	}
 
@@ -247,7 +245,7 @@ public class ClassGenerationActionTest {
 
 		action.execute(artifact);
 
-		List<String> strings = new ArrayList<String>(writers.size());
+		List<String> strings = new ArrayList<>(writers.size());
 		for (StringWriter writer : writers) {
 			strings.add(writer.toString());
 		}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextPrimitiveIT.java
----------------------------------------------------------------------
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextPrimitiveIT.java b/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextPrimitiveIT.java
index acc7c69..e57ab05 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextPrimitiveIT.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextPrimitiveIT.java
@@ -19,6 +19,7 @@
 package org.apache.cayenne;
 
 import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.query.SortOrder;
 import org.apache.cayenne.test.jdbc.DBHelper;
@@ -104,4 +105,32 @@ public class CayenneContextPrimitiveIT extends ClientCase {
         assertFalse(tTablePrimitives.getBoolean("BOOLEAN_COLUMN"));
         assertEquals(8, tTablePrimitives.getInt("INT_COLUMN"));
     }
+
+    @Test
+    public void testCommitEmptyChangesPrimitives() throws Exception {
+
+        ClientTablePrimitives object = context.newObject(ClientTablePrimitives.class);
+
+        context.commitChanges();
+
+        assertFalse(tTablePrimitives.getBoolean("BOOLEAN_COLUMN"));
+        assertEquals(0, tTablePrimitives.getInt("INT_COLUMN"));
+
+        object.setBooleanColumn(true);
+        object.setIntColumn(8);
+        context.commitChanges();
+
+        assertTrue(tTablePrimitives.getBoolean("BOOLEAN_COLUMN"));
+        assertEquals(8, tTablePrimitives.getInt("INT_COLUMN"));
+    }
+
+    @Test
+    public void testSelectEmptyPrimitives() throws Exception {
+        tTablePrimitives.insert(1, accessStackAdapter.supportsBoolean() ? true : 1, null);
+
+        ClientTablePrimitives object = ObjectSelect.query(ClientTablePrimitives.class).selectFirst(context);
+
+        assertTrue(object.isBooleanColumn());
+        assertEquals(0, object.getIntColumn());
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java
index ca49870..3c773f5 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java
@@ -1,6 +1,10 @@
 package org.apache.cayenne.crypto.db.auto;
 
-import org.apache.cayenne.CayenneDataObject;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.exp.Property;
 
 /**
@@ -9,7 +13,7 @@ import org.apache.cayenne.exp.Property;
  * since it may be overwritten next time code is regenerated.
  * If you need to make any customizations, please use subclass.
  */
-public abstract class _Table1 extends CayenneDataObject {
+public abstract class _Table1 extends BaseDataObject {
 
     private static final long serialVersionUID = 1L; 
 
@@ -20,34 +24,120 @@ public abstract class _Table1 extends CayenneDataObject {
     public static final Property<Integer> PLAIN_INT = Property.create("plainInt", Integer.class);
     public static final Property<String> PLAIN_STRING = Property.create("plainString", String.class);
 
+    protected int cryptoInt;
+    protected String cryptoString;
+    protected int plainInt;
+    protected String plainString;
+
+
     public void setCryptoInt(int cryptoInt) {
-        writeProperty("cryptoInt", cryptoInt);
+        beforePropertyWrite("cryptoInt", this.cryptoInt, cryptoInt);
+        this.cryptoInt = cryptoInt;
     }
+
     public int getCryptoInt() {
-        Object value = readProperty("cryptoInt");
-        return (value != null) ? (Integer) value : 0;
+        beforePropertyRead("cryptoInt");
+        return cryptoInt;
     }
 
     public void setCryptoString(String cryptoString) {
-        writeProperty("cryptoString", cryptoString);
+        beforePropertyWrite("cryptoString", this.cryptoString, cryptoString);
+        this.cryptoString = cryptoString;
     }
+
     public String getCryptoString() {
-        return (String)readProperty("cryptoString");
+        beforePropertyRead("cryptoString");
+        return cryptoString;
     }
 
     public void setPlainInt(int plainInt) {
-        writeProperty("plainInt", plainInt);
+        beforePropertyWrite("plainInt", this.plainInt, plainInt);
+        this.plainInt = plainInt;
     }
+
     public int getPlainInt() {
-        Object value = readProperty("plainInt");
-        return (value != null) ? (Integer) value : 0;
+        beforePropertyRead("plainInt");
+        return plainInt;
     }
 
     public void setPlainString(String plainString) {
-        writeProperty("plainString", plainString);
+        beforePropertyWrite("plainString", this.plainString, plainString);
+        this.plainString = plainString;
     }
+
     public String getPlainString() {
-        return (String)readProperty("plainString");
+        beforePropertyRead("plainString");
+        return plainString;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "cryptoInt":
+                return this.cryptoInt;
+            case "cryptoString":
+                return this.cryptoString;
+            case "plainInt":
+                return this.plainInt;
+            case "plainString":
+                return this.plainString;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "cryptoInt":
+                this.cryptoInt = val == null ? 0 : (Integer)val;
+                break;
+            case "cryptoString":
+                this.cryptoString = (String)val;
+                break;
+            case "plainInt":
+                this.plainInt = val == null ? 0 : (Integer)val;
+                break;
+            case "plainString":
+                this.plainString = (String)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(cryptoInt);
+        out.writeObject(cryptoString);
+        out.writeInt(plainInt);
+        out.writeObject(plainString);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        cryptoInt = in.readInt();
+        cryptoString = (String)in.readObject();
+        plainInt = in.readInt();
+        plainString = (String)in.readObject();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java
index 3c65795..c10987e 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java
@@ -1,6 +1,10 @@
 package org.apache.cayenne.crypto.db.auto;
 
-import org.apache.cayenne.CayenneDataObject;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.exp.Property;
 
 /**
@@ -9,7 +13,7 @@ import org.apache.cayenne.exp.Property;
  * since it may be overwritten next time code is regenerated.
  * If you need to make any customizations, please use subclass.
  */
-public abstract class _Table2 extends CayenneDataObject {
+public abstract class _Table2 extends BaseDataObject {
 
     private static final long serialVersionUID = 1L; 
 
@@ -18,18 +22,84 @@ public abstract class _Table2 extends CayenneDataObject {
     public static final Property<byte[]> CRYPTO_BYTES = Property.create("cryptoBytes", byte[].class);
     public static final Property<byte[]> PLAIN_BYTES = Property.create("plainBytes", byte[].class);
 
+    protected byte[] cryptoBytes;
+    protected byte[] plainBytes;
+
+
     public void setCryptoBytes(byte[] cryptoBytes) {
-        writeProperty("cryptoBytes", cryptoBytes);
+        beforePropertyWrite("cryptoBytes", this.cryptoBytes, cryptoBytes);
+        this.cryptoBytes = cryptoBytes;
     }
+
     public byte[] getCryptoBytes() {
-        return (byte[])readProperty("cryptoBytes");
+        beforePropertyRead("cryptoBytes");
+        return cryptoBytes;
     }
 
     public void setPlainBytes(byte[] plainBytes) {
-        writeProperty("plainBytes", plainBytes);
+        beforePropertyWrite("plainBytes", this.plainBytes, plainBytes);
+        this.plainBytes = plainBytes;
     }
+
     public byte[] getPlainBytes() {
-        return (byte[])readProperty("plainBytes");
+        beforePropertyRead("plainBytes");
+        return plainBytes;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "cryptoBytes":
+                return this.cryptoBytes;
+            case "plainBytes":
+                return this.plainBytes;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "cryptoBytes":
+                this.cryptoBytes = (byte[])val;
+                break;
+            case "plainBytes":
+                this.plainBytes = (byte[])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.writeObject(cryptoBytes);
+        out.writeObject(plainBytes);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        cryptoBytes = (byte[])in.readObject();
+        plainBytes = (byte[])in.readObject();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java
index 917197c..9e0d027 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java
@@ -1,6 +1,10 @@
 package org.apache.cayenne.crypto.db.auto;
 
-import org.apache.cayenne.CayenneDataObject;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.exp.Property;
 
 /**
@@ -9,7 +13,7 @@ import org.apache.cayenne.exp.Property;
  * since it may be overwritten next time code is regenerated.
  * If you need to make any customizations, please use subclass.
  */
-public abstract class _Table3 extends CayenneDataObject {
+public abstract class _Table3 extends BaseDataObject {
 
     private static final long serialVersionUID = 1L; 
 
@@ -17,11 +21,66 @@ public abstract class _Table3 extends CayenneDataObject {
 
     public static final Property<String> CRYPTO_STRING = Property.create("cryptoString", String.class);
 
+    protected String cryptoString;
+
+
     public void setCryptoString(String cryptoString) {
-        writeProperty("cryptoString", cryptoString);
+        beforePropertyWrite("cryptoString", this.cryptoString, cryptoString);
+        this.cryptoString = cryptoString;
     }
+
     public String getCryptoString() {
-        return (String)readProperty("cryptoString");
+        beforePropertyRead("cryptoString");
+        return cryptoString;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "cryptoString":
+                return this.cryptoString;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "cryptoString":
+                this.cryptoString = (String)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.writeObject(cryptoString);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        cryptoString = (String)in.readObject();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table4.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table4.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table4.java
index ae30286..5563c63 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table4.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table4.java
@@ -1,6 +1,10 @@
 package org.apache.cayenne.crypto.db.auto;
 
-import org.apache.cayenne.CayenneDataObject;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.exp.Property;
 
 /**
@@ -9,7 +13,7 @@ import org.apache.cayenne.exp.Property;
  * since it may be overwritten next time code is regenerated.
  * If you need to make any customizations, please use subclass.
  */
-public abstract class _Table4 extends CayenneDataObject {
+public abstract class _Table4 extends BaseDataObject {
 
     private static final long serialVersionUID = 1L; 
 
@@ -18,19 +22,84 @@ public abstract class _Table4 extends CayenneDataObject {
     public static final Property<Integer> PLAIN_INT = Property.create("plainInt", Integer.class);
     public static final Property<String> PLAIN_STRING = Property.create("plainString", String.class);
 
+    protected int plainInt;
+    protected String plainString;
+
+
     public void setPlainInt(int plainInt) {
-        writeProperty("plainInt", plainInt);
+        beforePropertyWrite("plainInt", this.plainInt, plainInt);
+        this.plainInt = plainInt;
     }
+
     public int getPlainInt() {
-        Object value = readProperty("plainInt");
-        return (value != null) ? (Integer) value : 0;
+        beforePropertyRead("plainInt");
+        return plainInt;
     }
 
     public void setPlainString(String plainString) {
-        writeProperty("plainString", plainString);
+        beforePropertyWrite("plainString", this.plainString, plainString);
+        this.plainString = plainString;
     }
+
     public String getPlainString() {
-        return (String)readProperty("plainString");
+        beforePropertyRead("plainString");
+        return plainString;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "plainInt":
+                return this.plainInt;
+            case "plainString":
+                return this.plainString;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "plainInt":
+                this.plainInt = val == null ? 0 : (Integer)val;
+                break;
+            case "plainString":
+                this.plainString = (String)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(plainInt);
+        out.writeObject(plainString);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        plainInt = in.readInt();
+        plainString = (String)in.readObject();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table5.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table5.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table5.java
index 3f4127f..44aa168 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table5.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table5.java
@@ -1,6 +1,10 @@
 package org.apache.cayenne.crypto.db.auto;
 
-import org.apache.cayenne.CayenneDataObject;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.exp.Property;
 
 /**
@@ -9,7 +13,7 @@ import org.apache.cayenne.exp.Property;
  * since it may be overwritten next time code is regenerated.
  * If you need to make any customizations, please use subclass.
  */
-public abstract class _Table5 extends CayenneDataObject {
+public abstract class _Table5 extends BaseDataObject {
 
     private static final long serialVersionUID = 1L; 
 
@@ -19,28 +23,102 @@ public abstract class _Table5 extends CayenneDataObject {
     public static final Property<Integer> CRYPTO_INT3 = Property.create("cryptoInt3", Integer.class);
     public static final Property<Integer> CRYPTO_INT4 = Property.create("cryptoInt4", Integer.class);
 
+    protected int cryptoInt1;
+    protected int cryptoInt3;
+    protected int cryptoInt4;
+
+
     public void setCryptoInt1(int cryptoInt1) {
-        writeProperty("cryptoInt1", cryptoInt1);
+        beforePropertyWrite("cryptoInt1", this.cryptoInt1, cryptoInt1);
+        this.cryptoInt1 = cryptoInt1;
     }
+
     public int getCryptoInt1() {
-        Object value = readProperty("cryptoInt1");
-        return (value != null) ? (Integer) value : 0;
+        beforePropertyRead("cryptoInt1");
+        return cryptoInt1;
     }
 
     public void setCryptoInt3(int cryptoInt3) {
-        writeProperty("cryptoInt3", cryptoInt3);
+        beforePropertyWrite("cryptoInt3", this.cryptoInt3, cryptoInt3);
+        this.cryptoInt3 = cryptoInt3;
     }
+
     public int getCryptoInt3() {
-        Object value = readProperty("cryptoInt3");
-        return (value != null) ? (Integer) value : 0;
+        beforePropertyRead("cryptoInt3");
+        return cryptoInt3;
     }
 
     public void setCryptoInt4(int cryptoInt4) {
-        writeProperty("cryptoInt4", cryptoInt4);
+        beforePropertyWrite("cryptoInt4", this.cryptoInt4, cryptoInt4);
+        this.cryptoInt4 = cryptoInt4;
     }
+
     public int getCryptoInt4() {
-        Object value = readProperty("cryptoInt4");
-        return (value != null) ? (Integer) value : 0;
+        beforePropertyRead("cryptoInt4");
+        return cryptoInt4;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "cryptoInt1":
+                return this.cryptoInt1;
+            case "cryptoInt3":
+                return this.cryptoInt3;
+            case "cryptoInt4":
+                return this.cryptoInt4;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "cryptoInt1":
+                this.cryptoInt1 = val == null ? 0 : (Integer)val;
+                break;
+            case "cryptoInt3":
+                this.cryptoInt3 = val == null ? 0 : (Integer)val;
+                break;
+            case "cryptoInt4":
+                this.cryptoInt4 = val == null ? 0 : (Integer)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(cryptoInt1);
+        out.writeInt(cryptoInt3);
+        out.writeInt(cryptoInt4);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        cryptoInt1 = in.readInt();
+        cryptoInt3 = in.readInt();
+        cryptoInt4 = in.readInt();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table6.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table6.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table6.java
index cd5df66..61db882 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table6.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table6.java
@@ -1,6 +1,10 @@
 package org.apache.cayenne.crypto.db.auto;
 
-import org.apache.cayenne.CayenneDataObject;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.exp.Property;
 
 /**
@@ -9,7 +13,7 @@ import org.apache.cayenne.exp.Property;
  * since it may be overwritten next time code is regenerated.
  * If you need to make any customizations, please use subclass.
  */
-public abstract class _Table6 extends CayenneDataObject {
+public abstract class _Table6 extends BaseDataObject {
 
     private static final long serialVersionUID = 1L; 
 
@@ -18,20 +22,84 @@ public abstract class _Table6 extends CayenneDataObject {
     public static final Property<Long> CRYPTO_INT1 = Property.create("cryptoInt1", Long.class);
     public static final Property<Integer> CRYPTO_INT4 = Property.create("cryptoInt4", Integer.class);
 
+    protected long cryptoInt1;
+    protected int cryptoInt4;
+
+
     public void setCryptoInt1(long cryptoInt1) {
-        writeProperty("cryptoInt1", cryptoInt1);
+        beforePropertyWrite("cryptoInt1", this.cryptoInt1, cryptoInt1);
+        this.cryptoInt1 = cryptoInt1;
     }
+
     public long getCryptoInt1() {
-        Object value = readProperty("cryptoInt1");
-        return (value != null) ? (Long) value : 0;
+        beforePropertyRead("cryptoInt1");
+        return cryptoInt1;
     }
 
     public void setCryptoInt4(int cryptoInt4) {
-        writeProperty("cryptoInt4", cryptoInt4);
+        beforePropertyWrite("cryptoInt4", this.cryptoInt4, cryptoInt4);
+        this.cryptoInt4 = cryptoInt4;
     }
+
     public int getCryptoInt4() {
-        Object value = readProperty("cryptoInt4");
-        return (value != null) ? (Integer) value : 0;
+        beforePropertyRead("cryptoInt4");
+        return cryptoInt4;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "cryptoInt1":
+                return this.cryptoInt1;
+            case "cryptoInt4":
+                return this.cryptoInt4;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "cryptoInt1":
+                this.cryptoInt1 = val == null ? 0 : (Long)val;
+                break;
+            case "cryptoInt4":
+                this.cryptoInt4 = val == null ? 0 : (Integer)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.writeLong(cryptoInt1);
+        out.writeInt(cryptoInt4);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        cryptoInt1 = in.readLong();
+        cryptoInt4 = in.readInt();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalDateTestEntity.java
----------------------------------------------------------------------
diff --git a/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalDateTestEntity.java b/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalDateTestEntity.java
index d5db3b9..8b41f9c 100644
--- a/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalDateTestEntity.java
+++ b/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalDateTestEntity.java
@@ -1,8 +1,11 @@
 package org.apache.cayenne.java8.db.auto;
 
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.time.LocalDate;
 
-import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.exp.Property;
 
 /**
@@ -11,7 +14,7 @@ import org.apache.cayenne.exp.Property;
  * since it may be overwritten next time code is regenerated.
  * If you need to make any customizations, please use subclass.
  */
-public abstract class _LocalDateTestEntity extends CayenneDataObject {
+public abstract class _LocalDateTestEntity extends BaseDataObject {
 
     private static final long serialVersionUID = 1L; 
 
@@ -19,11 +22,66 @@ public abstract class _LocalDateTestEntity extends CayenneDataObject {
 
     public static final Property<LocalDate> DATE = Property.create("date", LocalDate.class);
 
+    protected LocalDate date;
+
+
     public void setDate(LocalDate date) {
-        writeProperty("date", date);
+        beforePropertyWrite("date", this.date, date);
+        this.date = date;
     }
+
     public LocalDate getDate() {
-        return (LocalDate)readProperty("date");
+        beforePropertyRead("date");
+        return date;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "date":
+                return this.date;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "date":
+                this.date = (LocalDate)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.writeObject(date);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        date = (LocalDate)in.readObject();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalDateTimeTestEntity.java
----------------------------------------------------------------------
diff --git a/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalDateTimeTestEntity.java b/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalDateTimeTestEntity.java
index 59623a0..b630aad 100644
--- a/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalDateTimeTestEntity.java
+++ b/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalDateTimeTestEntity.java
@@ -1,8 +1,11 @@
 package org.apache.cayenne.java8.db.auto;
 
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.time.LocalDateTime;
 
-import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.exp.Property;
 
 /**
@@ -11,7 +14,7 @@ import org.apache.cayenne.exp.Property;
  * since it may be overwritten next time code is regenerated.
  * If you need to make any customizations, please use subclass.
  */
-public abstract class _LocalDateTimeTestEntity extends CayenneDataObject {
+public abstract class _LocalDateTimeTestEntity extends BaseDataObject {
 
     private static final long serialVersionUID = 1L; 
 
@@ -19,11 +22,66 @@ public abstract class _LocalDateTimeTestEntity extends CayenneDataObject {
 
     public static final Property<LocalDateTime> TIMESTAMP = Property.create("timestamp", LocalDateTime.class);
 
+    protected LocalDateTime timestamp;
+
+
     public void setTimestamp(LocalDateTime timestamp) {
-        writeProperty("timestamp", timestamp);
+        beforePropertyWrite("timestamp", this.timestamp, timestamp);
+        this.timestamp = timestamp;
     }
+
     public LocalDateTime getTimestamp() {
-        return (LocalDateTime)readProperty("timestamp");
+        beforePropertyRead("timestamp");
+        return timestamp;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "timestamp":
+                return this.timestamp;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "timestamp":
+                this.timestamp = (LocalDateTime)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.writeObject(timestamp);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        timestamp = (LocalDateTime)in.readObject();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalTimeTestEntity.java
----------------------------------------------------------------------
diff --git a/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalTimeTestEntity.java b/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalTimeTestEntity.java
index 3b2ea88..f1496d3 100644
--- a/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalTimeTestEntity.java
+++ b/cayenne-java8/src/test/java/org/apache/cayenne/java8/db/auto/_LocalTimeTestEntity.java
@@ -1,8 +1,11 @@
 package org.apache.cayenne.java8.db.auto;
 
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.time.LocalTime;
 
-import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.exp.Property;
 
 /**
@@ -11,7 +14,7 @@ import org.apache.cayenne.exp.Property;
  * since it may be overwritten next time code is regenerated.
  * If you need to make any customizations, please use subclass.
  */
-public abstract class _LocalTimeTestEntity extends CayenneDataObject {
+public abstract class _LocalTimeTestEntity extends BaseDataObject {
 
     private static final long serialVersionUID = 1L; 
 
@@ -19,11 +22,66 @@ public abstract class _LocalTimeTestEntity extends CayenneDataObject {
 
     public static final Property<LocalTime> TIME = Property.create("time", LocalTime.class);
 
+    protected LocalTime time;
+
+
     public void setTime(LocalTime time) {
-        writeProperty("time", time);
+        beforePropertyWrite("time", this.time, time);
+        this.time = time;
     }
+
     public LocalTime getTime() {
-        return (LocalTime)readProperty("time");
+        beforePropertyRead("time");
+        return time;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "time":
+                return this.time;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "time":
+                this.time = (LocalTime)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.writeObject(time);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        time = (LocalTime)in.readObject();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/544aae08/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
new file mode 100644
index 0000000..17447c7
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/BaseDataObject.java
@@ -0,0 +1,722 @@
+/*****************************************************************
+ *   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.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.EmbeddedAttribute;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.reflect.PropertyUtils;
+import org.apache.cayenne.validation.BeanValidationFailure;
+import org.apache.cayenne.validation.ValidationFailure;
+import org.apache.cayenne.validation.ValidationResult;
+
+/**
+ * Base implementation of 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})
+ *
+ * @see CayenneDataObject
+ * @since 4.1
+ */
+public abstract class BaseDataObject extends PersistentObject implements DataObject, Validating {
+
+    private static final long serialVersionUID = 4598677040697008371L;
+
+    protected long snapshotVersion = DEFAULT_VERSION;
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        // return null by default to keep compatibility with pre 4.1 versions
+        return null;
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        throw new IllegalArgumentException("Unknown property: " + propName);
+    }
+
+    protected void beforePropertyRead(String propName) {
+        if(objectContext != null) {
+            // will resolve faults ourselves below as checking class descriptors
+            // for the "lazyFaulting" flag is inefficient. Passing "false" here to
+            // suppress fault processing
+            objectContext.prepareForAccess(this, propName, false);
+        }
+    }
+
+    protected void beforePropertyWrite(String propName, Object oldValue, Object newValue) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, propName, false);
+            objectContext.propertyChanged(this, propName, oldValue, newValue);
+        }
+    }
+
+    @Override
+    public Object readProperty(String propertyName) {
+        beforePropertyRead(propertyName);
+
+        Object object = readPropertyDirectly(propertyName);
+
+        if (object instanceof Fault) {
+            object = ((Fault) object).resolveFault(this, propertyName);
+            writePropertyDirectly(propertyName, object);
+        }
+
+        return object;
+    }
+
+    /**
+     * Returns a value of the property identified by a property path. Supports
+     * reading both mapped and unmapped properties. Unmapped properties are
+     * accessed in a manner consistent with JavaBeans specification.
+     * <p>
+     * Property path (or nested property) is a dot-separated path used to
+     * traverse object relationships until the final object is found. If a null
+     * object found while traversing path, null is returned. If a list is
+     * encountered in the middle of the path, CayenneRuntimeException is thrown.
+     * Unlike {@link #readPropertyDirectly(String)}, this method will resolve an
+     * object if it is HOLLOW.
+     * <p>
+     * Examples:
+     * </p>
+     * <ul>
+     * <li>Read this object property:<br>
+     * <code>String name = (String)artist.readNestedProperty("name");</code><br>
+     * <br>
+     * </li>
+     * <li>Read an object related to this object:<br>
+     * <code>Gallery g = (Gallery)paintingInfo.readNestedProperty("toPainting.toGallery");</code>
+     * <br>
+     * <br>
+     * </li>
+     * <li>Read a property of an object related to this object: <br>
+     * <code>String name = (String)painting.readNestedProperty("toArtist.artistName");</code>
+     * <br>
+     * <br>
+     * </li>
+     * <li>Read to-many relationship list:<br>
+     * <code>List exhibits = (List)painting.readNestedProperty("toGallery.exhibitArray");</code>
+     * <br>
+     * <br>
+     * </li>
+     * <li>Read to-many relationship in the middle of the path:<br>
+     * <code>List&lt;String&gt; names = (List&lt;String&gt;)artist.readNestedProperty("paintingArray.paintingName");</code>
+     * <br>
+     * <br>
+     * </li>
+     * </ul>
+     *
+     * @since 1.0.5
+     */
+    public Object readNestedProperty(String path) {
+
+        if ((null == path) || (0 == path.length())) {
+            throw new IllegalArgumentException("the path must be supplied in order to lookup a nested property");
+        }
+
+        int dotIndex = path.indexOf('.');
+
+        if (0 == dotIndex) {
+            throw new IllegalArgumentException("the path is invalid because it starts with a period character");
+        }
+
+        if (dotIndex == path.length() - 1) {
+            throw new IllegalArgumentException("the path is invalid because it ends with a period character");
+        }
+
+        if (-1 == dotIndex) {
+            return readSimpleProperty(path);
+        }
+
+        String path0 = path.substring(0, dotIndex);
+        String pathRemainder = path.substring(dotIndex + 1);
+
+        // this is copied from the old code where the placement of a plus
+        // character at the end of a segment of a property path would
+        // simply strip out the plus. I am not entirely sure why this is
+        // done. See unit test 'testReadNestedPropertyToManyInMiddle1'.
+
+        if ('+' == path0.charAt(path0.length() - 1)) {
+            path0 = path0.substring(0, path0.length() - 1);
+        }
+
+        Object property = readSimpleProperty(path0);
+
+        if (property == null) {
+            return null;
+        } else if (property instanceof DataObject) {
+            return ((DataObject) property).readNestedProperty(pathRemainder);
+        } else {
+            return Cayenne.readNestedProperty(property, pathRemainder);
+        }
+    }
+
+    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) {
+            object = PropertyUtils.getProperty(this, property);
+        }
+
+        return object;
+    }
+
+    @Override
+    public void writeProperty(String propName, Object val) {
+        Object oldValue = readPropertyDirectly(propName);
+        beforePropertyWrite(propName, oldValue, val);
+
+        writePropertyDirectly(propName, val);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void removeToManyTarget(String relName, DataObject value, boolean setReverse) {
+
+        // Now do the rest of the normal handling (regardless of whether it was
+        // flattened or not)
+        Object holder = readProperty(relName);
+
+        // call 'propertyChanged' AFTER readProperty as readProperty ensures
+        // that this object fault is resolved
+        objectContext.propertyChanged(this, relName, value, null);
+
+        // TODO: andrus 8/20/2007 - can we optimize this somehow, avoiding type checking??
+        if (holder instanceof Collection) {
+            ((Collection<Object>) holder).remove(value);
+        } else if (holder instanceof Map) {
+            ((Map<Object, Object>) holder).remove(getMapKey(relName, value));
+        }
+
+        if (value != null && setReverse) {
+            unsetReverseRelationship(relName, value);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void addToManyTarget(String relName, DataObject value, boolean setReverse) {
+        if (value == null) {
+            throw new NullPointerException("Attempt to add null target DataObject.");
+        }
+
+        willConnect(relName, value);
+
+        // Now do the rest of the normal handling (regardless of whether it was
+        // flattened or not)
+        Object holder = readProperty(relName);
+
+        // call 'propertyChanged' AFTER readProperty as readProperty ensures
+        // that this object fault is resolved
+        objectContext.propertyChanged(this, relName, null, value);
+
+        // TODO: andrus 8/20/2007 - can we optimize this somehow, avoiding type checking??
+        if (holder instanceof Collection) {
+            ((Collection<Object>) holder).add(value);
+        } else if (holder instanceof Map) {
+            ((Map<Object, Object>) holder).put(getMapKey(relName, value), value);
+        }
+
+        if (setReverse) {
+            setReverseRelationship(relName, value);
+        }
+    }
+
+    /**
+     * Sets the relationships to the specified <code>DataObject</code> objects.
+     *
+     * <p>
+     * New relationships will be created with
+     * {@link #addToManyTarget(String, org.apache.cayenne.DataObject, boolean)},
+     * already established relationships stay untouched. Missing relationships
+     * will be removed with
+     * {@link #removeToManyTarget(String, org.apache.cayenne.DataObject, boolean)}
+     * and returnd as List. You may delete them manually.
+     * </p>
+     *
+     * <p>
+     * Notice: Moving an object relationship to another object, is still needing
+     * an manually "unregister" from the first object by
+     * {@link #removeToManyTarget(String, org.apache.cayenne.DataObject, boolean)}
+     * </p>
+     *
+     * @param relName
+     *            name of the relation
+     * @param values
+     *            <code>DataObject</code> objects of this
+     *            <code>Collection</code> are set to the object. No changes will
+     *            be made to the the <code>Collection</code>, a copy is used. It
+     *            is safe to pass a persisted <code>Collection</code> of another
+     *            object.
+     * @param setReverse
+     *            update reverse relationships
+     * @return <code>List&lt;? extends DataObject&gt;</code> of unrelated
+     *         DataObjects. If no relationship was removed an empty List is
+     *         returned.
+     * @throws IllegalArgumentException
+     *             if no relationship could be read by relName, or if the passed
+     *             <code>Collection</code> is null. To clear all relationships
+     *             use an empty <code>Collection</code>
+     * @throws UnsupportedOperationException
+     *             if the relation Collection Type is neither
+     *             <code>java.util.Collection</code> nor
+     *             <code>java.util.Map</code>
+     * @since 4.0
+     */
+    @SuppressWarnings("unchecked")
+    public List<? extends DataObject> setToManyTarget(String relName, Collection<? extends DataObject> values,
+                                                      boolean setReverse) {
+        if (values == null) {
+            throw new IllegalArgumentException("values Collection is null. To clear all relationships use an empty Collection");
+        }
+
+        Object property = readProperty(relName);
+        if(property == null) {
+            throw new IllegalArgumentException("unknown relName " + relName);
+        }
+        Collection<DataObject> old = null;
+        if (property instanceof Map) {
+            old = ((Map) property).values();
+        } else if (property instanceof Collection) {
+            old = (Collection) property;
+        } else {
+            throw new UnsupportedOperationException("setToManyTarget operates only with Map or Collection types");
+        }
+
+        // operate on a copy of passed collection
+        values = new ArrayList<>(values);
+
+        List<DataObject> removedObjects = new ArrayList<>();
+
+        // remove all relationships, which are missing in passed collection
+        Object[] oldValues = old.toArray();
+        for (Object obj : oldValues) {
+            if (!values.contains(obj)) {
+                DataObject obj2 = (DataObject) obj;
+                removeToManyTarget(relName, obj2, setReverse);
+                // collect objects whose relationship was removed
+                removedObjects.add((DataObject) obj2);
+            }
+        }
+
+        // dont add elements which are already present
+        for (Object obj : old) {
+            values.remove(obj);
+        }
+
+        // add new elements
+        for (DataObject obj : values) {
+            addToManyTarget(relName, obj, setReverse);
+        }
+
+        return removedObjects;
+    }
+
+    @Override
+    public void setToOneTarget(String relationshipName, DataObject value, boolean setReverse) {
+
+        willConnect(relationshipName, value);
+
+        Object oldTarget = readProperty(relationshipName);
+        if (oldTarget == value) {
+            return;
+        }
+
+        getObjectContext().propertyChanged(this, relationshipName, oldTarget, value);
+
+        if (setReverse) {
+            // unset old reverse relationship
+            if (oldTarget instanceof DataObject) {
+                unsetReverseRelationship(relationshipName, (DataObject) oldTarget);
+            }
+
+            // set new reverse relationship
+            if (value != null) {
+                setReverseRelationship(relationshipName, value);
+            }
+        }
+
+        // readProperty will call this
+        //objectContext.prepareForAccess(this, relationshipName, false);
+        writePropertyDirectly(relationshipName, value);
+    }
+
+    /**
+     * Called before establishing a relationship with another object. Applies
+     * "persistence by reachability" logic, pulling one of the two objects to a
+     * DataConext of another object in case one of the objects is transient. If
+     * both objects are persistent, and they don't have the same DataContext,
+     * CayenneRuntimeException is thrown.
+     *
+     * @since 1.2
+     */
+    protected void willConnect(String relationshipName, Persistent object) {
+        // first handle most common case - both objects are in the same
+        // ObjectContext or target is null
+        if (object == null || this.getObjectContext() == object.getObjectContext()) {
+            return;
+        } else if (this.getObjectContext() == null && object.getObjectContext() != null) {
+            object.getObjectContext().registerNewObject(this);
+        } else if (this.getObjectContext() != null && object.getObjectContext() == null) {
+            this.getObjectContext().registerNewObject(object);
+        } else {
+            throw new CayenneRuntimeException("Cannot set object as destination of relationship %s"
+                    + " because it is in a different ObjectContext",  relationshipName);
+        }
+    }
+
+    /**
+     * Initializes reverse relationship from object <code>val</code> to this
+     * object.
+     *
+     * @param relName
+     *            name of relationship from this object to <code>val</code>.
+     */
+    protected void setReverseRelationship(String relName, DataObject val) {
+        ObjRelationship rel = objectContext.getEntityResolver().getObjEntity(objectId.getEntityName())
+                .getRelationship(relName);
+        ObjRelationship revRel = rel.getReverseRelationship();
+        if (revRel != null) {
+            if (revRel.isToMany()) {
+                val.addToManyTarget(revRel.getName(), this, false);
+            } else {
+                val.setToOneTarget(revRel.getName(), this, false);
+            }
+        }
+    }
+
+    /**
+     * Removes current object from reverse relationship of object
+     * <code>val</code> to this object.
+     */
+    protected void unsetReverseRelationship(String relName, DataObject val) {
+
+        EntityResolver resolver = objectContext.getEntityResolver();
+        ObjEntity entity = resolver.getObjEntity(objectId.getEntityName());
+
+        if (entity == null) {
+            throw new IllegalStateException("DataObject's entity is unmapped, objectId: " + objectId);
+        }
+
+        ObjRelationship rel = entity.getRelationship(relName);
+        ObjRelationship revRel = rel.getReverseRelationship();
+        if (revRel != null) {
+            if (revRel.isToMany()) {
+                val.removeToManyTarget(revRel.getName(), this, false);
+            } else {
+                val.setToOneTarget(revRel.getName(), null, false);
+            }
+        }
+    }
+
+    @Override
+    public void setPersistenceState(int persistenceState) {
+        this.persistenceState = persistenceState;
+    }
+
+    /**
+     * @since 1.1
+     */
+    @Override
+    public long getSnapshotVersion() {
+        return snapshotVersion;
+    }
+
+    /**
+     * @since 1.1
+     */
+    @Override
+    public void setSnapshotVersion(long snapshotVersion) {
+        this.snapshotVersion = snapshotVersion;
+    }
+
+    /**
+     * Performs property validation of the object, appending any validation
+     * failures to the provided validationResult object. This method is invoked
+     * from "validateFor.." before committing a NEW or MODIFIED object to the
+     * database. Validation includes checking for null values and value sizes.
+     * CayenneDataObject subclasses may override this method, calling super.
+     *
+     * @since 1.1
+     */
+    protected void validateForSave(ValidationResult validationResult) {
+
+        ObjEntity objEntity = getObjectContext().getEntityResolver().getObjEntity(this);
+        if (objEntity == null) {
+            throw new CayenneRuntimeException("No ObjEntity mapping found for DataObject %s", getClass().getName());
+        }
+
+        // validate mandatory attributes
+
+        Map<String, ValidationFailure> failedDbAttributes = null;
+
+        for (ObjAttribute next : objEntity.getAttributes()) {
+
+            // TODO: andrus, 2/20/2007 - handle embedded attribute
+            if (next instanceof EmbeddedAttribute) {
+                continue;
+            }
+
+            DbAttribute dbAttribute = next.getDbAttribute();
+            if (dbAttribute == null) {
+                throw new CayenneRuntimeException("ObjAttribute '%s"
+                        + "' does not have a corresponding DbAttribute", next.getName());
+            }
+
+            // pk may still be generated
+            if (dbAttribute.isPrimaryKey()) {
+                continue;
+            }
+
+            Object value = this.readPropertyDirectly(next.getName());
+            if (dbAttribute.isMandatory()) {
+                ValidationFailure failure = BeanValidationFailure.validateNotNull(this, next.getName(), value);
+
+                if (failure != null) {
+
+                    if (failedDbAttributes == null) {
+                        failedDbAttributes = new HashMap<>();
+                    }
+
+                    failedDbAttributes.put(dbAttribute.getName(), failure);
+                    continue;
+                }
+            }
+
+            // validate length
+            if (value != null && dbAttribute.getMaxLength() > 0) {
+                if (value.getClass().isArray()) {
+                    int len = Array.getLength(value);
+                    if (len > dbAttribute.getMaxLength()) {
+                        String message = "\"" + next.getName() + "\" exceeds maximum allowed length ("
+                                + dbAttribute.getMaxLength() + " bytes): " + len;
+                        validationResult.addFailure(new BeanValidationFailure(this, next.getName(), message));
+                    }
+                } else if (value instanceof CharSequence) {
+                    int len = ((CharSequence) value).length();
+                    if (len > dbAttribute.getMaxLength()) {
+                        String message = "\"" + next.getName() + "\" exceeds maximum allowed length ("
+                                + dbAttribute.getMaxLength() + " chars): " + len;
+                        validationResult.addFailure(new BeanValidationFailure(this, next.getName(), message));
+                    }
+                }
+            }
+        }
+
+        // validate mandatory relationships
+        for (final ObjRelationship relationship : objEntity.getRelationships()) {
+
+            List<DbRelationship> dbRels = relationship.getDbRelationships();
+            if (dbRels.isEmpty()) {
+                continue;
+            }
+
+            // skip db relationships that we can't validate or that can't be invalid here
+            // can't handle paths longer than two db relationships
+            // see ObjRelationship.recalculateReadOnlyValue() for more info
+            if (relationship.isSourceIndependentFromTargetChange()) {
+                continue;
+            }
+
+            // if db relationship is not based on a PK and is based on mandatory
+            // attributes, see if we have a target object set
+            // relationship will be validated only if all db path has mandatory
+            // db relationships
+            boolean validate = true;
+            for (DbRelationship dbRelationship : dbRels) {
+                for (DbJoin join : dbRelationship.getJoins()) {
+                    DbAttribute source = join.getSource();
+                    if (source.isMandatory()) {
+                        // clear attribute failures...
+                        if (failedDbAttributes != null && !failedDbAttributes.isEmpty()) {
+                            failedDbAttributes.remove(source.getName());
+                        }
+                    } else {
+                        // do not validate if the relation is based on
+                        // multiple keys with some that can be nullable.
+                        validate = false;
+                    }
+                }
+            }
+
+            if (validate) {
+                Object value = this.readPropertyDirectly(relationship.getName());
+                ValidationFailure failure = BeanValidationFailure.validateNotNull(this, relationship.getName(), value);
+
+                if (failure != null) {
+                    validationResult.addFailure(failure);
+                }
+            }
+        }
+
+        // deal with previously found attribute failures...
+        if (failedDbAttributes != null && !failedDbAttributes.isEmpty()) {
+            for (ValidationFailure failure : failedDbAttributes.values()) {
+                validationResult.addFailure(failure);
+            }
+        }
+    }
+
+    /**
+     * Calls {@link #validateForSave(ValidationResult)}. CayenneDataObject
+     * subclasses may override it providing validation logic that should be
+     * executed for the newly created objects before saving them.
+     *
+     * @since 1.1
+     */
+    @Override
+    public void validateForInsert(ValidationResult validationResult) {
+        validateForSave(validationResult);
+    }
+
+    /**
+     * Calls {@link #validateForSave(ValidationResult)}. CayenneDataObject
+     * subclasses may override it providing validation logic that should be
+     * executed for the modified objects before saving them.
+     *
+     * @since 1.1
+     */
+    @Override
+    public void validateForUpdate(ValidationResult validationResult) {
+        validateForSave(validationResult);
+    }
+
+    /**
+     * This implementation does nothing. CayenneDataObject subclasses may
+     * override it providing validation logic that should be executed for the
+     * deleted objects before committing them.
+     *
+     * @since 1.1
+     */
+    @Override
+    public void validateForDelete(ValidationResult validationResult) {
+        // does nothing
+    }
+
+    /**
+     * @since 1.2
+     */
+    @Override
+    public void setObjectContext(ObjectContext objectContext) {
+        this.objectContext = objectContext;
+
+        if (objectContext == null) {
+            this.persistenceState = PersistenceState.TRANSIENT;
+        }
+    }
+
+    /**
+     * Serialization support.
+     * Will write down persistenceState and objectId, delegating data serialization down to sub-classes.
+     */
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    /**
+     * Serialization support.
+     * Will read persistenceState and objectId, delegating data serialization down to sub-classes.
+     */
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    protected void writeSerialized(ObjectOutputStream out) throws IOException {
+        out.writeInt(persistenceState);
+        out.writeObject(objectId);
+
+        if(persistenceState == PersistenceState.COMMITTED
+                || persistenceState == PersistenceState.HOLLOW) {
+            return;
+        }
+
+        writeState(out);
+    }
+
+    protected void readSerialized(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        this.persistenceState = in.readInt();
+        this.objectId = (ObjectId) in.readObject();
+
+        if(persistenceState == PersistenceState.COMMITTED
+                || persistenceState == PersistenceState.HOLLOW) {
+            persistenceState = PersistenceState.HOLLOW;
+            return;
+        }
+
+        readState(in);
+    }
+
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        // no additional info for base class
+    }
+
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        // no additional info for base class
+    }
+
+    /**
+     * A variation of "toString" method, that may be more efficient in some
+     * cases. For example when printing a list of objects into the same String.
+     */
+    public StringBuffer toStringBuffer(StringBuffer buffer, boolean fullDesc) {
+        String id = (objectId != null) ? objectId.toString() : "<no id>";
+        String state = PersistenceState.persistenceStateName(persistenceState);
+
+        buffer.append('{').append(id).append("; ").append(state).append("; ");
+
+        if (fullDesc) {
+            appendProperties(buffer);
+        }
+
+        buffer.append("}");
+        return buffer;
+    }
+
+    protected void appendProperties(StringBuffer buffer) {
+    }
+
+    @Override
+    public String toString() {
+        return toStringBuffer(new StringBuffer(), true).toString();
+    }
+
+}