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 2019/09/02 09:29:29 UTC

[cayenne] 01/03: CAY-2606 Can't resolve obj path with embeddable component

This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 02ad8881dec2ab9de7578e34f4c4250d034ac9a5
Author: Nikita Timofeev <st...@gmail.com>
AuthorDate: Thu Aug 29 10:24:18 2019 +0300

    CAY-2606 Can't resolve obj path with embeddable component
---
 RELEASE-NOTES.txt                                  |   1 +
 .../java/org/apache/cayenne/gen/CgenModule.java    |   4 +-
 .../java/org/apache/cayenne/gen/ImportUtils.java   |   5 +
 .../java/org/apache/cayenne/gen/PropertyUtils.java |  81 +++++++------
 .../EmbeddablePropertyDescriptorCreator.java       |  35 +++---
 .../templates/v4_1/embeddable-singleclass.vm       |  64 ++++++++++-
 .../templates/v4_1/embeddable-superclass.vm        |  60 +++++++++-
 .../org/apache/cayenne/gen/PropertyUtilsTest.java  |  13 ++-
 ...ranslationResult.java => EmbeddableObject.java} |  44 +++----
 .../cayenne/access/DataDomainQueryAction.java      |  51 ++++++++-
 .../jdbc/reader/DefaultRowReaderFactory.java       |  11 +-
 .../access/jdbc/reader/EmbeddableRowReader.java    |  79 +++++++++++++
 .../select/CustomColumnSetExtractor.java           |  25 ++++
 .../access/translator/select/ObjPathProcessor.java |  17 +++
 .../access/translator/select/PathProcessor.java    |   6 +
 .../translator/select/PathTranslationResult.java   |   3 +
 .../translator/select/QualifierTranslator.java     |  90 ++++++++++-----
 .../cayenne/exp/property/EmbeddableProperty.java}  |  24 ++--
 ...RelationshipProperty.java => PathProperty.java} |  62 +++-------
 .../cayenne/exp/property/PropertyFactory.java      |   5 +
 .../cayenne/exp/property/RelationshipProperty.java | 120 +------------------
 .../org/apache/cayenne/map/EmbeddedResult.java     |  59 ++++++++++
 .../apache/cayenne/map/PathComponentIterator.java  |  15 ++-
 .../java/org/apache/cayenne/map/SQLResult.java     |  20 +++-
 .../DefaultEmbeddableResultSegment.java}           |  42 ++++---
 .../cayenne/query/EmbeddableResultSegment.java}    |  22 +++-
 .../org/apache/cayenne/access/EmbeddingIT.java     | 127 ++++++++++++++++++++-
 .../cayenne/testdo/embeddable/EmbedEntity1.java    |   2 +
 .../cayenne/testdo/embeddable/EmbedEntity2.java    |   9 ++
 .../testdo/embeddable/auto/_EmbedEntity1.java      |  33 +++++-
 .../{_EmbedEntity1.java => _EmbedEntity2.java}     |  74 ++++++------
 .../testdo/embeddable/auto/_Embeddable1.java       |  41 ++++++-
 .../src/test/resources/cayenne-embeddable.xml      |   2 +
 .../src/test/resources/embeddable.map.xml          |  25 +++-
 34 files changed, 881 insertions(+), 390 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index b638050..f7413ee 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -81,6 +81,7 @@ CAY-2600 Modeler DbImport: Can't retrieve schema for databases with no catalog s
 CAY-2601 Modeler DbImport: result dialog issues
 CAY-2603 NPE reloading project in the model
 CAY-2605 Modeler: Unable to save - java.nio.file.InvalidPathException
+CAY-2606 Can't resolve obj path with embeddable component
 CAY-2608 CayenneModeler: NPE when reverse engineering with an auto-adapter DataSource
 CAY-2609 Modeler: can't close dbImport result dialog window
 
diff --git a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenModule.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenModule.java
index 59cf873..076a5b6 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenModule.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenModule.java
@@ -26,6 +26,7 @@ import org.apache.cayenne.di.Module;
 import org.apache.cayenne.di.spi.DefaultAdhocObjectFactory;
 import org.apache.cayenne.di.spi.DefaultClassLoaderManager;
 import org.apache.cayenne.gen.property.DatePropertyDescriptorCreator;
+import org.apache.cayenne.gen.property.EmbeddablePropertyDescriptorCreator;
 import org.apache.cayenne.gen.property.NumericPropertyDescriptorCreator;
 import org.apache.cayenne.gen.property.PropertyDescriptorCreator;
 import org.apache.cayenne.gen.property.StringPropertyDescriptorCreator;
@@ -49,7 +50,8 @@ public class CgenModule implements Module{
         contributeUserProperties(binder)
                 .add(NumericPropertyDescriptorCreator.class)
                 .add(DatePropertyDescriptorCreator.class)
-                .add(StringPropertyDescriptorCreator.class);
+                .add(StringPropertyDescriptorCreator.class)
+                .add(EmbeddablePropertyDescriptorCreator.class);
     }
 
     public static ListBuilder<PropertyDescriptorCreator> contributeUserProperties(Binder binder) {
diff --git a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ImportUtils.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ImportUtils.java
index ccd72f7..24d8b91 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ImportUtils.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ImportUtils.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.gen;
 
 import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.EmbeddableAttribute;
 import org.apache.cayenne.map.ObjAttribute;
 import org.apache.cayenne.util.Util;
 
@@ -222,6 +223,10 @@ public class ImportUtils {
         return attribute.isMandatory() && isPrimitive(attribute.getType());
     }
 
+	public boolean canUsePrimitive(EmbeddableAttribute attribute) {
+		return isPrimitive(attribute.getType());
+	}
+
 	/**
 	 * Generate package and list of import statements based on the registered
 	 * types.
diff --git a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/PropertyUtils.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/PropertyUtils.java
index dfef034..08277e1 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/PropertyUtils.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/PropertyUtils.java
@@ -27,6 +27,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 
+import org.apache.cayenne.EmbeddableObject;
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.di.AdhocObjectFactory;
@@ -34,6 +35,7 @@ import org.apache.cayenne.di.DIRuntimeException;
 import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.BaseProperty;
 import org.apache.cayenne.exp.property.DateProperty;
+import org.apache.cayenne.exp.property.EmbeddableProperty;
 import org.apache.cayenne.exp.property.EntityProperty;
 import org.apache.cayenne.exp.property.ListProperty;
 import org.apache.cayenne.exp.property.MapProperty;
@@ -69,6 +71,7 @@ public class PropertyUtils {
         FACTORY_METHODS.put(ListProperty.class.getName(), "createList");
         FACTORY_METHODS.put(SetProperty.class.getName(), "createSet");
         FACTORY_METHODS.put(MapProperty.class.getName(), "createMap");
+        FACTORY_METHODS.put(EmbeddableProperty.class.getName(), "createEmbeddable");
     }
 
     private static final List<Class<?>> JAVA_DATE_TYPES = Arrays.asList(
@@ -124,23 +127,21 @@ public class PropertyUtils {
     }
 
     public void addImport(ObjAttribute attribute) throws ClassNotFoundException {
-        if(attribute instanceof EmbeddedAttribute) {
-            addImport((EmbeddedAttribute)attribute);
-            return;
-        }
         importUtils.addType(PropertyFactory.class.getName());
         importUtils.addType(attribute.getType());
         importUtils.addType(getPropertyDescriptor(attribute.getType()).getPropertyType());
     }
 
     public void addImport(EmbeddedAttribute attribute) throws ClassNotFoundException {
-        Embeddable embeddable = attribute.getEmbeddable();
-        importUtils.addType(embeddable.getClassName());
-        for(EmbeddableAttribute embeddableAttribute : embeddable.getAttributes()) {
-            importUtils.addType(embeddableAttribute.getType());
-            importUtils.addType(getPropertyDescriptor(embeddableAttribute.getType()).getPropertyType());
-            importUtils.addType(ExpressionFactory.class.getName());
-        }
+        importUtils.addType(PropertyFactory.class.getName());
+        importUtils.addType(attribute.getType());
+        importUtils.addType(getPropertyDescriptor(EmbeddableObject.class.getName()).getPropertyType());
+    }
+
+    public void addImport(EmbeddableAttribute attribute) throws ClassNotFoundException {
+        importUtils.addType(PropertyFactory.class.getName());
+        importUtils.addType(attribute.getType());
+        importUtils.addType(getPropertyDescriptor(attribute.getType()).getPropertyType());
     }
 
     public void addImport(ObjRelationship relationship) {
@@ -182,10 +183,6 @@ public class PropertyUtils {
     }
 
     public String propertyDefinition(ObjAttribute attribute, boolean client) throws ClassNotFoundException {
-        if(attribute instanceof EmbeddedAttribute) {
-            return propertyDefinition((EmbeddedAttribute)attribute);
-        }
-
         StringUtils utils = StringUtils.getInstance();
         String attributeType = utils.stripGeneric(importUtils.formatJavaType(attribute.getType(), false));
         PropertyDescriptor propertyDescriptor = getPropertyDescriptor(attribute.getType());
@@ -216,31 +213,41 @@ public class PropertyUtils {
 
     public String propertyDefinition(EmbeddedAttribute attribute) throws ClassNotFoundException {
         StringUtils utils = StringUtils.getInstance();
+        String attributeType = utils.stripGeneric(importUtils.formatJavaType(attribute.getType(), false));
+        PropertyDescriptor propertyDescriptor = getPropertyDescriptor(EmbeddableObject.class.getName());
+        return String.format("public static final %s<%s> %s = %s(\"%s\", %s.class);",
+                importUtils.formatJavaType(propertyDescriptor.getPropertyType()),
+                attributeType,
+                generatePropertyName(attribute),
+                propertyDescriptor.getPropertyFactoryMethod(),
+                attribute.getName(),
+                attributeType
+        );
+    }
+
+    public String propertyDefinition(EmbeddableAttribute attribute) throws ClassNotFoundException {
+        StringUtils utils = StringUtils.getInstance();
+        String attributeType = utils.stripGeneric(importUtils.formatJavaType(attribute.getType(), false));
+        PropertyDescriptor propertyDescriptor = getPropertyDescriptor(attribute.getType());
+        return String.format("public static final %s<%s> %s = %s(\"%s\", %s.class);",
+                importUtils.formatJavaType(propertyDescriptor.getPropertyType()),
+                attributeType,
+                generatePropertyName(attribute),
+                propertyDescriptor.getPropertyFactoryMethod(),
+                attribute.getName(),
+                attributeType
+        );
+    }
 
+    protected String generatePropertyName(EmbeddableAttribute attribute) {
+        StringUtils utils = StringUtils.getInstance();
         Embeddable embeddable = attribute.getEmbeddable();
-        Collection<EmbeddableAttribute> attributes = embeddable.getAttributes();
-
-        String[] attributesDefinitions = new String[attributes.size()];
-        int i = 0;
-        for(EmbeddableAttribute embeddableAttribute : attributes) {
-            PropertyDescriptor propertyDescriptor = getPropertyDescriptor(embeddableAttribute.getType());
-            String attributeType = utils.stripGeneric(importUtils.formatJavaType(embeddableAttribute.getType(), false));
-            String path = attribute.getAttributeOverrides()
-                    .getOrDefault(embeddableAttribute.getName(), embeddableAttribute.getDbAttributeName());
-
-            String propertyName = utils.capitalizedAsConstant(attribute.getName()) + "_" + utils.capitalizedAsConstant(embeddableAttribute.getName());
-            attributesDefinitions[i++] =  String.format("public static final %s<%s> %s " +
-                            "= %s(ExpressionFactory.dbPathExp(\"%s\"), %s.class);",
-                    importUtils.formatJavaType(propertyDescriptor.getPropertyType()),
-                    attributeType,
-                    propertyName,
-                    propertyDescriptor.getPropertyFactoryMethod(),
-                    path,
-                    attributeType
-            );
+        String name = utils.capitalizedAsConstant(attribute.getName());
+        // ensure that final name is unique
+        while(embeddable.getAttribute(name) != null) {
+            name = name + DUPLICATE_NAME_SUFFIX;
         }
-
-        return String.join("\n    ", attributesDefinitions);
+        return name;
     }
 
     public String propertyDefinition(ObjRelationship relationship, boolean client) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/property/EmbeddablePropertyDescriptorCreator.java
similarity index 57%
copy from cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
copy to cayenne-cgen/src/main/java/org/apache/cayenne/gen/property/EmbeddablePropertyDescriptorCreator.java
index ef2ab3f..93b62bc 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/property/EmbeddablePropertyDescriptorCreator.java
@@ -7,7 +7,7 @@
  *  "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
+ *    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
@@ -17,35 +17,26 @@
  *  under the License.
  ****************************************************************/
 
-package org.apache.cayenne.access.translator.select;
+package org.apache.cayenne.gen.property;
 
-import java.util.List;
 import java.util.Optional;
 
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.EmbeddableObject;
+import org.apache.cayenne.exp.property.EmbeddableProperty;
+import org.apache.cayenne.gen.PropertyDescriptor;
 
 /**
- * Interface that describes result of path translation
- *
  * @since 4.2
  */
-interface PathTranslationResult {
-
-    String getFinalPath();
-
-    Optional<DbRelationship> getDbRelationship();
-
-    List<DbAttribute> getDbAttributes();
+public class EmbeddablePropertyDescriptorCreator implements PropertyDescriptorCreator {
 
-    List<String> getAttributePaths();
+    private static final String FACTORY_METHOD = "PropertyFactory.createEmbeddable";
 
-    default DbAttribute getLastAttribute() {
-        return getDbAttributes().get(getDbAttributes().size() - 1);
+    @Override
+    public Optional<PropertyDescriptor> apply(Class<?> aClass) {
+        if(EmbeddableObject.class.isAssignableFrom(aClass)) {
+            return Optional.of(new PropertyDescriptor(EmbeddableProperty.class.getName(), FACTORY_METHOD));
+        }
+        return Optional.empty();
     }
-
-    default String getLastAttributePath() {
-        return getAttributePaths().get(getAttributePaths().size() - 1);
-    }
-
 }
diff --git a/cayenne-cgen/src/main/resources/templates/v4_1/embeddable-singleclass.vm b/cayenne-cgen/src/main/resources/templates/v4_1/embeddable-singleclass.vm
index 336cb86..6ffb578 100644
--- a/cayenne-cgen/src/main/resources/templates/v4_1/embeddable-singleclass.vm
+++ b/cayenne-cgen/src/main/resources/templates/v4_1/embeddable-singleclass.vm
@@ -5,7 +5,7 @@
 ##  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
-##
+##x
 ##    https://www.apache.org/licenses/LICENSE-2.0
 ##
 ##  Unless required by applicable law or agreed to in writing,
@@ -37,23 +37,30 @@
 ${importUtils.setPackage($subPackageName)}##
 ${importUtils.addType("java.io.Serializable")}##
 ${importUtils.addReservedType("${subPackageName}.${subClassName}")}##
-${importUtils.addType("${basePackageName}.${baseClassName}")}##
+${importUtils.addType("org.apache.cayenne.EmbeddableObject")}##
 ${importUtils.addType("org.apache.cayenne.Persistent")}##
 #foreach( $attr in ${object.Attributes} )
-$importUtils.addType(${attr.Type})##
+$propertyUtils.addImport($attr)##
 #end
 ${importUtils.generate()}
 
-##  extends ${baseClassName} - always Object, so skip it
-public abstract class ${subClassName} implements Serializable {
-
+## extends ${baseClassName} - always Object, so skip it
+public class ${subClassName} implements EmbeddableObject, Serializable {
 ## Create property names
 #if( $createPropertyNames )
+
 #foreach( $attr in ${object.Attributes} )
     public static final String ${stringUtils.capitalizedAsConstant($attr.Name)}_PROPERTY = "${attr.Name}";
 #end
+#end
+#######################
+## Create Properties ##
+#######################
 
+#foreach( $attr in ${object.Attributes} )
+    $propertyUtils.propertyDefinition($attr)
 #end
+
     // special properties injected by Cayenne
     private Persistent owner;
     private String embeddedProperty;
@@ -96,4 +103,49 @@ public abstract class ${subClassName} implements Serializable {
 #end
 
 #end
+###########################################################
+## Create writePropertyDirect/readPropertyDirect methods ##
+###########################################################
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+#foreach( $attr in ${object.Attributes} )
+#set ( $name = "$stringUtils.formatVariableName(${attr.Name})")
+            case "${attr.Name}":
+                return this.${name};
+#end
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+#foreach( $attr in ${object.Attributes} )
+#set ( $name = "$stringUtils.formatVariableName(${attr.Name})")
+#set ( $flag = $importUtils.canUsePrimitive($attr) )
+#set ( $type = "$importUtils.formatJavaType(${attr.Type}, $flag)")
+            case "${attr.Name}":
+#if ( $importUtils.isBoolean($type) )
+                this.${name} = val == null ? false : ($type)val;
+#elseif ($importUtils.isPrimitive($type))
+                this.${name} = val == null ? 0 : ($type)val;
+#else
+                this.${name} = ($type)val;
+#end
+                break;
+#end
+            default:
+                throw new IllegalArgumentException("Unknown property: " + propName);
+        }
+    }
 }
diff --git a/cayenne-cgen/src/main/resources/templates/v4_1/embeddable-superclass.vm b/cayenne-cgen/src/main/resources/templates/v4_1/embeddable-superclass.vm
index 573e697..c040c3e 100644
--- a/cayenne-cgen/src/main/resources/templates/v4_1/embeddable-superclass.vm
+++ b/cayenne-cgen/src/main/resources/templates/v4_1/embeddable-superclass.vm
@@ -37,10 +37,10 @@
 ${importUtils.setPackage($superPackageName)}##
 ${importUtils.addType("java.io.Serializable")}##
 ${importUtils.addReservedType("${superPackageName}.${superClassName}")}##
-${importUtils.addType("${basePackageName}.${baseClassName}")}##
+${importUtils.addType("org.apache.cayenne.EmbeddableObject")}##
 ${importUtils.addType("org.apache.cayenne.Persistent")}##
 #foreach( $attr in ${object.Attributes} )
-$importUtils.addType(${attr.Type})##
+$propertyUtils.addImport($attr)##
 #end
 ${importUtils.generate()}
 
@@ -51,15 +51,22 @@ ${importUtils.generate()}
  * If you need to make any customizations, please use subclass. 
  */
 ## extends ${baseClassName} - always Object, so skip it
-public abstract class ${superClassName} implements Serializable {
-
+public abstract class ${superClassName} implements EmbeddableObject, Serializable {
 ## Create property names
 #if( $createPropertyNames )
+
 #foreach( $attr in ${object.Attributes} )
     public static final String ${stringUtils.capitalizedAsConstant($attr.Name)}_PROPERTY = "${attr.Name}";
 #end
+#end
+#######################
+## Create Properties ##
+#######################
 
+#foreach( $attr in ${object.Attributes} )
+    $propertyUtils.propertyDefinition($attr)
 #end
+
     // special properties injected by Cayenne
     private Persistent owner;
     private String embeddedProperty;
@@ -102,4 +109,49 @@ public abstract class ${superClassName} implements Serializable {
 #end
 
 #end
+###########################################################
+## Create writePropertyDirect/readPropertyDirect methods ##
+###########################################################
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+#foreach( $attr in ${object.Attributes} )
+#set ( $name = "$stringUtils.formatVariableName(${attr.Name})")
+            case "${attr.Name}":
+                return this.${name};
+#end
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+#foreach( $attr in ${object.Attributes} )
+#set ( $name = "$stringUtils.formatVariableName(${attr.Name})")
+#set ( $flag = $importUtils.canUsePrimitive($attr) )
+#set ( $type = "$importUtils.formatJavaType(${attr.Type}, $flag)")
+            case "${attr.Name}":
+#if ( $importUtils.isBoolean($type) )
+                this.${name} = val == null ? false : ($type)val;
+#elseif ($importUtils.isPrimitive($type))
+                this.${name} = val == null ? 0 : ($type)val;
+#else
+                this.${name} = ($type)val;
+#end
+                break;
+#end
+            default:
+                throw new IllegalArgumentException("Unknown property: " + propName);
+        }
+    }
 }
diff --git a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/PropertyUtilsTest.java b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/PropertyUtilsTest.java
index 7b75365..d5920a6 100644
--- a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/PropertyUtilsTest.java
+++ b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/PropertyUtilsTest.java
@@ -27,6 +27,7 @@ import org.apache.cayenne.configuration.server.ServerModule;
 import org.apache.cayenne.di.DIBootstrap;
 import org.apache.cayenne.di.spi.DefaultScope;
 import org.apache.cayenne.exp.property.DateProperty;
+import org.apache.cayenne.exp.property.EmbeddableProperty;
 import org.apache.cayenne.exp.property.EntityProperty;
 import org.apache.cayenne.exp.property.ListProperty;
 import org.apache.cayenne.exp.property.MapProperty;
@@ -203,14 +204,14 @@ public class PropertyUtilsTest {
 
         dataMap.addEmbeddable(embeddable);
 
-        String definition = propertyUtils.propertyDefinition(embeddedAttribute);
-        assertEquals("public static final StringProperty<String> A_TEST_EMB_ATTR = PropertyFactory.createString(ExpressionFactory.dbPathExp(\"testPath\"), String.class);",
+        String definition = propertyUtils.propertyDefinition(attribute);
+        assertEquals("public static final StringProperty<String> TEST_EMB_ATTR = PropertyFactory.createString(\"testEmbAttr\", String.class);",
                 definition);
     }
 
     @Test
     public void simpleNumericPropertyEmbDefinition() throws ClassNotFoundException {
-        importUtils.addType(NumericProperty.class.getName());
+        importUtils.addType(EmbeddableProperty.class.getName());
 
         DataMap dataMap = new DataMap();
 
@@ -234,7 +235,7 @@ public class PropertyUtilsTest {
         dataMap.addEmbeddable(embeddable);
 
         String definition = propertyUtils.propertyDefinition(embeddedAttribute);
-        assertEquals("public static final NumericProperty<Integer> A_TEST_EMB_ATTR = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp(\"testPath\"), Integer.class);",
+        assertEquals("public static final EmbeddableProperty<test> A = PropertyFactory.createEmbeddable(\"a\", test.class);",
                 definition);
     }
 
@@ -264,8 +265,8 @@ public class PropertyUtilsTest {
 
         dataMap.addEmbeddable(embeddable);
 
-        String definition = propertyUtils.propertyDefinition(embeddedAttribute);
-        assertEquals("public static final CustomProperty<TimestampType> A_TEST_EMB_ATTR = new CustomProperty(ExpressionFactory.dbPathExp(\"testPath\"), TimestampType.class);",
+        String definition = propertyUtils.propertyDefinition(attribute);
+        assertEquals("public static final CustomProperty<TimestampType> TEST_EMB_ATTR = new CustomProperty(\"testEmbAttr\", TimestampType.class);",
                 definition);
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java b/cayenne-server/src/main/java/org/apache/cayenne/EmbeddableObject.java
similarity index 55%
copy from cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
copy to cayenne-server/src/main/java/org/apache/cayenne/EmbeddableObject.java
index ef2ab3f..c098b99 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/EmbeddableObject.java
@@ -7,7 +7,7 @@
  *  "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
+ *    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
@@ -17,35 +17,27 @@
  *  under the License.
  ****************************************************************/
 
-package org.apache.cayenne.access.translator.select;
-
-import java.util.List;
-import java.util.Optional;
-
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbRelationship;
+package org.apache.cayenne;
 
 /**
- * Interface that describes result of path translation
  *
  * @since 4.2
  */
-interface PathTranslationResult {
-
-    String getFinalPath();
-
-    Optional<DbRelationship> getDbRelationship();
-
-    List<DbAttribute> getDbAttributes();
-
-    List<String> getAttributePaths();
-
-    default DbAttribute getLastAttribute() {
-        return getDbAttributes().get(getDbAttributes().size() - 1);
-    }
-
-    default String getLastAttributePath() {
-        return getAttributePaths().get(getAttributePaths().size() - 1);
-    }
+public interface EmbeddableObject {
+
+    /**
+     * Modifies a value of a named property without altering the object state in any way,
+     * and without triggering any database operations. This method is intended mostly for
+     * internal use by Cayenne framework, and shouldn't be called from the application
+     * code.
+     */
+    void writePropertyDirectly(String propertyName, Object val);
+
+    /**
+     * Returns mapped property value as currently stored in the DataObject. Returned value
+     * maybe a fault or a real value. This method will not attempt to resolve faults, or
+     * to read unmapped properties.
+     */
+    Object readPropertyDirectly(String propertyName);
 
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
index dba1041..8697160 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.access;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.DataRow;
+import org.apache.cayenne.EmbeddableObject;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
@@ -31,9 +32,11 @@ import org.apache.cayenne.cache.QueryCacheEntryFactory;
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.Embeddable;
 import org.apache.cayenne.map.EntityInheritanceTree;
 import org.apache.cayenne.map.LifecycleEvent;
 import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.query.EmbeddableResultSegment;
 import org.apache.cayenne.query.EntityResultSegment;
 import org.apache.cayenne.query.ObjectIdQuery;
 import org.apache.cayenne.query.PrefetchSelectQuery;
@@ -483,6 +486,8 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
                         if (metadata.isSingleResultSetMapping()) {
                             if (rsMapping.get(0) instanceof EntityResultSegment) {
                                 converter = new SingleObjectConversionStrategy();
+                        } else if(rsMapping.get(0) instanceof EmbeddableResultSegment) {
+                            converter = new SingleEmbeddableConversionStrategy();
                             } else {
                                 converter = new SingleScalarConversionStrategy();
                             }
@@ -705,6 +710,33 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
         }
     }
 
+    class SingleEmbeddableConversionStrategy extends ObjectConversionStrategy<DataRow> {
+
+        @Override
+        void convert(List<DataRow> mainRows) {
+            EmbeddableResultSegment resultSegment = (EmbeddableResultSegment)metadata.getResultSetMapping().get(0);
+            Embeddable embeddable = resultSegment.getEmbeddable();
+            Class<?> embeddableClass;
+            try {
+                embeddableClass = Class.forName(embeddable.getClassName());
+            } catch (Exception e) {
+                throw new CayenneRuntimeException("Unable create Embeddable class %s", e, embeddable.getClassName());
+            }
+            List<EmbeddableObject> result = new ArrayList<>(mainRows.size());
+            mainRows.forEach(dataRow -> {
+                EmbeddableObject eo;
+                try {
+                    eo = (EmbeddableObject)embeddableClass.newInstance();
+                } catch (InstantiationException | IllegalAccessException e) {
+                    throw new CayenneRuntimeException("Unable create instance of Embeddable %s", e, embeddable.getClassName());
+                }
+                dataRow.forEach(eo::writePropertyDirectly);
+                result.add(eo);
+            });
+            updateResponse(mainRows, result);
+        }
+    }
+
     class MixedConversionStrategy extends ObjectConversionStrategy<Object[]> {
 
         protected PrefetchProcessorNode toResultsTree(ClassDescriptor descriptor, PrefetchTreeNode prefetchTree,
@@ -748,9 +780,8 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
             List<Object> rsMapping = metadata.getResultSetMapping();
             int width = rsMapping.size();
 
-            // no conversions needed for scalar positions; reuse Object[]'s to
-            // fill them
-            // with resolved objects
+            // no conversions needed for scalar positions;
+            // reuse Object[]'s to fill them with resolved objects
             List<PrefetchProcessorNode> segmentNodes = new ArrayList<>(width);
             for (int i = 0; i < width; i++) {
                 Object mapping = rsMapping.get(i);
@@ -767,6 +798,20 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
                         Object[] row = mainRows.get(j);
                         row[i] = objects.get(j);
                     }
+                } else if (mapping instanceof EmbeddableResultSegment) {
+                    EmbeddableResultSegment resultSegment = (EmbeddableResultSegment)mapping;
+                    Embeddable embeddable = resultSegment.getEmbeddable();
+                    try {
+                        Class<?> embeddableClass = Class.forName(embeddable.getClassName());
+                        for(Object[] row : mainRows) {
+                            DataRow dataRow = (DataRow)row[i];
+                            EmbeddableObject eo = (EmbeddableObject)embeddableClass.newInstance();
+                            dataRow.forEach(eo::writePropertyDirectly);
+                            row[i] = eo;
+                        }
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                    }
                 }
             }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
index db41752..4394a08 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
@@ -35,6 +35,7 @@ import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.map.Entity;
 import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.query.EmbeddableResultSegment;
 import org.apache.cayenne.query.EntityResultSegment;
 import org.apache.cayenne.query.QueryMetadata;
 import org.apache.cayenne.query.ScalarResultSegment;
@@ -69,6 +70,8 @@ public class DefaultRowReaderFactory implements RowReaderFactory {
 			if (segment instanceof EntityResultSegment) {
 				return createEntityRowReader(descriptor, queryMetadata, (EntityResultSegment) segment,
 						postProcessorFactory);
+			} else if (segment instanceof EmbeddableResultSegment) {
+				return createEmbeddableRowReader(descriptor, queryMetadata, (EmbeddableResultSegment) segment);
 			} else {
 				return createScalarRowReader(descriptor, queryMetadata, (ScalarResultSegment) segment);
 			}
@@ -83,6 +86,8 @@ public class DefaultRowReaderFactory implements RowReaderFactory {
 							i,
 							createEntityRowReader(descriptor, queryMetadata, (EntityResultSegment) segment,
 									postProcessorFactory));
+				} else if(segment instanceof EmbeddableResultSegment) {
+					reader.addRowReader(i, createEmbeddableRowReader(descriptor, queryMetadata, (EmbeddableResultSegment) segment));
 				} else {
 					reader.addRowReader(i, createScalarRowReader(descriptor, queryMetadata, (ScalarResultSegment) segment));
 				}
@@ -92,8 +97,12 @@ public class DefaultRowReaderFactory implements RowReaderFactory {
 		}
 	}
 
+	private RowReader<?> createEmbeddableRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, EmbeddableResultSegment segment) {
+		return new EmbeddableRowReader(descriptor, queryMetadata, segment);
+	}
+
 	protected RowReader<?> createScalarRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, ScalarResultSegment segment) {
-		return new ScalarRowReader<Object>(descriptor, segment);
+		return new ScalarRowReader<>(descriptor, segment);
 	}
 
 	protected RowReader<?> createEntityRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata,
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/EmbeddableRowReader.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/EmbeddableRowReader.java
new file mode 100644
index 0000000..d9056e5
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/EmbeddableRowReader.java
@@ -0,0 +1,79 @@
+/*****************************************************************
+ *   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.access.jdbc.reader;
+
+import java.sql.ResultSet;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
+import org.apache.cayenne.access.jdbc.RowDescriptor;
+import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.query.EmbeddableResultSegment;
+import org.apache.cayenne.query.QueryMetadata;
+import org.apache.cayenne.util.Util;
+
+/**
+ * @since 4.2
+ */
+class EmbeddableRowReader implements RowReader<DataRow> {
+
+    private final int startIndex;
+    private final int mapCapacity;
+    private final ExtendedType[] converters;
+    private final String[] labels;
+    private final int[] types;
+
+    EmbeddableRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, EmbeddableResultSegment segment) {
+        int segmentWidth = segment.getFields().size();
+        this.startIndex = segment.getColumnOffset();
+        this.converters = new ExtendedType[segmentWidth];
+        this.types = new int[segmentWidth];
+        this.labels = new String[segmentWidth];
+
+        ExtendedType[] converters = descriptor.getConverters();
+        ColumnDescriptor[] columns = descriptor.getColumns();
+        for (int i = 0; i < segmentWidth; i++) {
+            this.converters[i] = converters[startIndex + i];
+            types[i] = columns[startIndex + i].getJdbcType();
+            labels[i] = segment.getFields().get(columns[startIndex +i].getName());
+        }
+        this.mapCapacity = (int) Math.ceil(segmentWidth / 0.75);
+    }
+
+    @Override
+    public DataRow readRow(ResultSet resultSet) {
+        try {
+            DataRow row = new DataRow(mapCapacity);
+            int len = converters.length;
+            for (int i = 0; i < len; i++) {
+                // note: jdbc column indexes start from 1, not 0 as in arrays
+                Object val = converters[i].materializeObject(resultSet, startIndex + i + 1, types[i]);
+                row.put(labels[i], val);
+            }
+            return row;
+        } catch (CayenneRuntimeException cex) {
+            // rethrow unmodified
+            throw cex;
+        } catch (Exception otherex) {
+            throw new CayenneRuntimeException("Exception materializing id column.", Util.unwindException(otherex));
+        }
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractor.java
index 818731a..1519b60 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/CustomColumnSetExtractor.java
@@ -23,11 +23,15 @@ import java.util.Collection;
 import java.util.Map;
 
 import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.EmbeddableObject;
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.parser.ASTDbPath;
 import org.apache.cayenne.exp.property.BaseProperty;
+import org.apache.cayenne.map.EmbeddedAttribute;
+import org.apache.cayenne.map.EmbeddedResult;
 import org.apache.cayenne.map.JoinType;
 import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.reflect.ClassDescriptor;
@@ -51,6 +55,8 @@ class CustomColumnSetExtractor implements ColumnExtractor {
         for (BaseProperty<?> property : columns) {
             if (isFullObjectProp(property)) {
                 extractFullObject(prefix, property);
+            } else if(isEmbeddedProp(property)) {
+                extractEmbeddedObject(prefix, property);
             } else {
                 extractSimpleProperty(property);
             }
@@ -82,6 +88,25 @@ class CustomColumnSetExtractor implements ColumnExtractor {
                     && Persistent.class.isAssignableFrom(property.getType()));
     }
 
+    private boolean isEmbeddedProp(BaseProperty<?> property) {
+        return EmbeddableObject.class.isAssignableFrom(property.getType());
+    }
+
+    private void extractEmbeddedObject(String prefix, BaseProperty<?> property) {
+        Object o = property.getExpression().evaluate(context.getMetadata().getObjEntity());
+        if(!(o instanceof EmbeddedAttribute)) {
+            throw new CayenneRuntimeException("EmbeddedAttribute expected, %s found", o);
+        }
+        EmbeddedAttribute attribute = (EmbeddedAttribute) o;
+        EmbeddedResult result = new EmbeddedResult(attribute.getEmbeddable(), attribute.getAttributes().size());
+        attribute.getAttributes().forEach(attr -> {
+            Node sqlNode = context.getQualifierTranslator().translate(ExpressionFactory.dbPathExp(attr.getDbAttributePath()));
+            context.addResultNode(sqlNode, true, null, null);
+            result.addAttribute(attr);
+        });
+        context.getSqlResult().addEmbeddedResult(result);
+    }
+
     private void extractFullObject(String prefix, BaseProperty<?> property) {
         prefix = calculatePrefix(prefix, property);
         ensureJoin(prefix);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjPathProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjPathProcessor.java
index 5775a5c..e01ec36 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjPathProcessor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ObjPathProcessor.java
@@ -19,7 +19,10 @@
 
 package org.apache.cayenne.access.translator.select;
 
+import java.util.Optional;
+
 import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.Embeddable;
 import org.apache.cayenne.map.EmbeddedAttribute;
 import org.apache.cayenne.map.JoinType;
 import org.apache.cayenne.map.ObjAttribute;
@@ -86,6 +89,13 @@ class ObjPathProcessor extends PathProcessor<ObjEntity> {
     protected void processAttribute(ObjAttribute attribute) {
         if(attribute instanceof EmbeddedAttribute) {
             embeddedAttribute = (EmbeddedAttribute)attribute;
+            if(lastComponent) {
+                embeddedAttribute.getAttributes().forEach(a -> {
+                    int len = currentDbPath.length();
+                    processAttribute(a);
+                    currentDbPath.delete(len, currentDbPath.length());
+                });
+            }
             return;
         }
 
@@ -132,4 +142,11 @@ class ObjPathProcessor extends PathProcessor<ObjEntity> {
         currentDbPath.append(result.getFinalPath());
     }
 
+    @Override
+    public Optional<Embeddable> getEmbeddable() {
+        if(embeddedAttribute != null) {
+            return Optional.of(embeddedAttribute.getEmbeddable());
+        }
+        return Optional.empty();
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathProcessor.java
index 18ec894..425c570 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathProcessor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathProcessor.java
@@ -27,6 +27,7 @@ import java.util.Optional;
 
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.Embeddable;
 import org.apache.cayenne.map.Entity;
 
 /**
@@ -110,6 +111,11 @@ abstract class PathProcessor<T extends Entity> implements PathTranslationResult
     }
 
     @Override
+    public Optional<Embeddable> getEmbeddable() {
+        return Optional.empty();
+    }
+
+    @Override
     public String getFinalPath() {
         return currentDbPath.toString();
     }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
index ef2ab3f..fe4968b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
@@ -24,6 +24,7 @@ import java.util.Optional;
 
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.Embeddable;
 
 /**
  * Interface that describes result of path translation
@@ -36,6 +37,8 @@ interface PathTranslationResult {
 
     Optional<DbRelationship> getDbRelationship();
 
+    Optional<Embeddable> getEmbeddable();
+
     List<DbAttribute> getDbAttributes();
 
     List<String> getAttributePaths();
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
index 2311f0f..c199f49 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
@@ -22,11 +22,13 @@ package org.apache.cayenne.access.translator.select;
 import java.util.ArrayDeque;
 import java.util.Collection;
 import java.util.Deque;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
 import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.EmbeddableObject;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.access.sqlbuilder.ExpressionNodeBuilder;
@@ -56,6 +58,7 @@ import org.apache.cayenne.exp.property.BaseProperty;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.Embeddable;
 
 import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.aliased;
 import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.function;
@@ -238,7 +241,9 @@ class QualifierTranslator implements TraversalHandler {
     }
 
     private Node processPathTranslationResult(Expression node, Expression parentNode, PathTranslationResult result) {
-        if(result.getDbRelationship().isPresent()
+        if(result.getEmbeddable().isPresent()) {
+            return createEmbeddableMatch(node, parentNode, result);
+        } else if(result.getDbRelationship().isPresent()
                 && result.getDbAttributes().size() > 1
                 && result.getDbRelationship().get().getTargetEntity().getPrimaryKeys().size() > 1) {
             return createMultiAttributeMatch(node, parentNode, result);
@@ -250,6 +255,39 @@ class QualifierTranslator implements TraversalHandler {
         }
     }
 
+    private Node createEmbeddableMatch(Expression node, Expression parentNode, PathTranslationResult result) {
+        Embeddable embeddable = result.getEmbeddable()
+                .orElseThrow(() -> new CayenneRuntimeException("Incorrect path '%s' translation, embeddable expected"
+                        , result.getFinalPath()));
+
+        Map<String, Object> valueSnapshot = getEmbeddableValueSnapshot(embeddable, node, parentNode);
+
+        expressionsToSkip.add(node);
+        expressionsToSkip.add(parentNode);
+
+        return buildMultiValueComparision(result, valueSnapshot);
+    }
+
+    private Map<String, Object> getEmbeddableValueSnapshot(Embeddable embeddable, Expression node, Expression parentNode) {
+        int siblings = parentNode.getOperandCount();
+        for(int i=0; i<siblings; i++) {
+            Object operand = parentNode.getOperand(i);
+            if(node == operand) {
+                continue;
+            }
+
+            if(operand instanceof EmbeddableObject) {
+                EmbeddableObject embeddableObject = (EmbeddableObject)operand;
+                Map<String, Object> snapshot = new HashMap<>(embeddable.getAttributes().size());
+                embeddable.getAttributeMap().forEach((name, attr) ->
+                        snapshot.put(attr.getDbAttributeName(), embeddableObject.readPropertyDirectly(name)));
+                return snapshot;
+            }
+        }
+
+        throw new CayenneRuntimeException("Embeddable attribute ObjPath isn't matched with valid value.");
+    }
+
     private Node createMultiAttributeMatch(Expression node, Expression parentNode, PathTranslationResult result) {
         DbRelationship relationship = result.getDbRelationship()
                 .orElseThrow(() -> new CayenneRuntimeException("Incorrect path '%s' translation, relationship expected"
@@ -262,37 +300,16 @@ class QualifierTranslator implements TraversalHandler {
                     , relationship.getSourceEntityName(), relationship.getName());
         }
 
-        expressionsToSkip.add(node);
-        expressionsToSkip.add(parentNode);
-
         Map<String, Object> valueSnapshot = getMultiAttributeValueSnapshot(node, parentNode);
-        if(valueSnapshot == null) {
-            throw new CayenneRuntimeException("Multi attribute ObjPath isn't matched with valid value. " +
-                    "List or Persistent object required.");
-        }
-
-        ExpressionNodeBuilder expressionNodeBuilder = null;
-        ExpressionNodeBuilder eq;
-
-        String path = result.getLastAttributePath();
-        String alias = context.getTableTree().aliasForPath(path);
-
         // convert snapshot if we have attributes from source, not target
         if(result.getLastAttribute().getEntity() == relationship.getSourceEntity()) {
             valueSnapshot = relationship.srcFkSnapshotWithTargetSnapshot(valueSnapshot);
         }
 
-        for(DbAttribute attribute : result.getDbAttributes()) {
-            Object nextValue = valueSnapshot.get(attribute.getName());
-            eq = table(alias).column(attribute).eq(value(nextValue));
-            if (expressionNodeBuilder == null) {
-                expressionNodeBuilder = eq;
-            } else {
-                expressionNodeBuilder = expressionNodeBuilder.and(eq);
-            }
-        }
+        expressionsToSkip.add(node);
+        expressionsToSkip.add(parentNode);
 
-        return expressionNodeBuilder.build();
+        return buildMultiValueComparision(result, valueSnapshot);
     }
 
     private Map<String, Object> getMultiAttributeValueSnapshot(Expression node, Expression parentNode) {
@@ -313,7 +330,28 @@ class QualifierTranslator implements TraversalHandler {
             }
         }
 
-        return null;
+        throw new CayenneRuntimeException("Multi attribute ObjPath isn't matched with valid value. " +
+                "List or Persistent object required.");
+    }
+
+    private Node buildMultiValueComparision(PathTranslationResult result, Map<String, Object> valueSnapshot) {
+        ExpressionNodeBuilder expressionNodeBuilder = null;
+        ExpressionNodeBuilder eq;
+
+        String path = result.getLastAttributePath();
+        String alias = context.getTableTree().aliasForPath(path);
+
+        for (DbAttribute attribute : result.getDbAttributes()) {
+            Object nextValue = valueSnapshot.get(attribute.getName());
+            eq = table(alias).column(attribute).eq(value(nextValue));
+            if (expressionNodeBuilder == null) {
+                expressionNodeBuilder = eq;
+            } else {
+                expressionNodeBuilder = expressionNodeBuilder.and(eq);
+            }
+        }
+
+        return expressionNodeBuilder.build();
     }
 
     private boolean nodeProcessed(Expression node) {
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity1.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/EmbeddableProperty.java
similarity index 56%
copy from cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity1.java
copy to cayenne-server/src/main/java/org/apache/cayenne/exp/property/EmbeddableProperty.java
index 119a0ce..cb5c564 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity1.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/EmbeddableProperty.java
@@ -7,7 +7,7 @@
  *  "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
+ *    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
@@ -16,13 +16,23 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
-package org.apache.cayenne.testdo.embeddable;
 
-import org.apache.cayenne.testdo.embeddable.auto._EmbedEntity1;
+package org.apache.cayenne.exp.property;
 
-public class EmbedEntity1 extends _EmbedEntity1 {
+/**
+ * Property that represents object attribute mapped on {@link org.apache.cayenne.map.Embeddable} object.
+ * @since 4.2
+ */
+public class EmbeddableProperty<E> extends BaseProperty<E> implements PathProperty<E> {
 
+    /**
+     * Constructs a new property with the given name and type
+     *
+     * @param name       of the property (will be used as alias for the expression)
+     * @param type       of the property
+     * @see PropertyFactory#createEmbeddable(String, Class)
+     */
+    protected EmbeddableProperty(String name, Class<? super E> type) {
+        super(name, null, type);
+    }
 }
-
-
-
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/RelationshipProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/PathProperty.java
similarity index 71%
copy from cayenne-server/src/main/java/org/apache/cayenne/exp/property/RelationshipProperty.java
copy to cayenne-server/src/main/java/org/apache/cayenne/exp/property/PathProperty.java
index 5412306..9746950 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/RelationshipProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/PathProperty.java
@@ -7,7 +7,7 @@
  *  "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
+ *    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
@@ -19,19 +19,16 @@
 
 package org.apache.cayenne.exp.property;
 
-import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.EmbeddableObject;
 import org.apache.cayenne.Persistent;
-import org.apache.cayenne.query.PrefetchTreeNode;
 
 /**
- * Interface (or "Trait") that provides basic functionality for all types of relationships.
- * <p>
- * Provides "dot", prefetch and "outer" functionality.
+ * Property that represents path segment (relationship or embeddable).
+ * Basically it provides {@code dot()} operator.
  *
- * @see org.apache.cayenne.exp.property
  * @since 4.2
  */
-public interface RelationshipProperty<E> extends Property<E> {
+public interface PathProperty<E> extends Property<E> {
 
     /**
      * Constructs a property path by appending the argument to the existing property separated by a dot.
@@ -103,7 +100,7 @@ public interface RelationshipProperty<E> extends Property<E> {
      * @param property to append to path
      * @return a newly created Property object.
      */
-    default <T extends Persistent> EntityProperty<T> dot(EntityProperty<T> property) {
+    default <T extends Persistent > EntityProperty<T> dot(EntityProperty<T> property) {
         String path = getName() + "." + property.getName();
         return PropertyFactory.createEntity(path,
                 PropertyUtils.buildExp(path, getExpression().getPathAliases()),
@@ -151,46 +148,13 @@ public interface RelationshipProperty<E> extends Property<E> {
     }
 
     /**
-     * Returns a version of this property that represents an OUTER join. It is
-     * up to caller to ensure that the property corresponds to a relationship,
-     * as "outer" attributes make no sense.
-     */
-    BaseProperty<E> outer();
-
-    /**
-     * Returns a prefetch tree that follows this property path, potentially
-     * spanning a number of phantom nodes, and having a single leaf with "joint"
-     * prefetch semantics.
-     */
-    default PrefetchTreeNode joint() {
-        if(!getExpression().getPathAliases().isEmpty()) {
-            throw new CayenneRuntimeException("Can't use aliases with prefetch");
-        }
-        return PrefetchTreeNode.withPath(getName(), PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
-    }
-
-    /**
-     * Returns a prefetch tree that follows this property path, potentially
-     * spanning a number of phantom nodes, and having a single leaf with
-     * "disjoint" prefetch semantics.
-     */
-    default PrefetchTreeNode disjoint() {
-        if(!getExpression().getPathAliases().isEmpty()) {
-            throw new CayenneRuntimeException("Can't use aliases with prefetch");
-        }
-        return PrefetchTreeNode.withPath(getName(), PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
-    }
-
-    /**
-     * Returns a prefetch tree that follows this property path, potentially
-     * spanning a number of phantom nodes, and having a single leaf with
-     * "disjoint by id" prefetch semantics.
+     * Constructs a new property path by appending the argument to the existing property separated by a dot.
+     *
+     * @param property to append to path
+     * @return a newly created Property object.
      */
-    default PrefetchTreeNode disjointById() {
-        if(!getExpression().getPathAliases().isEmpty()) {
-            throw new CayenneRuntimeException("Can't use aliases with prefetch");
-        }
-        return PrefetchTreeNode.withPath(getName(), PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS);
+    default <T extends EmbeddableObject> EmbeddableProperty<T> dot(EmbeddableProperty<T> property) {
+        String path = getName() + "." + property.getName();
+        return PropertyFactory.createEmbeddable(path, property.getType());
     }
-
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/PropertyFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/PropertyFactory.java
index 14b004d..cf1ef6e 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/PropertyFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/PropertyFactory.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.exp.property;
 
 import java.time.LocalDateTime;
 
+import org.apache.cayenne.EmbeddableObject;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.exp.Expression;
@@ -372,4 +373,8 @@ public class PropertyFactory {
     public static <K, V extends Persistent> MapProperty<K, V> createMap(String name, Class<K> keyType, Class<V> entityType) {
         return createMap(name, null, keyType, entityType);
     }
+
+    public static <T extends EmbeddableObject> EmbeddableProperty<T> createEmbeddable(String name, Class<T> embeddableType) {
+        return new EmbeddableProperty<>(name, embeddableType);
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/RelationshipProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/RelationshipProperty.java
index 5412306..4ccd893 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/RelationshipProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/RelationshipProperty.java
@@ -20,7 +20,6 @@
 package org.apache.cayenne.exp.property;
 
 import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.Persistent;
 import org.apache.cayenne.query.PrefetchTreeNode;
 
 /**
@@ -31,124 +30,7 @@ import org.apache.cayenne.query.PrefetchTreeNode;
  * @see org.apache.cayenne.exp.property
  * @since 4.2
  */
-public interface RelationshipProperty<E> extends Property<E> {
-
-    /**
-     * Constructs a property path by appending the argument to the existing property separated by a dot.
-     *
-     * @return a newly created Property object.
-     */
-    default BaseProperty<Object> dot(String property) {
-        String path = getName() + "." + property;
-        return PropertyFactory.createBase(path,
-                PropertyUtils.buildExp(path, getExpression().getPathAliases()),
-                null);
-    }
-
-    /**
-     * Constructs a new property path by appending the argument to the existing property separated by a dot.
-     *
-     * @param property to append to path
-     * @return a newly created Property object.
-     */
-    default <T> BaseProperty<T> dot(BaseProperty<T> property) {
-        String path = getName() + "." + property.getName();
-        return PropertyFactory.createBase(path,
-                PropertyUtils.buildExp(path, getExpression().getPathAliases()),
-                property.getType());
-    }
-
-    /**
-     * Constructs a new property path by appending the argument to the existing property separated by a dot.
-     *
-     * @param property to append to path
-     * @return a newly created Property object.
-     */
-    default <T extends Number> NumericProperty<T> dot(NumericProperty<T> property) {
-        String path = getName() + "." + property.getName();
-        return PropertyFactory.createNumeric(path,
-                PropertyUtils.buildExp(path, getExpression().getPathAliases()),
-                property.getType());
-    }
-
-    /**
-     * Constructs a new property path by appending the argument to the existing property separated by a dot.
-     *
-     * @param property to append to path
-     * @return a newly created Property object.
-     */
-    default <T extends CharSequence> StringProperty<T> dot(StringProperty<T> property) {
-        String path = getName() + "." + property.getName();
-        return PropertyFactory.createString(path,
-                PropertyUtils.buildExp(path, getExpression().getPathAliases()),
-                property.getType());
-    }
-
-    /**
-     * Constructs a new property path by appending the argument to the existing property separated by a dot.
-     *
-     * @param property to append to path
-     * @return a newly created Property object.
-     */
-    default <T> DateProperty<T> dot(DateProperty<T> property) {
-        String path = getName() + "." + property.getName();
-        return PropertyFactory.createDate(path,
-                PropertyUtils.buildExp(path, getExpression().getPathAliases()),
-                property.getType());
-    }
-
-    /**
-     * Constructs a new property path by appending the argument to the existing property separated by a dot.
-     *
-     * @param property to append to path
-     * @return a newly created Property object.
-     */
-    default <T extends Persistent> EntityProperty<T> dot(EntityProperty<T> property) {
-        String path = getName() + "." + property.getName();
-        return PropertyFactory.createEntity(path,
-                PropertyUtils.buildExp(path, getExpression().getPathAliases()),
-                property.getType());
-    }
-
-    /**
-     * Constructs a new property path by appending the argument to the existing property separated by a dot.
-     *
-     * @param property to append to path
-     * @return a newly created Property object.
-     */
-    default <T extends Persistent> ListProperty<T> dot(ListProperty<T> property) {
-        String path = getName() + "." + property.getName();
-        return PropertyFactory.createList(path,
-                PropertyUtils.buildExp(path, getExpression().getPathAliases()),
-                property.getEntityType());
-    }
-
-    /**
-     * Constructs a new property path by appending the argument to the existing property separated by a dot.
-     *
-     * @param property to append to path
-     * @return a newly created Property object.
-     */
-    default <T extends Persistent> SetProperty<T> dot(SetProperty<T> property) {
-        String path = getName() + "." + property.getName();
-        return PropertyFactory.createSet(path,
-                PropertyUtils.buildExp(path, getExpression().getPathAliases()),
-                property.getEntityType());
-    }
-
-    /**
-     * Constructs a new property path by appending the argument to the existing property separated by a dot.
-     *
-     * @param property to append to path
-     * @return a newly created Property object.
-     */
-    default <K, V extends Persistent> MapProperty<K, V> dot(MapProperty<K, V> property) {
-        String path = getName() + "." + property.getName();
-        return PropertyFactory.createMap(path,
-                PropertyUtils.buildExp(path, getExpression().getPathAliases()),
-                property.getKeyType(),
-                property.getEntityType());
-    }
+public interface RelationshipProperty<E> extends PathProperty<E> {
 
     /**
      * Returns a version of this property that represents an OUTER join. It is
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EmbeddedResult.java b/cayenne-server/src/main/java/org/apache/cayenne/map/EmbeddedResult.java
new file mode 100644
index 0000000..529bccf
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/map/EmbeddedResult.java
@@ -0,0 +1,59 @@
+/*****************************************************************
+ *   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.map;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A metadata object that provides mapping of a set of result columns to an Embeddable object.
+ * Used by {@link SQLResult}.
+ * Note that fields in the EmbeddedResult are not required to follow the order of columns
+ * in the actual query, and can be added in the arbitrary order.
+ *
+ * @since 4.2
+ */
+public class EmbeddedResult {
+
+    private final Map<String, String> fields;
+    private final Embeddable embeddable;
+
+    public EmbeddedResult(Embeddable embeddable, int size) {
+        this.embeddable = embeddable;
+        this.fields = new HashMap<>((int) Math.ceil(size / 0.75));
+    }
+
+    public void addAttribute(ObjAttribute attr) {
+        fields.put(attr.getDbAttributePath(), getAttributeName(attr));
+    }
+
+    private static String getAttributeName(ObjAttribute attr) {
+        String name = attr.getName();
+        return name.substring(name.lastIndexOf('.') + 1);
+    }
+
+    public Map<String, String> getFields() {
+        return fields;
+    }
+
+    public Embeddable getEmbeddable() {
+        return embeddable;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/PathComponentIterator.java b/cayenne-server/src/main/java/org/apache/cayenne/map/PathComponentIterator.java
index 0ff4e0a..3d2bee8 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/PathComponentIterator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/map/PathComponentIterator.java
@@ -37,6 +37,7 @@ class PathComponentIterator implements Iterator<PathComponent<Attribute, Relatio
     private final String path;
     private final Map<String, String> aliasMap;
 
+    private EmbeddedAttribute embeddedAttribute;
     private Entity currentEntity;
 
     PathComponentIterator(Entity root, String path, Map<String, String> aliasMap) {
@@ -44,6 +45,7 @@ class PathComponentIterator implements Iterator<PathComponent<Attribute, Relatio
         this.path = Objects.requireNonNull(path);
         this.aliasMap = Objects.requireNonNull(aliasMap);
         this.toks = new StringTokenizer(path, Entity.PATH_SEPARATOR);
+        this.embeddedAttribute = null;
     }
 
     public boolean hasNext() {
@@ -62,10 +64,19 @@ class PathComponentIterator implements Iterator<PathComponent<Attribute, Relatio
         }
 
         // see if this is an attribute
-        Attribute attr = currentEntity.getAttribute(pathComp);
+        Attribute attr;
+        if(embeddedAttribute != null) {
+            attr = embeddedAttribute.getAttribute(pathComp);
+            embeddedAttribute = null;
+        } else {
+            attr = currentEntity.getAttribute(pathComp);
+        }
+
         if (attr != null) {
             // do a sanity check...
-            if (toks.hasMoreTokens()) {
+            if(attr instanceof EmbeddedAttribute) {
+                embeddedAttribute = (EmbeddedAttribute)attr;
+            } else if (toks.hasMoreTokens()) {
                 throw new ExpressionException(
                         "Attribute must be the last component of the path: '" + pathComp + "'.", path, null);
             }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/SQLResult.java b/cayenne-server/src/main/java/org/apache/cayenne/map/SQLResult.java
index 1993451..18bde42 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/SQLResult.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/map/SQLResult.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.map;
 
+import org.apache.cayenne.query.DefaultEmbeddableResultSegment;
 import org.apache.cayenne.reflect.ClassDescriptor;
 
 import java.util.ArrayList;
@@ -69,6 +70,11 @@ public class SQLResult {
                 ClassDescriptor classDescriptor = resolver.getClassDescriptor(entityName);
                 resolvedComponents.add(new DefaultEntityResultSegment(classDescriptor, fields, offset));
                 offset = offset + fields.size();
+            } else if (component instanceof EmbeddedResult) {
+                EmbeddedResult embeddedResult = (EmbeddedResult)component;
+                Map<String, String> fields = embeddedResult.getFields();
+                resolvedComponents.add(new DefaultEmbeddableResultSegment(embeddedResult.getEmbeddable(), fields, offset));
+                offset = offset + fields.size();
             } else {
                 throw new IllegalArgumentException("Unsupported result descriptor component: " + component);
             }
@@ -103,21 +109,25 @@ public class SQLResult {
     }
 
     public void addEntityResult(EntityResult entityResult) {
-        if (resultDescriptors == null) {
-            resultDescriptors = new ArrayList<>(3);
-        }
+        checkAndAdd(entityResult);
+    }
 
-        resultDescriptors.add(entityResult);
+    public void addEmbeddedResult(EmbeddedResult embeddedResult) {
+        checkAndAdd(embeddedResult);
     }
 
     /**
      * Adds a result set column name to the mapping.
      */
     public void addColumnResult(String column) {
+        checkAndAdd(column);
+    }
+
+    private void checkAndAdd(Object result) {
         if (resultDescriptors == null) {
             resultDescriptors = new ArrayList<>(3);
         }
 
-        resultDescriptors.add(column);
+        resultDescriptors.add(result);
     }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java b/cayenne-server/src/main/java/org/apache/cayenne/query/DefaultEmbeddableResultSegment.java
similarity index 54%
copy from cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
copy to cayenne-server/src/main/java/org/apache/cayenne/query/DefaultEmbeddableResultSegment.java
index ef2ab3f..0e94d65 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PathTranslationResult.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/DefaultEmbeddableResultSegment.java
@@ -7,7 +7,7 @@
  *  "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
+ *    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
@@ -17,35 +17,39 @@
  *  under the License.
  ****************************************************************/
 
-package org.apache.cayenne.access.translator.select;
+package org.apache.cayenne.query;
 
-import java.util.List;
-import java.util.Optional;
+import java.util.Map;
 
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.Embeddable;
 
 /**
- * Interface that describes result of path translation
- *
  * @since 4.2
  */
-interface PathTranslationResult {
-
-    String getFinalPath();
-
-    Optional<DbRelationship> getDbRelationship();
+public class DefaultEmbeddableResultSegment implements EmbeddableResultSegment {
 
-    List<DbAttribute> getDbAttributes();
+    private final Embeddable embeddable;
+    private final Map<String, String> fields;
+    private final int offset;
 
-    List<String> getAttributePaths();
+    public DefaultEmbeddableResultSegment(Embeddable embeddable, Map<String, String> fields, int offset) {
+        this.embeddable = embeddable;
+        this.fields = fields;
+        this.offset = offset;
+    }
 
-    default DbAttribute getLastAttribute() {
-        return getDbAttributes().get(getDbAttributes().size() - 1);
+    @Override
+    public Map<String, String> getFields() {
+        return fields;
     }
 
-    default String getLastAttributePath() {
-        return getAttributePaths().get(getAttributePaths().size() - 1);
+    @Override
+    public int getColumnOffset() {
+        return offset;
     }
 
+    @Override
+    public Embeddable getEmbeddable() {
+        return embeddable;
+    }
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity1.java b/cayenne-server/src/main/java/org/apache/cayenne/query/EmbeddableResultSegment.java
similarity index 68%
copy from cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity1.java
copy to cayenne-server/src/main/java/org/apache/cayenne/query/EmbeddableResultSegment.java
index 119a0ce..f61dfc7 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity1.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/EmbeddableResultSegment.java
@@ -7,7 +7,7 @@
  *  "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
+ *    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
@@ -16,13 +16,25 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
-package org.apache.cayenne.testdo.embeddable;
 
-import org.apache.cayenne.testdo.embeddable.auto._EmbedEntity1;
+package org.apache.cayenne.query;
 
-public class EmbedEntity1 extends _EmbedEntity1 {
+import java.util.Map;
 
-}
+import org.apache.cayenne.map.Embeddable;
+
+/**
+ * @since 4.2
+ */
+public interface EmbeddableResultSegment {
 
+    Embeddable getEmbeddable();
 
+    Map<String, String> getFields();
 
+    /**
+     * Returns a zero-based column index of the first column of this segment in the
+     * ResultSet.
+     */
+    int getColumnOffset();
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/EmbeddingIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/EmbeddingIT.java
index 87797ad..0f3f291 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/EmbeddingIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/EmbeddingIT.java
@@ -26,6 +26,7 @@ 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.embeddable.EmbedEntity1;
+import org.apache.cayenne.testdo.embeddable.EmbedEntity2;
 import org.apache.cayenne.testdo.embeddable.Embeddable1;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
@@ -48,21 +49,29 @@ public class EmbeddingIT extends ServerCase {
     protected DBHelper dbHelper;
     
     protected TableHelper tEmbedEntity1;
+    protected TableHelper tEmbedEntity2;
 
     @Before
     public void setUp() throws Exception {
         tEmbedEntity1 = new TableHelper(dbHelper, "EMBED_ENTITY1");
         tEmbedEntity1.setColumns("ID", "NAME", "EMBEDDED10", "EMBEDDED20", "EMBEDDED30", "EMBEDDED40");
+
+        tEmbedEntity2 = new TableHelper(dbHelper, "EMBED_ENTITY2");
+        tEmbedEntity2.setColumns("ID", "NAME", "ENTITY1_ID", "EMBEDDED10", "EMBEDDED20");
     }
     
     protected void createSelectDataSet() throws Exception {
-        tEmbedEntity1.delete().execute();
         tEmbedEntity1.insert(1, "n1", "e1", "e2", "e3", "e4");
         tEmbedEntity1.insert(2, "n2", "ex1", "ex2", "ex3", "ex4");
     }
-    
+
+    protected void createSelectDataSet2() throws Exception {
+        createSelectDataSet();
+        tEmbedEntity2.insert(1, "n2-1", 1, "e1", "e2");
+        tEmbedEntity2.insert(2, "n2-1", 2, "e1", "e2");
+    }
+
     protected void createUpdateDataSet() throws Exception {
-        tEmbedEntity1.delete().execute();
         tEmbedEntity1.insert(1, "n1", "e1", "e2", "e3", "e4");
     }
 
@@ -105,8 +114,8 @@ public class EmbeddingIT extends ServerCase {
         createSelectDataSet();
 
         List<EmbedEntity1> result = ObjectSelect.query(EmbedEntity1.class)
-                .where(EmbedEntity1.EMBEDDED1_EMBEDDED10.eq("e1"))
-                .orderBy(EmbedEntity1.EMBEDDED2_EMBEDDED10.asc())
+                .where(EmbedEntity1.EMBEDDED1.dot(Embeddable1.EMBEDDED10).eq("e1"))
+                .orderBy(EmbedEntity1.EMBEDDED1.dot(Embeddable1.EMBEDDED10).asc())
                 .select(context);
         assertEquals(1, result.size());
         assertEquals("e1", result.get(0).getEmbedded1().getEmbedded10());
@@ -181,4 +190,112 @@ public class EmbeddingIT extends ServerCase {
         assertNotNull(row);
         assertEquals("x1", row.get("EMBEDDED10"));
     }
+
+    @Test
+    public void testPropertyExpression() throws Exception {
+        createSelectDataSet();
+
+        List<EmbedEntity1> result = ObjectSelect.query(EmbedEntity1.class)
+                .where(EmbedEntity1.EMBEDDED1.dot(Embeddable1.EMBEDDED10).eq("e1"))
+                .orderBy(EmbedEntity1.EMBEDDED2.dot(Embeddable1.EMBEDDED20).desc())
+                .select(context);
+
+        assertEquals(1, result.size());
+    }
+
+    @Test
+    public void testRelatedEmbedded() throws Exception {
+        createSelectDataSet2();
+
+        List<EmbedEntity2> result = ObjectSelect.query(EmbedEntity2.class)
+                .where(EmbedEntity2.ENTITY1.dot(EmbedEntity1.EMBEDDED1).dot(Embeddable1.EMBEDDED10).eq("e1"))
+                .orderBy(EmbedEntity2.ENTITY1.dot(EmbedEntity1.EMBEDDED2).dot(Embeddable1.EMBEDDED20).desc())
+                .select(context);
+
+        assertEquals(1, result.size());
+    }
+
+    @Test
+    public void testPrefetchWithEmbedded() throws Exception {
+        createSelectDataSet2();
+
+        List<EmbedEntity2> result = ObjectSelect.query(EmbedEntity2.class)
+                .prefetch(EmbedEntity2.ENTITY1.joint())
+                .select(context);
+
+        assertEquals(2, result.size());
+        assertNotNull(result.get(0).getEntity1().getEmbedded1());
+        assertNotNull(result.get(1).getEntity1().getEmbedded1());
+    }
+
+    @Test
+    public void testInMemoryFilteringByEmbeddable() throws Exception {
+        createSelectDataSet();
+
+        List<EmbedEntity1> result = ObjectSelect.query(EmbedEntity1.class).select(context);
+        assertEquals(2, result.size());
+
+        List<EmbedEntity1> filtered = EmbedEntity1.EMBEDDED1.dot(Embeddable1.EMBEDDED10).eq("e1").filterObjects(result);
+        assertEquals(1, filtered.size());
+        assertEquals("n1", filtered.get(0).getName());
+    }
+
+    @Test
+    public void testColumnSelect() throws Exception {
+        createSelectDataSet2();
+
+        List<Embeddable1> result = ObjectSelect.columnQuery(EmbedEntity1.class, EmbedEntity1.EMBEDDED2)
+                .orderBy(EmbedEntity1.EMBEDDED2.dot(Embeddable1.EMBEDDED10).asc())
+                .select(context);
+        assertEquals(2, result.size());
+        assertEquals("e3", result.get(0).getEmbedded10());
+        assertEquals("e4", result.get(0).getEmbedded20());
+        assertEquals("ex3", result.get(1).getEmbedded10());
+        assertEquals("ex4", result.get(1).getEmbedded20());
+
+        result.get(0).setEmbedded10("test");
+        context.commitChanges();
+     }
+
+    @Test
+    public void testColumnSelectMultiple() throws Exception {
+        createSelectDataSet2();
+
+        List<Object[]> result = ObjectSelect.columnQuery(EmbedEntity1.class, EmbedEntity1.EMBEDDED1, EmbedEntity1.EMBEDDED2)
+                .orderBy(EmbedEntity1.EMBEDDED2.dot(Embeddable1.EMBEDDED10).asc())
+                .select(context);
+        assertEquals(2, result.size());
+        assertEquals("e3", ((Embeddable1)result.get(0)[1]).getEmbedded10());
+        assertEquals("e4", ((Embeddable1)result.get(0)[1]).getEmbedded20());
+        assertEquals("ex3", ((Embeddable1)result.get(1)[1]).getEmbedded10());
+        assertEquals("ex4", ((Embeddable1)result.get(1)[1]).getEmbedded20());
+    }
+
+    @Test
+    public void testColumnSelectMixed() throws Exception {
+        createSelectDataSet2();
+
+        List<Object[]> result = ObjectSelect.columnQuery(EmbedEntity1.class, EmbedEntity1.EMBEDDED1.dot(Embeddable1.EMBEDDED10), EmbedEntity1.EMBEDDED2)
+                .orderBy(EmbedEntity1.EMBEDDED2.dot(Embeddable1.EMBEDDED10).asc())
+                .select(context);
+        assertEquals(2, result.size());
+        assertEquals("e3", ((Embeddable1)result.get(0)[1]).getEmbedded10());
+        assertEquals("e4", ((Embeddable1)result.get(0)[1]).getEmbedded20());
+        assertEquals("ex3", ((Embeddable1)result.get(1)[1]).getEmbedded10());
+        assertEquals("ex4", ((Embeddable1)result.get(1)[1]).getEmbedded20());
+    }
+
+    @Test
+    public void testWhere() throws Exception {
+        createSelectDataSet2();
+
+        Embeddable1 embeddable1 = new Embeddable1();
+        embeddable1.setEmbedded10("e1");
+        embeddable1.setEmbedded20("e2");
+
+        List<EmbedEntity1> result = ObjectSelect.query(EmbedEntity1.class)
+                .where(EmbedEntity1.EMBEDDED1.eq(embeddable1))
+                .select(context);
+        assertEquals(1, result.size());
+    }
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity1.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity1.java
index 119a0ce..f2c2eaa 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity1.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity1.java
@@ -22,6 +22,8 @@ import org.apache.cayenne.testdo.embeddable.auto._EmbedEntity1;
 
 public class EmbedEntity1 extends _EmbedEntity1 {
 
+    private static final long serialVersionUID = 1L; 
+
 }
 
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity2.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity2.java
new file mode 100644
index 0000000..128e79b
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/EmbedEntity2.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.embeddable;
+
+import org.apache.cayenne.testdo.embeddable.auto._EmbedEntity2;
+
+public class EmbedEntity2 extends _EmbedEntity2 {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_EmbedEntity1.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_EmbedEntity1.java
index 53fd122..e5d29cb 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_EmbedEntity1.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_EmbedEntity1.java
@@ -3,11 +3,14 @@ package org.apache.cayenne.testdo.embeddable.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.exp.ExpressionFactory;
+import org.apache.cayenne.exp.property.EmbeddableProperty;
+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.embeddable.EmbedEntity2;
 import org.apache.cayenne.testdo.embeddable.Embeddable1;
 
 /**
@@ -22,16 +25,16 @@ public abstract class _EmbedEntity1 extends BaseDataObject {
 
     public static final String ID_PK_COLUMN = "ID";
 
-    public static final StringProperty<String> EMBEDDED1_EMBEDDED20 = PropertyFactory.createString(ExpressionFactory.dbPathExp("EMBEDDED20"), String.class);
-    public static final StringProperty<String> EMBEDDED1_EMBEDDED10 = PropertyFactory.createString(ExpressionFactory.dbPathExp("EMBEDDED10"), String.class);
-    public static final StringProperty<String> EMBEDDED2_EMBEDDED20 = PropertyFactory.createString(ExpressionFactory.dbPathExp("EMBEDDED40"), String.class);
-    public static final StringProperty<String> EMBEDDED2_EMBEDDED10 = PropertyFactory.createString(ExpressionFactory.dbPathExp("EMBEDDED30"), String.class);
+    public static final EmbeddableProperty<Embeddable1> EMBEDDED1 = PropertyFactory.createEmbeddable("embedded1", Embeddable1.class);
+    public static final EmbeddableProperty<Embeddable1> EMBEDDED2 = PropertyFactory.createEmbeddable("embedded2", Embeddable1.class);
     public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
+    public static final ListProperty<EmbedEntity2> EMBED_ENTITY2S = PropertyFactory.createList("embedEntity2s", EmbedEntity2.class);
 
     protected Embeddable1 embedded1;
     protected Embeddable1 embedded2;
     protected String name;
 
+    protected Object embedEntity2s;
 
     public void setEmbedded1(Embeddable1 embedded1) {
         beforePropertyWrite("embedded1", this.embedded1, embedded1);
@@ -63,6 +66,19 @@ public abstract class _EmbedEntity1 extends BaseDataObject {
         return this.name;
     }
 
+    public void addToEmbedEntity2s(EmbedEntity2 obj) {
+        addToManyTarget("embedEntity2s", obj, true);
+    }
+
+    public void removeFromEmbedEntity2s(EmbedEntity2 obj) {
+        removeToManyTarget("embedEntity2s", obj, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<EmbedEntity2> getEmbedEntity2s() {
+        return (List<EmbedEntity2>)readProperty("embedEntity2s");
+    }
+
     @Override
     public Object readPropertyDirectly(String propName) {
         if(propName == null) {
@@ -76,6 +92,8 @@ public abstract class _EmbedEntity1 extends BaseDataObject {
                 return this.embedded2;
             case "name":
                 return this.name;
+            case "embedEntity2s":
+                return this.embedEntity2s;
             default:
                 return super.readPropertyDirectly(propName);
         }
@@ -97,6 +115,9 @@ public abstract class _EmbedEntity1 extends BaseDataObject {
             case "name":
                 this.name = (String)val;
                 break;
+            case "embedEntity2s":
+                this.embedEntity2s = val;
+                break;
             default:
                 super.writePropertyDirectly(propName, val);
         }
@@ -116,6 +137,7 @@ public abstract class _EmbedEntity1 extends BaseDataObject {
         out.writeObject(this.embedded1);
         out.writeObject(this.embedded2);
         out.writeObject(this.name);
+        out.writeObject(this.embedEntity2s);
     }
 
     @Override
@@ -124,6 +146,7 @@ public abstract class _EmbedEntity1 extends BaseDataObject {
         this.embedded1 = (Embeddable1)in.readObject();
         this.embedded2 = (Embeddable1)in.readObject();
         this.name = (String)in.readObject();
+        this.embedEntity2s = in.readObject();
     }
 
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_EmbedEntity1.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_EmbedEntity2.java
similarity index 56%
copy from cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_EmbedEntity1.java
copy to cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_EmbedEntity2.java
index 53fd122..5d867b2 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_EmbedEntity1.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_EmbedEntity2.java
@@ -5,52 +5,42 @@ import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.property.EmbeddableProperty;
+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.embeddable.EmbedEntity1;
 import org.apache.cayenne.testdo.embeddable.Embeddable1;
 
 /**
- * Class _EmbedEntity1 was generated by Cayenne.
+ * Class _EmbedEntity2 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 _EmbedEntity1 extends BaseDataObject {
+public abstract class _EmbedEntity2 extends BaseDataObject {
 
     private static final long serialVersionUID = 1L; 
 
     public static final String ID_PK_COLUMN = "ID";
 
-    public static final StringProperty<String> EMBEDDED1_EMBEDDED20 = PropertyFactory.createString(ExpressionFactory.dbPathExp("EMBEDDED20"), String.class);
-    public static final StringProperty<String> EMBEDDED1_EMBEDDED10 = PropertyFactory.createString(ExpressionFactory.dbPathExp("EMBEDDED10"), String.class);
-    public static final StringProperty<String> EMBEDDED2_EMBEDDED20 = PropertyFactory.createString(ExpressionFactory.dbPathExp("EMBEDDED40"), String.class);
-    public static final StringProperty<String> EMBEDDED2_EMBEDDED10 = PropertyFactory.createString(ExpressionFactory.dbPathExp("EMBEDDED30"), String.class);
+    public static final EmbeddableProperty<Embeddable1> EMBEDDED = PropertyFactory.createEmbeddable("embedded", Embeddable1.class);
     public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
+    public static final EntityProperty<EmbedEntity1> ENTITY1 = PropertyFactory.createEntity("entity1", EmbedEntity1.class);
 
-    protected Embeddable1 embedded1;
-    protected Embeddable1 embedded2;
+    protected Embeddable1 embedded;
     protected String name;
 
+    protected Object entity1;
 
-    public void setEmbedded1(Embeddable1 embedded1) {
-        beforePropertyWrite("embedded1", this.embedded1, embedded1);
-        this.embedded1 = embedded1;
+    public void setEmbedded(Embeddable1 embedded) {
+        beforePropertyWrite("embedded", this.embedded, embedded);
+        this.embedded = embedded;
     }
 
-    public Embeddable1 getEmbedded1() {
-        beforePropertyRead("embedded1");
-        return this.embedded1;
-    }
-
-    public void setEmbedded2(Embeddable1 embedded2) {
-        beforePropertyWrite("embedded2", this.embedded2, embedded2);
-        this.embedded2 = embedded2;
-    }
-
-    public Embeddable1 getEmbedded2() {
-        beforePropertyRead("embedded2");
-        return this.embedded2;
+    public Embeddable1 getEmbedded() {
+        beforePropertyRead("embedded");
+        return this.embedded;
     }
 
     public void setName(String name) {
@@ -63,6 +53,14 @@ public abstract class _EmbedEntity1 extends BaseDataObject {
         return this.name;
     }
 
+    public void setEntity1(EmbedEntity1 entity1) {
+        setToOneTarget("entity1", entity1, true);
+    }
+
+    public EmbedEntity1 getEntity1() {
+        return (EmbedEntity1)readProperty("entity1");
+    }
+
     @Override
     public Object readPropertyDirectly(String propName) {
         if(propName == null) {
@@ -70,12 +68,12 @@ public abstract class _EmbedEntity1 extends BaseDataObject {
         }
 
         switch(propName) {
-            case "embedded1":
-                return this.embedded1;
-            case "embedded2":
-                return this.embedded2;
+            case "embedded":
+                return this.embedded;
             case "name":
                 return this.name;
+            case "entity1":
+                return this.entity1;
             default:
                 return super.readPropertyDirectly(propName);
         }
@@ -88,15 +86,15 @@ public abstract class _EmbedEntity1 extends BaseDataObject {
         }
 
         switch (propName) {
-            case "embedded1":
-                this.embedded1 = (Embeddable1)val;
-                break;
-            case "embedded2":
-                this.embedded2 = (Embeddable1)val;
+            case "embedded":
+                this.embedded = (Embeddable1)val;
                 break;
             case "name":
                 this.name = (String)val;
                 break;
+            case "entity1":
+                this.entity1 = val;
+                break;
             default:
                 super.writePropertyDirectly(propName, val);
         }
@@ -113,17 +111,17 @@ public abstract class _EmbedEntity1 extends BaseDataObject {
     @Override
     protected void writeState(ObjectOutputStream out) throws IOException {
         super.writeState(out);
-        out.writeObject(this.embedded1);
-        out.writeObject(this.embedded2);
+        out.writeObject(this.embedded);
         out.writeObject(this.name);
+        out.writeObject(this.entity1);
     }
 
     @Override
     protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
         super.readState(in);
-        this.embedded1 = (Embeddable1)in.readObject();
-        this.embedded2 = (Embeddable1)in.readObject();
+        this.embedded = (Embeddable1)in.readObject();
         this.name = (String)in.readObject();
+        this.entity1 = in.readObject();
     }
 
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_Embeddable1.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_Embeddable1.java
index 905765c..169d3de 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_Embeddable1.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/embeddable/auto/_Embeddable1.java
@@ -2,7 +2,10 @@ package org.apache.cayenne.testdo.embeddable.auto;
 
 import java.io.Serializable;
 
+import org.apache.cayenne.EmbeddableObject;
 import org.apache.cayenne.Persistent;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
 
 /** 
  * Embeddable class _Embeddable1 was generated by Cayenne.
@@ -10,7 +13,10 @@ import org.apache.cayenne.Persistent;
  * since it may be overwritten next time code is regenerated. 
  * If you need to make any customizations, please use subclass. 
  */
-public abstract class _Embeddable1 implements Serializable {
+public abstract class _Embeddable1 implements EmbeddableObject, Serializable {
+
+    public static final StringProperty<String> EMBEDDED20 = PropertyFactory.createString("embedded20", String.class);
+    public static final StringProperty<String> EMBEDDED10 = PropertyFactory.createString("embedded10", String.class);
 
     // special properties injected by Cayenne
     private Persistent owner;
@@ -48,4 +54,37 @@ public abstract class _Embeddable1 implements Serializable {
         return embedded10;
     }
 
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "embedded20":
+                return this.embedded20;
+            case "embedded10":
+                return this.embedded10;
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "embedded20":
+                this.embedded20 = (String)val;
+                break;
+            case "embedded10":
+                this.embedded10 = (String)val;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown property: " + propName);
+        }
+    }
 }
diff --git a/cayenne-server/src/test/resources/cayenne-embeddable.xml b/cayenne-server/src/test/resources/cayenne-embeddable.xml
index aedacf3..eabee1b 100644
--- a/cayenne-server/src/test/resources/cayenne-embeddable.xml
+++ b/cayenne-server/src/test/resources/cayenne-embeddable.xml
@@ -1,5 +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="embeddable"/>
 </domain>
diff --git a/cayenne-server/src/test/resources/embeddable.map.xml b/cayenne-server/src/test/resources/embeddable.map.xml
index 10c4db6..dbcae05 100644
--- a/cayenne-server/src/test/resources/embeddable.map.xml
+++ b/cayenne-server/src/test/resources/embeddable.map.xml
@@ -1,12 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
 <data-map xmlns="http://cayenne.apache.org/schema/10/modelMap"
 	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap http://cayenne.apache.org/schema/10/modelMap.xsd"
+	 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.embeddable"/>
 	<embeddable className="org.apache.cayenne.testdo.embeddable.Embeddable1">
-		<embeddable-attribute name="embedded10" type="java.lang.String" db-attribute-name="EMBEDDED10"/>
 		<embeddable-attribute name="embedded20" type="java.lang.String" db-attribute-name="EMBEDDED20"/>
+		<embeddable-attribute name="embedded10" type="java.lang.String" db-attribute-name="EMBEDDED10"/>
 	</embeddable>
 	<db-entity name="EMBED_ENTITY1">
 		<db-attribute name="EMBEDDED10" type="VARCHAR" length="100"/>
@@ -16,12 +16,31 @@
 		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 		<db-attribute name="NAME" type="VARCHAR" length="100"/>
 	</db-entity>
+	<db-entity name="EMBED_ENTITY2">
+		<db-attribute name="EMBEDDED10" type="VARCHAR" length="100"/>
+		<db-attribute name="EMBEDDED20" type="VARCHAR" length="100"/>
+		<db-attribute name="ENTITY1_ID" type="INTEGER"/>
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="NAME" type="VARCHAR" length="100"/>
+	</db-entity>
 	<obj-entity name="EmbedEntity1" className="org.apache.cayenne.testdo.embeddable.EmbedEntity1" dbEntityName="EMBED_ENTITY1">
 		<embedded-attribute name="embedded1" type="org.apache.cayenne.testdo.embeddable.Embeddable1"/>
 		<embedded-attribute name="embedded2" type="org.apache.cayenne.testdo.embeddable.Embeddable1">
-			<embeddable-attribute-override name="embedded10" db-attribute-path="EMBEDDED30"/>
 			<embeddable-attribute-override name="embedded20" db-attribute-path="EMBEDDED40"/>
+			<embeddable-attribute-override name="embedded10" db-attribute-path="EMBEDDED30"/>
 		</embedded-attribute>
 		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
 	</obj-entity>
+	<obj-entity name="EmbedEntity2" className="org.apache.cayenne.testdo.embeddable.EmbedEntity2" dbEntityName="EMBED_ENTITY2">
+		<embedded-attribute name="embedded" type="org.apache.cayenne.testdo.embeddable.Embeddable1"/>
+		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
+	</obj-entity>
+	<db-relationship name="untitledRel" source="EMBED_ENTITY1" target="EMBED_ENTITY2" toMany="true">
+		<db-attribute-pair source="ID" target="ENTITY1_ID"/>
+	</db-relationship>
+	<db-relationship name="untitledRel" source="EMBED_ENTITY2" target="EMBED_ENTITY1">
+		<db-attribute-pair source="ENTITY1_ID" target="ID"/>
+	</db-relationship>
+	<obj-relationship name="embedEntity2s" source="EmbedEntity1" target="EmbedEntity2" deleteRule="Deny" db-relationship-path="untitledRel"/>
+	<obj-relationship name="entity1" source="EmbedEntity2" target="EmbedEntity1" deleteRule="Nullify" db-relationship-path="untitledRel"/>
 </data-map>