added tests for lazy attributes
diff --git a/cayenne-cgen/src/main/resources/templates/v4_1/client-superclass.vm b/cayenne-cgen/src/main/resources/templates/v4_1/client-superclass.vm
index 74c1729..14a4e1e 100644
--- a/cayenne-cgen/src/main/resources/templates/v4_1/client-superclass.vm
+++ b/cayenne-cgen/src/main/resources/templates/v4_1/client-superclass.vm
@@ -78,8 +78,12 @@
 
 ## Create ivars
 #foreach( $attr in ${object.DeclaredAttributes} )
+#if ($attr.Lazy)
+    protected Object ${attr.Name};
+#else
     protected $importUtils.formatJavaType(${attr.Type}) ${attr.Name};
 #end
+#end
 #foreach( $rel in ${object.DeclaredRelationships} )
 #if( $rel.ToMany )
 #if ( ${rel.CollectionType} == "java.util.Map")
@@ -110,7 +114,17 @@
             objectContext.prepareForAccess(this, "${attr.Name}", false);
         }
 
+#if ($attr.Lazy)
+        if(this.$attrName instanceof Fault) {
+            this.$attrName = ((Fault) this.$attrName).resolveFault(this, "$attr.Name");
+        }
+#end
+
+#if ($attr.Lazy)
+        return ($importUtils.formatJavaType(${attr.Type})) $attrName;
+#else
         return $attrName;
+#end
     }
 
 #end
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/cay_2641/Cay2641IT.java b/cayenne-client/src/test/java/org/apache/cayenne/cay_2641/Cay2641IT.java
new file mode 100644
index 0000000..3b2d148
--- /dev/null
+++ b/cayenne-client/src/test/java/org/apache/cayenne/cay_2641/Cay2641IT.java
@@ -0,0 +1,97 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cay_2641;
+
+import org.apache.cayenne.CayenneContext;
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.testdo.cay_2641.client.ArtistLazy;
+import org.apache.cayenne.testdo.cay_2641.client.PaintingLazy;
+import org.apache.cayenne.unit.di.client.ClientCase;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+@UseServerRuntime(CayenneProjects.CAY_2641)
+public class Cay2641IT extends ClientCase {
+
+    @Inject
+    private CayenneContext context;
+
+    @Before
+    public void setup() {
+        ArtistLazy artistLazy = context.newObject(ArtistLazy.class);
+        artistLazy.setName("Test");
+        artistLazy.setSurname("Test1");
+
+        PaintingLazy paintingLazy = context.newObject(PaintingLazy.class);
+        paintingLazy.setName("Test");
+        paintingLazy.setArtist(artistLazy);
+
+        context.commitChanges();
+    }
+
+    @Test
+    public void testSampleSelect() {
+        List<ArtistLazy> artists = ObjectSelect.query(ArtistLazy.class).select(context);
+
+        assertEquals(artists.size(), 1);
+        assertEquals(artists.get(0).getSurname(), "Test1");
+
+        assertTrue(artists.get(0).readPropertyDirectly("name") instanceof Fault);
+
+        assertEquals(artists.get(0).getName(), "Test");
+    }
+
+    @Test
+    public void testColumnSelect() {
+        List<String> strings = ObjectSelect.columnQuery(ArtistLazy.class, ArtistLazy.NAME).select(context);
+
+        assertEquals(strings.size(), 1);
+        assertEquals(strings.get(0), "Test");
+    }
+
+    @Test
+    public void testPrefetchSelect() {
+        List<PaintingLazy> paintingLazyList1 = ObjectSelect.query(PaintingLazy.class).prefetch(PaintingLazy.ARTIST.joint()).select(context);
+
+        assertEquals(paintingLazyList1.size(), 1);
+        assertTrue(paintingLazyList1.get(0).getArtist().readPropertyDirectly("name") instanceof Fault);
+
+        List<PaintingLazy> paintingLazyList2 = ObjectSelect.query(PaintingLazy.class).prefetch(PaintingLazy.ARTIST.disjoint()).select(context);
+
+        assertEquals(paintingLazyList2.size(), 1);
+        assertTrue(paintingLazyList1.get(0).getArtist().readPropertyDirectly("name") instanceof Fault);
+
+        List<PaintingLazy> paintingLazyList3 = ObjectSelect.query(PaintingLazy.class).prefetch(PaintingLazy.ARTIST.disjointById()).select(context);
+
+        assertEquals(paintingLazyList3.size(), 1);
+        assertTrue(paintingLazyList1.get(0).getArtist().readPropertyDirectly("name") instanceof Fault);
+    }
+
+}
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/HessianConfig.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/HessianConfig.java
index 615474d..39e502e 100644
--- a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/HessianConfig.java
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/HessianConfig.java
@@ -82,7 +82,7 @@
             Collection<String> additionalWhitelist) {
 
         SerializerFactory factory = new CayenneSerializerFactory();
-
+        factory.setAllowNonSerializable(true);
         List<String> whitelist = new ArrayList<>(additionalWhitelist);
         if(resolver != null) {
             whitelist.add("org.apache.cayenne.*");
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowUtils.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowUtils.java
index ce65013..768ed80 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowUtils.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowUtils.java
@@ -89,19 +89,18 @@
                 ObjAttribute attr = property.getAttribute();
                 String dbAttrPath = attr.getDbAttributePath();
 
-                Object value = snapshot.get(dbAttrPath);
-                property.writePropertyDirectly(object, null, value);
-
                 // note that a check "snaphsot.get(..) == null" would be incorrect in this
                 // case, as NULL value is entirely valid; still save a map lookup by
                 // checking for the null value first
-                if (value == null && !snapshot.containsKey(dbAttrPath)) {
-                    if(attr.isLazy()) {
-                        property.writePropertyDirectly(object, null, new AttributeFault(property));
-                    } else {
-                        isPartialSnapshot[0] = true;
-                    }
+                if (snapshot.containsKey(dbAttrPath) && !attr.isLazy()) {
+                    Object value = snapshot.get(dbAttrPath);
+                    property.writePropertyDirectly(object, null, value);
+                } else if (attr.isLazy()) {
+                    property.writePropertyDirectly(object, null, new AttributeFault(property));
+                } else {
+                    isPartialSnapshot[0] = true;
                 }
+
                 return true;
             }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectDetachOperation.java b/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectDetachOperation.java
index f5a7064..8884e04 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectDetachOperation.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectDetachOperation.java
@@ -22,16 +22,10 @@
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.AttributeFault;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.PrefetchTreeNode;
-import org.apache.cayenne.reflect.ArcProperty;
-import org.apache.cayenne.reflect.AttributeProperty;
-import org.apache.cayenne.reflect.ClassDescriptor;
-import org.apache.cayenne.reflect.PropertyDescriptor;
-import org.apache.cayenne.reflect.PropertyVisitor;
-import org.apache.cayenne.reflect.ToManyMapProperty;
-import org.apache.cayenne.reflect.ToManyProperty;
-import org.apache.cayenne.reflect.ToOneProperty;
+import org.apache.cayenne.reflect.*;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -197,7 +191,11 @@
             public boolean visitAttribute(AttributeProperty property) {
                 PropertyDescriptor targetProperty = targetDescriptor
                         .getProperty(property.getName());
-                targetProperty.writeProperty(target, null, property.readProperty(source));
+                if (!property.getAttribute().isLazy()) {
+                    targetProperty.writeProperty(target, null, property.readProperty(source));
+                } else {
+                    targetProperty.writeProperty(target, null, new AttributeFault(property));
+                }
                 return true;
             }
         });
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2641.java b/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2641.java
new file mode 100644
index 0000000..e10be4b
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2641.java
@@ -0,0 +1,174 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://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.access;
+
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.ColumnSelect;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.cay_2641.ArtistLazy;
+import org.apache.cayenne.testdo.cay_2641.DatamapLazy;
+import org.apache.cayenne.testdo.cay_2641.PaintingLazy;
+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.junit.Before;
+import org.junit.Test;
+
+import java.sql.Types;
+import java.util.List;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @since 4.2
+ */
+@UseServerRuntime(CayenneProjects.CAY_2641)
+public class Cay2641 extends ServerCase {
+
+    @Inject
+    private ObjectContext context;
+
+    @Inject
+    private DBHelper dbHelper;
+
+    @Inject
+    private DbAdapter adapter;
+
+    @Before
+    public void setup() throws Exception {
+        TableHelper th = new TableHelper(dbHelper, "ArtistLazy")
+                .setColumns("ID", "NAME", "SURNAME")
+                .setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.VARCHAR);
+        th.insert(1, "artist1", "artist2");
+
+        th = new TableHelper(dbHelper, "PaintingLazy")
+                .setColumns("ID", "NAME", "ARTIST_ID")
+                .setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.INTEGER);
+        th.insert(1, "painting1", 1);
+    }
+
+    @Test
+    public void testTranslatorSql() {
+        ObjectSelect<ArtistLazy> artists = ObjectSelect.query(ArtistLazy.class);
+
+        DefaultSelectTranslator translator = new DefaultSelectTranslator(artists, adapter, context.getEntityResolver());
+
+        String sql = translator.getSql();
+        assertFalse(sql.contains("t0.NAME"));
+
+        String string = "SELECT t0.SURNAME, t0.ID FROM ArtistLazy t0";
+        assertTrue(sql.equals(string));
+
+        ColumnSelect<String> select = ObjectSelect.columnQuery(ArtistLazy.class, ArtistLazy.NAME);
+        translator = new DefaultSelectTranslator(select, adapter, context.getEntityResolver());
+        sql = translator.getSql();
+
+        assertTrue(sql.contains("t0.NAME"));
+    }
+
+    @Test
+    public void testTypeAttributes() {
+        List<ArtistLazy> artists = ObjectSelect.query(ArtistLazy.class).select(context);
+
+        Object object = artists.get(0).readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+
+        object = artists.get(0).readPropertyDirectly("surname");
+        assertTrue(object.equals("artist2"));
+    }
+
+    @Test
+    public void testTypeLazyAttribute() {
+        ArtistLazy artist = ObjectSelect.query(ArtistLazy.class).selectFirst(context);
+
+        Object object = artist.readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+
+        artist.getName();
+        object = artist.readPropertyDirectly("name");
+        assertTrue(object.equals("artist1"));
+    }
+
+    @Test
+    public void testPrefetchLazyTranslatorSql() {
+        ObjectSelect<PaintingLazy> paintingLazyObjectSelect = ObjectSelect.query(PaintingLazy.class).prefetch(PaintingLazy.ARTIST.joint());
+        DefaultSelectTranslator translator = new DefaultSelectTranslator(paintingLazyObjectSelect, adapter, context.getEntityResolver());
+        String sql = translator.getSql();
+        assertFalse(sql.contains("t0.NAME"));
+
+        String string = "SELECT DISTINCT t0.ARTIST_ID, t0.ID, t1.ID, t1.SURNAME FROM PaintingLazy t0 LEFT JOIN ArtistLazy t1 ON t0.ARTIST_ID = t1.ID";
+        assertTrue(sql.equals(string));
+    }
+
+    @Test
+    public void testPrefetchLazyTypeAttributes() {
+        List<PaintingLazy> paintingLazyList = ObjectSelect.query(PaintingLazy.class).prefetch(PaintingLazy.ARTIST.joint()).select(context);
+
+        Object object = paintingLazyList.get(0).readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+
+        object = paintingLazyList.get(0).getName();
+        assertTrue(object instanceof String);
+        assertTrue(object.equals("painting1"));
+
+        ArtistLazy artist = (ArtistLazy) paintingLazyList.get(0).readPropertyDirectly("artist");
+        object = artist.readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+
+        object = artist.readPropertyDirectly("surname");
+        assertTrue(object.equals("artist2"));
+
+        object = artist.getName();
+        assertTrue(object instanceof String);
+        assertTrue(object.equals("artist1"));
+    }
+
+    @Test
+    public void testsSimpleSelectCustomer() {
+        DatamapLazy optimistic = DatamapLazy.getInstance();
+        List<ArtistLazy> artistLazies = optimistic.performSimpleSelect(context);
+
+        Object object = artistLazies.get(0).readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+
+        object = artistLazies.get(0).readPropertyDirectly("surname");
+        assertTrue(object instanceof String);
+        assertTrue(object.equals("artist2"));
+    }
+
+    @Test
+    public void testsPrefetchSelectCustomer() {
+        DatamapLazy optimistic = DatamapLazy.getInstance();
+        List<PaintingLazy> paintingLazies = optimistic.performPrefetchSelect(context);
+
+        Object object = paintingLazies.get(0).readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+
+        ArtistLazy artist = (ArtistLazy) paintingLazies.get(0).readPropertyDirectly("artist");
+        object = artist.readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/ArtistLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/ArtistLazy.java
new file mode 100644
index 0000000..dcd9224
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/ArtistLazy.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.cay_2641;
+
+import org.apache.cayenne.testdo.cay_2641.auto._ArtistLazy;
+
+public class ArtistLazy extends _ArtistLazy {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/DatamapLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/DatamapLazy.java
new file mode 100644
index 0000000..f026e12
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/DatamapLazy.java
@@ -0,0 +1,18 @@
+package org.apache.cayenne.testdo.cay_2641;
+
+import org.apache.cayenne.testdo.cay_2641.auto._DatamapLazy;
+
+public class DatamapLazy extends _DatamapLazy {
+
+    private static DatamapLazy instance;
+
+    private DatamapLazy() {}
+
+    public static DatamapLazy getInstance() {
+        if(instance == null) {
+            instance = new DatamapLazy();
+        }
+
+        return instance;
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/PaintingLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/PaintingLazy.java
new file mode 100644
index 0000000..dd6f77a
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/PaintingLazy.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.cay_2641;
+
+import org.apache.cayenne.testdo.cay_2641.auto._PaintingLazy;
+
+public class PaintingLazy extends _PaintingLazy {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_ArtistLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_ArtistLazy.java
new file mode 100644
index 0000000..aedc3e9
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_ArtistLazy.java
@@ -0,0 +1,135 @@
+package org.apache.cayenne.testdo.cay_2641.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.List;
+
+import org.apache.cayenne.BaseDataObject;
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.exp.property.ListProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
+import org.apache.cayenne.testdo.cay_2641.PaintingLazy;
+
+/**
+ * Class _ArtistLazy 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 _ArtistLazy extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
+    public static final StringProperty<String> SURNAME = PropertyFactory.createString("surname", String.class);
+    public static final ListProperty<PaintingLazy> PAINTINGS = PropertyFactory.createList("paintings", PaintingLazy.class);
+
+    protected Object name;
+    protected String surname;
+
+    protected Object paintings;
+
+    public void setName(String name) {
+        beforePropertyWrite("name", this.name, name);
+        this.name = name;
+    }
+
+    public String getName() {
+        beforePropertyRead("name");
+        if(this.name instanceof Fault) {
+            this.name = ((Fault) this.name).resolveFault(this, "name");
+        }
+        return (String)this.name;
+    }
+
+    public void setSurname(String surname) {
+        beforePropertyWrite("surname", this.surname, surname);
+        this.surname = surname;
+    }
+
+    public String getSurname() {
+        beforePropertyRead("surname");
+        return this.surname;
+    }
+
+    public void addToPaintings(PaintingLazy obj) {
+        addToManyTarget("paintings", obj, true);
+    }
+
+    public void removeFromPaintings(PaintingLazy obj) {
+        removeToManyTarget("paintings", obj, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<PaintingLazy> getPaintings() {
+        return (List<PaintingLazy>)readProperty("paintings");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "name":
+                return this.name;
+            case "surname":
+                return this.surname;
+            case "paintings":
+                return this.paintings;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "name":
+                this.name = val;
+                break;
+            case "surname":
+                this.surname = (String)val;
+                break;
+            case "paintings":
+                this.paintings = 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(this.name);
+        out.writeObject(this.surname);
+        out.writeObject(this.paintings);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.name = in.readObject();
+        this.surname = (String)in.readObject();
+        this.paintings = in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_DatamapLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_DatamapLazy.java
new file mode 100644
index 0000000..756360a
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_DatamapLazy.java
@@ -0,0 +1,33 @@
+package org.apache.cayenne.testdo.cay_2641.auto;
+
+import java.util.List;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.query.MappedSelect;
+import org.apache.cayenne.testdo.cay_2641.ArtistLazy;
+import org.apache.cayenne.testdo.cay_2641.PaintingLazy;
+
+/**
+ * This class 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 class _DatamapLazy {
+
+    public static final String PREFETCH_SELECT_QUERYNAME = "prefetchSelect";
+
+    public static final String SIMPLE_SELECT_QUERYNAME = "simpleSelect";
+
+    public List<PaintingLazy> performPrefetchSelect(ObjectContext context) {
+        MappedSelect<PaintingLazy> query = MappedSelect.query(PREFETCH_SELECT_QUERYNAME, PaintingLazy.class);
+        return query.select(context);
+    }
+
+
+    public List<ArtistLazy> performSimpleSelect(ObjectContext context) {
+        MappedSelect<ArtistLazy> query = MappedSelect.query(SIMPLE_SELECT_QUERYNAME, ArtistLazy.class);
+        return query.select(context);
+    }
+
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_PaintingLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_PaintingLazy.java
new file mode 100644
index 0000000..5386919
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_PaintingLazy.java
@@ -0,0 +1,110 @@
+package org.apache.cayenne.testdo.cay_2641.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.exp.property.EntityProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
+import org.apache.cayenne.testdo.cay_2641.ArtistLazy;
+
+/**
+ * Class _PaintingLazy 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 _PaintingLazy extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
+    public static final EntityProperty<ArtistLazy> ARTIST = PropertyFactory.createEntity("artist", ArtistLazy.class);
+
+    protected Object name;
+
+    protected Object artist;
+
+    public void setName(String name) {
+        beforePropertyWrite("name", this.name, name);
+        this.name = name;
+    }
+
+    public String getName() {
+        beforePropertyRead("name");
+        if(this.name instanceof Fault) {
+            this.name = ((Fault) this.name).resolveFault(this, "name");
+        }
+        return (String)this.name;
+    }
+
+    public void setArtist(ArtistLazy artist) {
+        setToOneTarget("artist", artist, true);
+    }
+
+    public ArtistLazy getArtist() {
+        return (ArtistLazy)readProperty("artist");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "name":
+                return this.name;
+            case "artist":
+                return this.artist;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "name":
+                this.name = val;
+                break;
+            case "artist":
+                this.artist = 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(this.name);
+        out.writeObject(this.artist);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.name = in.readObject();
+        this.artist = in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/ArtistLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/ArtistLazy.java
new file mode 100644
index 0000000..593a6aa
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/ArtistLazy.java
@@ -0,0 +1,12 @@
+package org.apache.cayenne.testdo.cay_2641.client;
+
+import org.apache.cayenne.testdo.cay_2641.client.auto._ArtistLazy;
+
+/**
+ * A persistent class mapped as "ArtistLazy" Cayenne entity.
+ */
+public class ArtistLazy extends _ArtistLazy {
+
+     private static final long serialVersionUID = 1L; 
+     
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/PaintingLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/PaintingLazy.java
new file mode 100644
index 0000000..a290ca4
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/PaintingLazy.java
@@ -0,0 +1,12 @@
+package org.apache.cayenne.testdo.cay_2641.client;
+
+import org.apache.cayenne.testdo.cay_2641.client.auto._PaintingLazy;
+
+/**
+ * A persistent class mapped as "PaintingLazy" Cayenne entity.
+ */
+public class PaintingLazy extends _PaintingLazy {
+
+     private static final long serialVersionUID = 1L; 
+     
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/auto/_ArtistLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/auto/_ArtistLazy.java
new file mode 100644
index 0000000..ec1111d
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/auto/_ArtistLazy.java
@@ -0,0 +1,113 @@
+package org.apache.cayenne.testdo.cay_2641.client.auto;
+
+import java.util.List;
+
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.property.ListProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
+import org.apache.cayenne.testdo.cay_2641.client.PaintingLazy;
+import org.apache.cayenne.util.PersistentObjectList;
+
+/**
+ * A generated persistent class mapped as "ArtistLazy" Cayenne entity. It is a good idea to
+ * avoid changing this class manually, since it will be overwritten next time code is
+ * regenerated. If you need to make any customizations, put them in a subclass.
+ */
+public abstract class _ArtistLazy extends PersistentObject {
+
+    public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
+    public static final StringProperty<String> SURNAME = PropertyFactory.createString("surname", String.class);
+    public static final ListProperty<PaintingLazy> PAINTINGS = PropertyFactory.createList("paintings", PaintingLazy.class);
+
+    protected Object name;
+    protected String surname;
+    protected List<PaintingLazy> paintings;
+
+    public String getName() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        if(this.name instanceof Fault) {
+            this.name = ((Fault) this.name).resolveFault(this, "name");
+        }
+
+        return (String) name;
+    }
+
+    public void setName(String name) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+            objectContext.propertyChanged(this, "name", this.name, name);
+        }
+
+        this.name = name;
+    }
+
+    public String getSurname() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "surname", false);
+        }
+
+
+        return surname;
+    }
+
+    public void setSurname(String surname) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "surname", false);
+            objectContext.propertyChanged(this, "surname", this.surname, surname);
+        }
+
+        this.surname = surname;
+    }
+
+    public List<PaintingLazy> getPaintings() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList<>(this, "paintings");
+		}
+
+        return paintings;
+    }
+
+    public void addToPaintings(PaintingLazy object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList<>(this, "paintings");
+		}
+
+        this.paintings.add(object);
+    }
+
+    public void removeFromPaintings(PaintingLazy object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList<>(this, "paintings");
+		}
+
+        this.paintings.remove(object);
+    }
+
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "name":
+                return this.name;
+            case "surname":
+                return this.surname;
+            case "paintings":
+                return this.paintings;
+            default:
+                return null;
+        }
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/auto/_PaintingLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/auto/_PaintingLazy.java
new file mode 100644
index 0000000..29e52dc
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/auto/_PaintingLazy.java
@@ -0,0 +1,66 @@
+package org.apache.cayenne.testdo.cay_2641.client.auto;
+
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.ValueHolder;
+import org.apache.cayenne.exp.property.EntityProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
+import org.apache.cayenne.testdo.cay_2641.client.ArtistLazy;
+import org.apache.cayenne.util.PersistentObjectHolder;
+
+/**
+ * A generated persistent class mapped as "PaintingLazy" Cayenne entity. It is a good idea to
+ * avoid changing this class manually, since it will be overwritten next time code is
+ * regenerated. If you need to make any customizations, put them in a subclass.
+ */
+public abstract class _PaintingLazy extends PersistentObject {
+
+    public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
+    public static final EntityProperty<ArtistLazy> ARTIST = PropertyFactory.createEntity("artist", ArtistLazy.class);
+
+    protected Object name;
+    protected ValueHolder<ArtistLazy> artist;
+
+    public String getName() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        if(this.name instanceof Fault) {
+            this.name = ((Fault) this.name).resolveFault(this, "name");
+        }
+
+        return (String) name;
+    }
+
+    public void setName(String name) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+            objectContext.propertyChanged(this, "name", this.name, name);
+        }
+
+        this.name = name;
+    }
+
+    public ArtistLazy getArtist() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "artist", true);
+        } else if (this.artist == null) {
+        	this.artist = new PersistentObjectHolder<>(this, "artist");
+		}
+
+        return artist.getValue();
+    }
+
+    public void setArtist(ArtistLazy artist) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "artist", true);
+        } else if (this.artist == null) {
+        	this.artist = new PersistentObjectHolder<>(this, "artist");
+		}
+
+        this.artist.setValue(artist);
+    }
+
+}
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 bd971d8..3d7d1e5 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
@@ -86,4 +86,5 @@
     public static final String INHERITANCE_WITH_ENUM_PROJECT = "cayenne-inheritance-with-enum.xml";
     public static final String LAZY_ATTRIBUTES_PROJECT = "cayenne-lazy-attributes.xml";
     public static final String CAY_2666 = "cay2666/cayenne-cay-2666.xml";
+    public static final String CAY_2641 = "cay2641/cayenne-cay-2641.xml";
 }
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 2b8aed1..8067005 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 @@
 			"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", "hybrid-data-object.map.xml",
-			"java8.map.xml", "inheritance-with-enum.map.xml", "lazy-attributes.map.xml", "cay2666/datamap.map.xml" };
+			"java8.map.xml", "inheritance-with-enum.map.xml", "lazy-attributes.map.xml", "cay2666/datamap.map.xml", "cay2641/datamapLazy.map.xml" };
 
 	// hardcoded dependent entities that should be excluded
 	// if LOBs are not supported
diff --git a/cayenne-server/src/test/resources/cay2641/cayenne-cay-2641.xml b/cayenne-server/src/test/resources/cay2641/cayenne-cay-2641.xml
new file mode 100644
index 0000000..3990e67
--- /dev/null
+++ b/cayenne-server/src/test/resources/cay2641/cayenne-cay-2641.xml
@@ -0,0 +1,7 @@
+<?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 https://cayenne.apache.org/schema/10/domain.xsd"
+	 project-version="10">
+	<map name="datamapLazy"/>
+</domain>
diff --git a/cayenne-server/src/test/resources/cay2641/datamapLazy.map.xml b/cayenne-server/src/test/resources/cay2641/datamapLazy.map.xml
new file mode 100644
index 0000000..884f8c2
--- /dev/null
+++ b/cayenne-server/src/test/resources/cay2641/datamapLazy.map.xml
@@ -0,0 +1,52 @@
+<?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 https://cayenne.apache.org/schema/10/modelMap.xsd"
+	 project-version="10">
+	<property name="defaultPackage" value="org.apache.cayenne.testdo.cay_2641"/>
+	<property name="clientSupported" value="true"/>
+	<property name="defaultClientPackage" value="org.apache.cayenne.testdo.cay_2641.client"/>
+	<db-entity name="ArtistLazy">
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true" length="5"/>
+		<db-attribute name="NAME" type="VARCHAR" length="10"/>
+		<db-attribute name="SURNAME" type="VARCHAR" length="10"/>
+	</db-entity>
+	<db-entity name="PaintingLazy">
+		<db-attribute name="ARTIST_ID" type="INTEGER" length="10"/>
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true" length="10"/>
+		<db-attribute name="NAME" type="VARCHAR" length="10"/>
+	</db-entity>
+	<obj-entity name="ArtistLazy" className="org.apache.cayenne.testdo.cay_2641.ArtistLazy" clientClassName="org.apache.cayenne.testdo.cay_2641.client.ArtistLazy" dbEntityName="ArtistLazy">
+		<obj-attribute name="name" type="java.lang.String" lazy="true" db-attribute-path="NAME"/>
+		<obj-attribute name="surname" type="java.lang.String" db-attribute-path="SURNAME"/>
+	</obj-entity>
+	<obj-entity name="PaintingLazy" className="org.apache.cayenne.testdo.cay_2641.PaintingLazy" clientClassName="org.apache.cayenne.testdo.cay_2641.client.PaintingLazy" lock-type="optimistic" dbEntityName="PaintingLazy">
+		<obj-attribute name="name" type="java.lang.String" lazy="true" db-attribute-path="NAME"/>
+	</obj-entity>
+	<db-relationship name="paintings" source="ArtistLazy" target="PaintingLazy" toMany="true">
+		<db-attribute-pair source="ID" target="ARTIST_ID"/>
+	</db-relationship>
+	<db-relationship name="artist" source="PaintingLazy" target="ArtistLazy">
+		<db-attribute-pair source="ARTIST_ID" target="ID"/>
+	</db-relationship>
+	<obj-relationship name="paintings" source="ArtistLazy" target="PaintingLazy" deleteRule="Deny" db-relationship-path="paintings"/>
+	<obj-relationship name="artist" source="PaintingLazy" target="ArtistLazy" deleteRule="Nullify" db-relationship-path="artist"/>
+	<query name="prefetchSelect" type="SelectQuery" root="obj-entity" root-name="PaintingLazy">
+		<prefetch type="disjoint"><![CDATA[artist]]></prefetch>
+	</query>
+	<query name="simpleSelect" type="SelectQuery" root="obj-entity" root-name="ArtistLazy"/>
+	<cgen xmlns="http://cayenne.apache.org/schema/10/cgen">
+		<excludeEntities>ArtistLazy,PaintingLazy</excludeEntities>
+		<destDir>../../java</destDir>
+		<mode>all</mode>
+		<template>templates/v4_1/subclass.vm</template>
+		<superTemplate>templates/v4_1/superclass.vm</superTemplate>
+		<outputPattern>*.java</outputPattern>
+		<makePairs>true</makePairs>
+		<usePkgPath>true</usePkgPath>
+		<overwrite>false</overwrite>
+		<createPropertyNames>false</createPropertyNames>
+		<createPKProperties>false</createPKProperties>
+		<client>false</client>
+	</cgen>
+</data-map>