diff --git a/src/protean/core/entity.py b/src/protean/core/entity.py
index 09517d67..4e443bfb 100644
--- a/src/protean/core/entity.py
+++ b/src/protean/core/entity.py
@@ -6,7 +6,7 @@ from typing import Any, Union
 from protean.core.exceptions import ObjectNotFoundError
 from protean.core.exceptions import ValidationError
 from protean.core.field import Auto
-from protean.core.field import Field, Reference
+from protean.core.field import Field, Reference, ReferenceField
 from protean.utils.generic import classproperty
 from protean.utils.query import Q
 
@@ -48,11 +48,14 @@ class EntityBase(type):
         # Load declared fields from Base class, in case this Entity is subclassing another
         new_class._load_base_class_fields(bases, attrs)
 
+        # Lookup an already defined ID field or create an `Auto` field
+        new_class._set_id_field()
+
         # Set up Relation Fields
         new_class._set_up_reference_fields()
 
-        # Lookup an already defined ID field or create an `Auto` field
-        new_class._set_id_field()
+        # Load list of Attributes from declared fields, depending on type of fields
+        new_class._load_attributes()
 
         # Construct an empty QuerySet associated with this Entity class
         new_class.query = QuerySet(name)
@@ -119,6 +122,11 @@ class EntityBase(type):
         new_class._meta.declared_fields['id'] = id_field
         new_class._meta.id_field = id_field
 
+    def _load_attributes(new_class):
+        """Load list of attributes from declared fields"""
+        for field_name, field_obj in new_class._meta.declared_fields.items():
+            new_class._meta.attributes[field_obj.get_attribute_name()] = field_obj
+
 
 class EntityMeta:
     """ Metadata information for the entity including any options defined."""
@@ -129,6 +137,7 @@ class EntityMeta:
         # Initialize Options
         self.entity_cls = None
         self.declared_fields = {}
+        self.attributes = {}
         self.id_field = None
 
     @property
@@ -488,9 +497,7 @@ class Entity(metaclass=EntityBase):
         # for required fields
         for field_name, field_obj in self._meta.declared_fields.items():
             if field_name not in loaded_fields:
-                # Check that the field is not set already, which would happen if we are
-                #   dealing with reference fields
-                if getattr(self, field_name, None) is None:
+                if not isinstance(field_obj, (Reference, ReferenceField)):
                     setattr(self, field_name, None)
 
         # Raise any errors found during load
@@ -558,6 +565,11 @@ class Entity(metaclass=EntityBase):
         """Pass through method to retrieve declared fields defined for entity"""
         return cls._meta.declared_fields
 
+    @classproperty
+    def attributes(cls):
+        """Pass through method to retrieve attributes defined for entity"""
+        return cls._meta.attributes
+
     @classproperty
     def auto_fields(cls):
         """Pass through method to retrieve `Auto` fields defined for entity"""
diff --git a/src/protean/core/exceptions.py b/src/protean/core/exceptions.py
index 0f5ccd95..7de24031 100644
--- a/src/protean/core/exceptions.py
+++ b/src/protean/core/exceptions.py
@@ -11,8 +11,8 @@ class ObjectNotFoundError(Exception):
     """Object was not found, can raise 404"""
 
 
-class ValueError(Exception):
-    """Object of incorrect type, or with invalid state was assigned"""
+class NotSupportedError(Exception):
+    """Object does not support the operation being performed"""
 
 
 class ValidationError(Exception):
diff --git a/src/protean/core/field/__init__.py b/src/protean/core/field/__init__.py
index 35fb5750..f5839f53 100644
--- a/src/protean/core/field/__init__.py
+++ b/src/protean/core/field/__init__.py
@@ -1,6 +1,6 @@
 """Package for defining Field type and its implementations"""
 
-from .association import Reference
+from .association import Reference, ReferenceField
 from .base import Field
 from .basic import Auto
 from .basic import Boolean
@@ -18,4 +18,4 @@ from .ext import StringShort
 
 __all__ = ('Field', 'String', 'Boolean', 'Integer', 'Float', 'List', 'Dict',
            'Auto', 'Date', 'DateTime', 'Text', 'StringShort', 'StringMedium',
-           'StringLong', 'Reference')
+           'StringLong', 'Reference', 'ReferenceField')
diff --git a/src/protean/core/field/association.py b/src/protean/core/field/association.py
index f0d5d00e..ac7939e9 100644
--- a/src/protean/core/field/association.py
+++ b/src/protean/core/field/association.py
@@ -1,7 +1,10 @@
+from abc import abstractmethod
+
 from .base import Field
-from .mixins import FieldCacheMixin
+from .mixins import FieldCacheMixin, FieldDescriptorMixin
 
 from protean.core import exceptions
+from protean import utils
 
 
 class ReferenceField(Field):
@@ -13,27 +16,18 @@ class ReferenceField(Field):
         super().__init__(**kwargs)
 
     def __set__(self, instance, value):
-        """Override `__set__` to update relation field"""
+        """Override `__set__` to update relation field and keep it in sync with the shadow
+           attribute's value
+        """
         value = self._load(value)
 
         if value:
             instance.__dict__[self.field_name] = value
-
-            # Fetch target object and refresh the reference field value
-            reference_obj = self.reference.to_cls.find_by(
-                **{self.reference.linked_attribute: value})
-            if reference_obj:
-                self.reference.value = reference_obj
-                instance.__dict__[self.reference.field_name] = reference_obj
-            else:
-                # Object was not found in the database
-                raise exceptions.ValueError(
-                    "Target Object not found",
-                    self.reference.field_name)
         else:
             self._reset_values(instance)
 
     def __delete__(self, instance):
+        """Nullify values and linkages"""
         self._reset_values(instance)
 
     def _cast_to_type(self, value):
@@ -43,29 +37,28 @@ class ReferenceField(Field):
 
     def _reset_values(self, instance):
         """Reset all associated values and clean up dictionary items"""
-        instance.__dict__.pop(self.field_name)
-        instance.__dict__.pop(self.reference.field_name)
-        self.reference.value = None
         self.value = None
+        self.reference.value = None
+        instance.__dict__.pop(self.field_name, None)
+        instance.__dict__.pop(self.reference.field_name, None)
+        self.reference.delete_cached_value(instance)
 
 
 class Reference(FieldCacheMixin, Field):
     """
-    Provide a many-to-one relation by adding a column to the local entity
+    Provide a many-to-one relation by adding an attribute to the local entity
     to hold the remote value.
 
-    By default ForeignKey will target the pk of the remote model but this
+    By default ForeignKey will target the `id` column of the remote model but this
     behavior can be changed by using the ``via`` argument.
     """
 
     def __init__(self, to_cls, via=None, **kwargs):
+        # FIXME ensure `via` argument is of type `str`
         super().__init__(**kwargs)
         self.to_cls = to_cls
         self.via = via
 
-        # Choose the Linkage attribute between `via` and `id`
-        self.linked_attribute = self.via or 'id'
-
         self.relation = ReferenceField(self)
 
     def get_attribute_name(self):
@@ -77,37 +70,220 @@ class Reference(FieldCacheMixin, Field):
         Primarily used during Entity initialization to register shadow field"""
         return (self.attribute_name, self.relation)
 
+    def get_cache_name(self):
+        return self.field_name
+
+    @property
+    def linked_attribute(self):
+        """Choose the Linkage attribute between `via` and designated `id_field` of the target class
+
+        This method is initially called from `__set_name__()` -> `get_attribute_name()`
+        at which point, the `to_cls` has not been initialized properly. We simply default
+        the linked attribute to 'id' in that case.
+
+        Eventually, when setting value the first time, the `to_cls` entity is initialized
+        and the attribute name is reset correctly.
+        """
+        if isinstance(self.to_cls, str):
+            return 'id'
+        else:
+            return self.via or self.to_cls.id_field.attribute_name
+
+    def __get__(self, instance, owner):
+        """Retrieve associated objects"""
+
+        # If `to_cls` was specified as a string, take this opportunity to fetch
+        #   and update the correct entity class against it, if not already done
+        if isinstance(self.to_cls, str):
+            self.to_cls = utils.fetch_entity_cls_from_registry(self.to_cls)
+
+            # Refresh attribute name, now that we know `to_cls` Entity and it has been
+            #   initialized with `id_field`
+            self.attribute_name = self.get_attribute_name()
+
+        reference_obj = None
+        if hasattr(instance, '_state'):
+            try:
+                reference_obj = self.get_cached_value(instance)
+            except KeyError:
+                # Fetch target object by own Identifier
+                id_value = getattr(instance, self.get_attribute_name())
+                if id_value:
+                    reference_obj = self._fetch_objects(self.linked_attribute, id_value)
+                    if reference_obj:
+                        self._set_own_value(instance, reference_obj)
+                    else:
+                        # No Objects were found in the remote entity with this Entity's ID
+                        pass
+
+        return reference_obj
+
+    def _fetch_objects(self, key, value):
+        """Fetch Multiple linked objects"""
+        return self.to_cls.find_by(**{key: value})
+
     def __set__(self, instance, value):
         """Override `__set__` to coordinate between relation field and its shadow attribute"""
-        value = self._load(value)
-
         if value:
-            # Check if the reference object has been saved. Otherwise, throw ValueError
-            if value.id is None:  # FIXME not a comprehensive check. Should refer to state
-                raise exceptions.ValueError(
-                    "Target Object must be saved before being referenced",
-                    self.field_name)
-            else:
-                self.relation.value = value.id
-                instance.__dict__[self.field_name] = value
-                instance.__dict__[self.attribute_name] = getattr(value, self.linked_attribute)
+            if isinstance(self.to_cls, str):
+                self.to_cls = utils.fetch_entity_cls_from_registry(self.to_cls)
+
+                # Refresh attribute name, now that we know `to_cls` Entity and it has been
+                #   initialized with `id_field`
+                self.attribute_name = self.get_attribute_name()
+
+            value = self._load(value)
+
+            if value:
+                # Check if the reference object has been saved. Otherwise, throw ValueError
+                if value.id is None:  # FIXME not a comprehensive check. Should refer to state
+                    raise ValueError(
+                        "Target Object must be saved before being referenced",
+                        self.field_name)
+                else:
+                    self._set_own_value(instance, value)
+                    self._set_relation_value(instance, getattr(value, self.linked_attribute))
         else:
             self._reset_values(instance)
 
+    def _set_own_value(self, instance, value):
+        self.value = value
+        if value is None:
+            instance.__dict__.pop(self.field_name, None)
+            self.delete_cached_value(instance)
+        else:
+            instance.__dict__[self.field_name] = value
+            self.set_cached_value(instance, value)
+
+    def _set_relation_value(self, instance, value):
+        self.relation.value = value
+        if value is None:
+            instance.__dict__.pop(self.attribute_name, None)
+        else:
+            instance.__dict__[self.attribute_name] = value
+
     def __delete__(self, instance):
         self._reset_values(instance)
 
     def _reset_values(self, instance):
         """Reset all associated values and clean up dictionary items"""
-        self.value = None
-        self.relation.value = None
-        instance.__dict__.pop(self.field_name, None)
-        instance.__dict__.pop(self.attribute_name, None)
+        self._set_own_value(instance, None)
+        self._set_relation_value(instance, None)
 
     def _cast_to_type(self, value):
         if not isinstance(value, self.to_cls):
             self.fail('invalid', value=value)
         return value
 
+
+class Association(FieldDescriptorMixin, FieldCacheMixin):
+    """Base class for all association classes"""
+
+    def __init__(self, to_cls, via=None, **kwargs):
+        super().__init__(**kwargs)
+
+        self.to_cls = to_cls
+        self.via = via
+
+    def _cast_to_type(self, value):
+        """Verify type of value assigned to the association field"""
+        # FIXME Verify that the value being assigned is compatible with the associated Entity
+        return value
+
+    def _linked_attribute(self, owner):
+        """Choose the Linkage attribute between `via` and own entity's `id_field`
+
+           FIXME Explore converting this method into an attribute, and treating it
+           uniformly at `association` level.
+        """
+        return self.via or (utils.inflection.underscore(owner.__name__) + '_id')
+
+    def _fetch_to_cls_from_registry(self, entity):
+        """Private Method to fetch an Entity class from an entity's name"""
+        # Defensive check to ensure we only process if `to_cls` is a string
+        if isinstance(entity, str):
+            from protean.core.repository import repo_factory  # FIXME Move to a better placement
+
+            try:
+                return repo_factory.get_entity(self.to_cls)
+            except AssertionError:
+                # Entity has not been registered (yet)
+                # FIXME print a helpful debug message
+                raise
+        else:
+            return self.to_cls
+
+    def __get__(self, instance, owner):
+        """Retrieve associated objects"""
+
+        # If `to_cls` was specified as a string, take this opportunity to fetch
+        #   and update the correct entity class against it, if not already done
+        if isinstance(self.to_cls, str):
+            self.to_cls = self._fetch_to_cls_from_registry(self.to_cls)
+
+        try:
+            reference_obj = self.get_cached_value(instance)
+        except KeyError:
+            # Fetch target object by own Identifier
+            id_value = getattr(instance, instance.id_field.field_name)
+            reference_obj = self._fetch_objects(self._linked_attribute(owner), id_value)
+            if reference_obj:
+                self._set_own_value(instance, reference_obj)
+            else:
+                # No Objects were found in the remote entity with this Entity's ID
+                reference_obj = None
+
+        return reference_obj
+
+    def _set_own_value(self, instance, value):
+        self.value = value
+        instance.__dict__[self.field_name] = value
+        self.set_cached_value(instance, value)
+
+    @abstractmethod
+    def _fetch_objects(self, key, value):
+        """Placeholder method for customized Association query methods"""
+        raise NotImplementedError
+
+    def __set__(self, instance, value):
+        """Cannot set values through an association"""
+        raise exceptions.NotSupportedError(
+            "Object does not support the operation being performed",
+            self.field_name
+        )
+
+    def __delete__(self, instance):
+        """Cannot pop values for an association"""
+        raise exceptions.NotSupportedError(
+            "Object does not support the operation being performed",
+            self.field_name
+        )
+
     def get_cache_name(self):
-        return self.name
+        return self.field_name
+
+
+class HasOne(Association):
+    """
+    Provide a HasOne relation to a remote entity.
+
+    By default, the query will lookup an attribute of the form `<current_entity>_id`
+    to fetch and populate. This behavior can be changed by using the `via` argument.
+    """
+
+    def _fetch_objects(self, key, value):
+        """Fetch Multiple linked objects"""
+        return self.to_cls.find_by(**{key: value})
+
+
+class HasMany(Association):
+    """
+    Provide a HasMany relation to a remote entity.
+
+    By default, the query will lookup an attribute of the form `<current_entity>_id`
+    to fetch and populate. This behavior can be changed by using the `via` argument.
+    """
+
+    def _fetch_objects(self, key, value):
+        """Fetch Multiple linked objects"""
+        return self.to_cls.query.filter(**{key: value})
\ No newline at end of file
diff --git a/src/protean/core/field/base.py b/src/protean/core/field/base.py
index 94f1fce7..750e94a6 100644
--- a/src/protean/core/field/base.py
+++ b/src/protean/core/field/base.py
@@ -6,6 +6,8 @@ from abc import abstractmethod
 from typing import Any
 from typing import Iterable
 
+from .mixins import FieldDescriptorMixin
+
 from protean.core import exceptions
 
 MISSING_ERROR_MESSAGE = (
@@ -14,7 +16,7 @@ MISSING_ERROR_MESSAGE = (
 )
 
 
-class Field(metaclass=ABCMeta):
+class Field(FieldDescriptorMixin, metaclass=ABCMeta):
     """Abstract field from which other fields should extend.
 
     :param default: If set, this value will be used during entity loading
@@ -50,6 +52,9 @@ class Field(metaclass=ABCMeta):
                  label: str = None, choices: enum.Enum = None,
                  validators: Iterable = (), value=None, error_messages: dict = None):
 
+        # Nothing to be passed into FieldCacheMixin for initialization
+        super().__init__(**{})
+
         self.identifier = identifier
         self.default = default
 
@@ -76,9 +81,8 @@ class Field(metaclass=ABCMeta):
         # Value holder
         self._value = value
 
-        # These are set up when the owner (Entity class) adds the field to itself
-        self.field_name = None
-        self.attribute_name = None
+        # Hold a reference to Entity registering the field
+        self._entity_cls = None
 
         # Collect default error message from self and parent classes
         messages = {}
@@ -87,16 +91,11 @@ class Field(metaclass=ABCMeta):
         messages.update(error_messages or {})
         self.error_messages = messages
 
-    def __set_name__(self, entity_cls, name):
-        self.field_name = name
-        self.attribute_name = self.get_attribute_name()
-
-        # `self.label` should default to being based on the field name.
-        if self.label is None:
-            self.label = self.field_name.replace('_', ' ').capitalize()
-
     def __get__(self, instance, owner):
-        return instance.__dict__.get(self.field_name, self.value)
+        if hasattr(instance, '__dict__'):
+            return instance.__dict__.get(self.field_name, self.value)
+        else:
+            return None
 
     def __set__(self, instance, value):
         value = self._load(value)
@@ -116,14 +115,6 @@ class Field(metaclass=ABCMeta):
     def value(self, value):
         self._value = value if value else None
 
-    def get_attribute_name(self):
-        """Return Attribute name for the attribute.
-
-        Defaults to the field name in this base class, but can be overridden.
-        Handy when defining complex objects with shadow attributes, like Foreign keys.
-        """
-        return self.field_name
-
     def fail(self, key, **kwargs):
         """A helper method that simply raises a `ValidationError`.
         """
@@ -136,7 +127,6 @@ class Field(metaclass=ABCMeta):
             raise AssertionError(msg)
         if isinstance(msg, str):
             msg = msg.format(**kwargs)
-
         raise exceptions.ValidationError(msg, self.field_name)
 
     @property
@@ -219,3 +209,6 @@ class Field(metaclass=ABCMeta):
         self._run_validators(value)
 
         return value
+
+    def get_cache_name(self):
+        return self.field_name
diff --git a/src/protean/core/field/mixins.py b/src/protean/core/field/mixins.py
index c6dbc4ec..5e314bfd 100644
--- a/src/protean/core/field/mixins.py
+++ b/src/protean/core/field/mixins.py
@@ -2,7 +2,7 @@ NOT_PROVIDED = object()
 
 
 class FieldCacheMixin:
-    """Provide an API for working with the model's fields value cache."""
+    """Provide an API for working with the entity's fields value cache."""
 
     def get_cache_name(self):
         raise NotImplementedError
@@ -23,4 +23,47 @@ class FieldCacheMixin:
         instance._state.fields_cache[self.get_cache_name()] = value
 
     def delete_cached_value(self, instance):
-        del instance._state.fields_cache[self.get_cache_name()]
+        if self.get_cache_name() in instance._state.fields_cache:
+            del instance._state.fields_cache[self.get_cache_name()]
+
+
+class FieldDescriptorMixin:
+    """Provide basic implementation to treat the Field as a descriptor"""
+
+    def __init__(self, *args, **kwargs):
+        """Initialize common Field Attributes"""
+        # These are set up when the owner (Entity class) adds the field to itself
+        self.field_name = None
+        self.attribute_name = None
+        self.label = None
+
+    def __set_name__(self, entity_cls, name):
+        self.field_name = name
+        self.attribute_name = self.get_attribute_name()
+
+        # Record Entity setting up the field
+        self._entity_cls = entity_cls
+
+        # `self.label` should default to being based on the field name.
+        if self.label is None:
+            self.label = self.field_name.replace('_', ' ').capitalize()
+
+    def get_attribute_name(self):
+        """Return Attribute name for the attribute.
+
+        Defaults to the field name in this base class, but can be overridden.
+        Handy when defining complex objects with shadow attributes, like Foreign keys.
+        """
+        return self.field_name
+
+    def __get__(self, instance, owner):
+        """Placeholder for handling `getattr` operations on attributes"""
+        raise NotImplementedError
+
+    def __set__(self, instance, value):
+        """Placeholder for handling `setattr` operations on attributes"""
+        raise NotImplementedError
+
+    def __delete__(self, instance):
+        """Placeholder for handling `del` operations on attributes"""
+        raise NotImplementedError
diff --git a/src/protean/core/repository/factory.py b/src/protean/core/repository/factory.py
index b4883c3f..8f3c5f67 100644
--- a/src/protean/core/repository/factory.py
+++ b/src/protean/core/repository/factory.py
@@ -23,6 +23,7 @@ class RepositoryFactory:
     def __init__(self):
         """"Initialize repository factory"""
         self._registry = {}
+        self._entity_registry = {}
         self._model_registry = {}
         self._connections = local()
         self._repositories = None
@@ -94,6 +95,7 @@ class RepositoryFactory:
             self._registry[entity_name] = \
                 repo_cls(self.connections[model_cls.opts_.bind], model_cls)
             self._model_registry[entity_name] = model_cls
+            self._entity_registry[entity_name] = model_cls.Meta.entity
             logger.debug(
                 f'Registered model {model_name} for entity {entity_name} with repository provider '
                 f'{conn_info["PROVIDER"]}.')
@@ -103,13 +105,20 @@ class RepositoryFactory:
         try:
             return self._model_registry[entity_name]
         except KeyError:
-            raise AssertionError('No Model registered for {entity_name}')
+            raise AssertionError('No Model registered for {entity_name}'.format(entity_name))
+
+    def get_entity(self, entity_name):
+        """Retrieve Entity class registered by `entity_name`"""
+        try:
+            return self._entity_registry[entity_name]
+        except KeyError:
+            raise AssertionError('No Entity registered with name {entity_name}'.format(entity_name))
 
     def __getattr__(self, entity_name):
         try:
             return self._registry[entity_name]
         except KeyError:
-            raise AssertionError('No Model registered for {entity_name}')
+            raise AssertionError('No Model registered for {entity_name}'.format(entity_name))
 
     def close_connections(self):
         """ Close all connections registered with the repository """
diff --git a/src/protean/impl/repository/dict_repo.py b/src/protean/impl/repository/dict_repo.py
index 66fe818b..922717e3 100644
--- a/src/protean/impl/repository/dict_repo.py
+++ b/src/protean/impl/repository/dict_repo.py
@@ -7,7 +7,6 @@ from threading import Lock
 
 from protean.core.entity import Q
 from protean.core.exceptions import ObjectNotFoundError
-from protean.core.field import Auto
 from protean.core.repository import BaseAdapter
 from protean.core.repository import BaseConnectionHandler
 from protean.core.repository import BaseModel
@@ -298,7 +297,7 @@ class DictModel(BaseModel):
     def from_entity(cls, entity):
         """ Convert the entity to a dictionary record """
         dict_obj = {}
-        for field_name in entity.__class__.declared_fields:
+        for field_name in entity.__class__.attributes:
             dict_obj[field_name] = getattr(entity, field_name)
         return dict_obj
 
diff --git a/src/protean/utils/__init__.py b/src/protean/utils/__init__.py
index e69de29b..a7fcfa81 100644
--- a/src/protean/utils/__init__.py
+++ b/src/protean/utils/__init__.py
@@ -0,0 +1,14 @@
+def fetch_entity_cls_from_registry(entity):
+        """Util Method to fetch an Entity class from an entity's name"""
+        # Defensive check to ensure we only process if `to_cls` is a string
+        if isinstance(entity, str):
+            from protean.core.repository import repo_factory  # FIXME Move to a better placement
+
+            try:
+                return repo_factory.get_entity(entity)
+            except AssertionError:
+                # Entity has not been registered (yet)
+                # FIXME print a helpful debug message
+                raise
+        else:
+            return entity

Support for Referenced Objects
### Proposal:

Support relationships via the following two concepts:

**Foreign Keys**

- Indicated via the `references` option in Field definition
- validated during `save()` method call
- The value passed to `references` is the name of the attribute in the other entity (to be cross-referenced against)

**Virtual Linkages**

- `Linked` class defines a Virtual field, that is not persisted in the database and gives back the result object instance(s)
- `via` specifies the Linkage attribute, which can either be in the same entity (self-linkage) or a target entity
- `many` indicates multiple object instances, usually the "many" side of One-to-Many or Many-to-One relationships

The thought behind these concepts is to:

- Keep the relationship non-magical, meaning explicitly-defined
- Be able to adapt to different kinds of DBs (SQL, non-SQL, graph, etc.)
- Clearly distinguish between virtual in-memory objects (resulting from relationships) vs. the physical keys that will be present as part of the table
diff --git a/tests/conftest.py b/tests/conftest.py
index 14fbb80e..a21a9759 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,13 +10,30 @@ os.environ['PROTEAN_CONFIG'] = 'tests.support.sample_config'
 def run_around_tests():
     """Initialize DogModel with Dict Repo"""
     from protean.core.repository import repo_factory
-    from tests.support.dog import DogModel, RelatedDogModel, DogRelatedByEmailModel
-    from tests.support.human import HumanModel
+    from tests.support.dog import (DogModel, RelatedDogModel, DogRelatedByEmailModel,
+                                   HasOneDog1Model, HasOneDog2Model, HasOneDog3Model,
+                                   HasManyDog1Model, HasManyDog2Model, HasManyDog3Model)
+    from tests.support.human import (HumanModel, HasOneHuman1Model,
+                                     HasOneHuman2Model, HasOneHuman3Model,
+                                     HasManyHuman1Model, HasManyHuman2Model,
+                                     HasManyHuman3Model)
 
     repo_factory.register(DogModel)
     repo_factory.register(RelatedDogModel)
     repo_factory.register(DogRelatedByEmailModel)
+    repo_factory.register(HasOneDog1Model)
+    repo_factory.register(HasOneDog2Model)
+    repo_factory.register(HasOneDog3Model)
+    repo_factory.register(HasManyDog1Model)
+    repo_factory.register(HasManyDog2Model)
+    repo_factory.register(HasManyDog3Model)
     repo_factory.register(HumanModel)
+    repo_factory.register(HasOneHuman1Model)
+    repo_factory.register(HasOneHuman2Model)
+    repo_factory.register(HasOneHuman3Model)
+    repo_factory.register(HasManyHuman1Model)
+    repo_factory.register(HasManyHuman2Model)
+    repo_factory.register(HasManyHuman3Model)
 
     # A test function will be run at this point
     yield
@@ -24,4 +41,16 @@ def run_around_tests():
     repo_factory.Dog.delete_all()
     repo_factory.RelatedDog.delete_all()
     repo_factory.DogRelatedByEmail.delete_all()
+    repo_factory.HasOneDog1.delete_all()
+    repo_factory.HasOneDog2.delete_all()
+    repo_factory.HasOneDog3.delete_all()
+    repo_factory.HasManyDog1.delete_all()
+    repo_factory.HasManyDog2.delete_all()
+    repo_factory.HasManyDog3.delete_all()
     repo_factory.Human.delete_all()
+    repo_factory.HasOneHuman1.delete_all()
+    repo_factory.HasOneHuman2.delete_all()
+    repo_factory.HasOneHuman3.delete_all()
+    repo_factory.HasManyHuman1.delete_all()
+    repo_factory.HasManyHuman2.delete_all()
+    repo_factory.HasManyHuman3.delete_all()
diff --git a/tests/core/test_entity.py b/tests/core/test_entity.py
index d435750c..a303332e 100644
--- a/tests/core/test_entity.py
+++ b/tests/core/test_entity.py
@@ -1,13 +1,20 @@
 """Tests for Entity Functionality and Base Classes"""
 
+from collections import OrderedDict
+
+import mock
 import pytest
-from tests.support.dog import Dog, RelatedDog, DogRelatedByEmail
-from tests.support.human import Human
+from tests.support.dog import (Dog, RelatedDog, RelatedDog2, DogRelatedByEmail,
+                               HasOneDog1, HasOneDog2, HasOneDog3,
+                               HasManyDog1, HasManyDog2, HasManyDog3)
+from tests.support.human import (Human, HasOneHuman1, HasOneHuman2, HasOneHuman3,
+                                 HasManyHuman1, HasManyHuman2, HasManyHuman3)
+
 
 from protean.core import field
 from protean.core.entity import Entity, QuerySet
 from protean.core.exceptions import ObjectNotFoundError
-from protean.core.exceptions import ValidationError, ValueError
+from protean.core.exceptions import ValidationError
 from protean.utils.query import Q
 
 
@@ -573,6 +580,40 @@ class TestEntity:
             assert dog.is_destroyed
 
 
+class TestEntityMetaAttributes:
+    """Class that holds testcases for Entity's meta attributes"""
+
+    def test_meta_on_init(self):
+        """Test that `meta` attribute is available after initialization"""
+        dog = Dog(id=1, name='John Doe', age=10, owner='Jimmy')
+        assert hasattr(dog, '_meta')
+
+    def test_declared_fields_normal(self):
+        """Test declared fields on an entity without references"""
+        dog = Dog(id=1, name='John Doe', age=10, owner='Jimmy')
+
+        attribute_keys = list(OrderedDict(sorted(dog.attributes.items())).keys())
+        assert attribute_keys == ['age', 'id', 'name', 'owner']
+
+    def test_declared_fields_with_reference(self):
+        """Test declared fields on an entity with references"""
+        human = Human.create(first_name='Jeff', last_name='Kennedy',
+                             email='jeff.kennedy@presidents.com')
+        dog = RelatedDog(id=1, name='John Doe', age=10, owner=human)
+
+        attribute_keys = list(OrderedDict(sorted(dog.attributes.items())).keys())
+        assert attribute_keys == ['age', 'id', 'name', 'owner_id']
+
+    def test_declared_fields_with_hasone_association(self):
+        """Test declared fields on an entity with a HasOne association"""
+        human = HasOneHuman1.create(first_name='Jeff', last_name='Kennedy',
+                                    email='jeff.kennedy@presidents.com')
+        dog = HasOneDog1.create(id=1, name='John Doe', age=10, has_one_human1=human)
+
+        assert all(key in dog.attributes for key in ['age', 'has_one_human1_id', 'id', 'name'])
+        assert all(key in human.attributes for key in ['first_name', 'id', 'last_name', 'email'])
+
+
 class TestQuerySet:
     """Class that holds Tests for QuerySet"""
 
@@ -1272,15 +1313,29 @@ class TestAssociations:
             human = Human.create(first_name='Jeff', last_name='Kennedy',
                                  email='jeff.kennedy@presidents.com')
             dog = RelatedDog(id=1, name='John Doe', age=10, owner=human)
-            assert dog.owner == human
+            assert all(key in dog.__dict__ for key in ['owner', 'owner_id'])
+            assert dog.owner.id == human.id
+            assert dog.owner_id == human.id
+
+        def test_init_with_string_reference(self):
+            """Test successful RelatedDog initialization"""
+            human = Human.create(first_name='Jeff', last_name='Kennedy',
+                                 email='jeff.kennedy@presidents.com')
+            dog = RelatedDog2(id=1, name='John Doe', age=10, owner=human)
+            assert all(key in dog.__dict__ for key in ['owner', 'owner_id'])
+            assert dog.owner.id == human.id
+            assert dog.owner_id == human.id
+            assert not hasattr(human, 'dog')  # Reverse linkages are not provided by default
 
         def test_save(self):
             """Test successful RelatedDog save"""
             human = Human.create(first_name='Jeff', last_name='Kennedy',
                                  email='jeff.kennedy@presidents.com')
             dog = RelatedDog(id=1, name='John Doe', age=10, owner=human)
+            assert all(key in dog.__dict__ for key in ['owner', 'owner_id'])
             dog.save()
             assert dog.id is not None
+            assert all(key in dog.__dict__ for key in ['owner', 'owner_id'])
 
         def test_unsaved_entity_init(self):
             """Test that initialization fails when an unsaved entity is assigned to a relation"""
@@ -1293,7 +1348,9 @@ class TestAssociations:
             with pytest.raises(ValueError):
                 human = Human(first_name='Jeff', last_name='Kennedy',
                               email='jeff.kennedy@presidents.com')
+
                 dog = RelatedDog(id=1, name='John Doe', age=10)
+                assert any(key in dog.__dict__ for key in ['owner', 'owner_id']) is False
                 dog.owner = human
 
         def test_invalid_entity_type(self):
@@ -1308,25 +1365,49 @@ class TestAssociations:
             human = Human.create(first_name='Jeff', last_name='Kennedy',
                                  email='jeff.kennedy@presidents.com')
             dog = RelatedDog(id=1, name='John Doe', age=10, owner=human)
+            assert all(key in dog.__dict__ for key in ['owner', 'owner_id'])
             assert human.id is not None
             assert dog.owner_id == human.id
 
         def test_save_after_assign(self):
-            """Test identifier backing the association"""
+            """Test saving after assignment (post init)"""
             human = Human.create(id=101, first_name='Jeff', last_name='Kennedy',
                                  email='jeff.kennedy@presidents.com')
             dog = RelatedDog(id=1, name='John Doe', age=10)
+            assert any(key in dog.__dict__ for key in ['owner', 'owner_id']) is False
             dog.owner = human
+            dog.save()
+            assert all(key in dog.__dict__ for key in ['owner', 'owner_id'])
             assert dog.owner_id == human.id
 
+        def test_fetch_after_save(self):
+            """Test fetch after save and ensure reference is not auto-loaded"""
+            human = Human.create(id=101, first_name='Jeff', last_name='Kennedy',
+                                 email='jeff.kennedy@presidents.com')
+            dog = RelatedDog(id=1, name='John Doe', age=10)
+            dog.owner = human
+            dog.save()
+
+            dog2 = RelatedDog.get(dog.id)
+            # Reference attribute is not loaded automatically
+            assert 'owner' not in dog2.__dict__
+            assert dog2.owner_id == human.id
+
+            # Accessing attribute shows it up in __dict__
+            getattr(dog2, 'owner')
+            assert 'owner' in dog2.__dict__
+
         def test_shadow_attribute_init(self):
             """Test identifier backing the association"""
             human = Human.create(id=101, first_name='Jeff', last_name='Kennedy',
                                  email='jeff.kennedy@presidents.com')
             dog = RelatedDog(id=1, name='John Doe', age=10, owner_id=human.id)
+            assert 'owner_id' in dog.__dict__
+            assert 'owner' not in dog.__dict__
             dog.save()
             assert dog.owner_id == human.id
             assert dog.owner.id == human.id
+            assert all(key in dog.__dict__ for key in ['owner', 'owner_id'])
 
         def test_shadow_attribute_assign(self):
             """Test identifier backing the association"""
@@ -1334,9 +1415,11 @@ class TestAssociations:
                                  email='jeff.kennedy@presidents.com')
             dog = RelatedDog(id=1, name='John Doe', age=10)
             dog.owner_id = human.id
+            assert 'owner' not in dog.__dict__
             dog.save()
             assert dog.owner_id == human.id
             assert dog.owner.id == human.id
+            assert 'owner' in dog.__dict__
 
         def test_reference_reset_association_to_None(self):
             """Test that the reference field and shadow attribute are reset together"""
@@ -1347,6 +1430,7 @@ class TestAssociations:
             assert dog.owner.id == human.id
 
             dog.owner = None
+            assert any(key in dog.__dict__ for key in ['owner', 'owner_id']) is False
             assert dog.owner is None
             assert dog.owner_id is None
 
@@ -1359,6 +1443,7 @@ class TestAssociations:
             assert dog.owner.id == human.id
 
             dog.owner_id = None
+            assert any(key in dog.__dict__ for key in ['owner', 'owner_id']) is False
             assert dog.owner is None
             assert dog.owner_id is None
 
@@ -1371,6 +1456,7 @@ class TestAssociations:
             assert dog.owner.id == human.id
 
             del dog.owner
+            assert any(key in dog.__dict__ for key in ['owner', 'owner_id']) is False
             assert dog.owner is None
             assert dog.owner_id is None
 
@@ -1383,6 +1469,7 @@ class TestAssociations:
             assert dog.owner.id == human.id
 
             del dog.owner_id
+            assert any(key in dog.__dict__ for key in ['owner', 'owner_id']) is False
             assert dog.owner is None
             assert dog.owner_id is None
 
@@ -1391,6 +1478,7 @@ class TestAssociations:
             human = Human.create(first_name='Jeff', last_name='Kennedy',
                                  email='jeff.kennedy@presidents.com')
             dog = DogRelatedByEmail.create(id=1, name='John Doe', age=10, owner=human)
+            assert all(key in dog.__dict__ for key in ['owner', 'owner_email'])
             assert hasattr(dog, 'owner_email')
             assert dog.owner_email == human.email
 
@@ -1400,6 +1488,120 @@ class TestAssociations:
                                  email='jeff.kennedy@presidents.com')
             dog = DogRelatedByEmail(id=1, name='John Doe', age=10)
             dog.owner_email = human.email
+            assert 'owner' not in dog.__dict__
             dog.save()
             assert hasattr(dog, 'owner_email')
             assert dog.owner_email == human.email
+
+        @mock.patch('protean.core.entity.Entity.find_by')
+        def test_caching(self, find_by_mock):
+            """Test that subsequent accesses after first retrieval don't fetch record again"""
+            human = Human.create(first_name='Jeff', last_name='Kennedy',
+                                 email='jeff.kennedy@presidents.com')
+            dog = RelatedDog(id=1, name='John Doe', age=10, owner_id=human.id)
+
+            for _ in range(3):
+                getattr(dog, 'owner')
+            assert find_by_mock.call_count == 1
+
+    class TestHasOne:
+        """Class to test HasOne Association"""
+
+        def test_init(self):
+            """Test successful HasOne initialization"""
+            human = HasOneHuman1.create(
+                first_name='Jeff', last_name='Kennedy',
+                email='jeff.kennedy@presidents.com')
+            dog = HasOneDog1.create(id=101, name='John Doe', age=10, has_one_human1=human)
+            assert dog.has_one_human1 == human
+            assert human.dog.id == dog.id
+
+        def test_init_with_via(self):
+            """Test successful HasOne initialization with a class containing via"""
+            human = HasOneHuman2.create(
+                first_name='Jeff', last_name='Kennedy',
+                email='jeff.kennedy@presidents.com')
+            dog = HasOneDog2.create(id=101, name='John Doe', age=10, human=human)
+            assert dog.human == human
+            assert 'dog' not in human.__dict__
+            assert human.dog.id == dog.id
+            assert 'dog' in human.__dict__
+
+        def test_init_with_no_reference(self):
+            """Test successful HasOne initialization with a class containing via"""
+            human = HasOneHuman3.create(
+                first_name='Jeff', last_name='Kennedy',
+                email='jeff.kennedy@presidents.com')
+            dog = HasOneDog3.create(id=101, name='John Doe', age=10, human_id=human.id)
+            assert dog.human_id == human.id
+            assert not hasattr(dog, 'human')
+            assert 'human' not in dog.__dict__
+            assert human.dog.id == dog.id
+
+        @mock.patch('protean.core.entity.Entity.find_by')
+        def test_caching(self, find_by_mock):
+            """Test that subsequent accesses after first retrieval don't fetch record again"""
+            human = HasOneHuman3.create(
+                first_name='Jeff', last_name='Kennedy',
+                email='jeff.kennedy@presidents.com')
+            HasOneDog3.create(id=101, name='John Doe', age=10, human_id=human.id)
+
+            for _ in range(3):
+                getattr(human, 'dog')
+            assert find_by_mock.call_count == 1
+
+    class TestHasMany:
+        """Class to test HasMany Association"""
+
+        def test_init(self):
+            """Test successful HasOne initialization"""
+            human = HasManyHuman1.create(
+                first_name='Jeff', last_name='Kennedy',
+                email='jeff.kennedy@presidents.com')
+            dog1 = HasManyDog1.create(id=101, name='John Doe', age=10, has_many_human1=human)
+            dog2 = HasManyDog1.create(id=102, name='Jane Doe', age=10, has_many_human1=human)
+
+            assert dog1.has_many_human1.id == human.id
+            assert dog2.has_many_human1.id == human.id
+            assert 'dogs' not in human.__dict__
+            assert len(human.dogs) == 2
+            assert 'dogs' in human.__dict__  # Avaiable after access
+
+        def test_init_with_via(self):
+            """Test successful HasMany initialization with a class containing via"""
+            human = HasManyHuman2.create(
+                first_name='Jeff', last_name='Kennedy',
+                email='jeff.kennedy@presidents.com')
+            dog1 = HasManyDog2.create(id=101, name='John Doe', age=10, human=human)
+            dog2 = HasManyDog2.create(id=102, name='Jane Doe', age=10, human=human)
+
+            assert dog1.human.id == human.id
+            assert dog2.human.id == human.id
+
+            assert len(human.dogs) == 2
+
+        def test_init_with_no_reference(self):
+            """Test successful HasOne initialization with a class containing via"""
+            human = HasManyHuman3.create(
+                first_name='Jeff', last_name='Kennedy',
+                email='jeff.kennedy@presidents.com')
+            dog1 = HasManyDog3.create(id=101, name='John Doe', age=10, human_id=human.id)
+
+            assert dog1.human_id == human.id
+            assert not hasattr(dog1, 'human')
+
+        @mock.patch('protean.core.entity.QuerySet.filter')
+        @mock.patch('protean.core.entity.Entity.exists')
+        def test_caching(self, exists_mock, filter_mock):
+            """Test that subsequent accesses after first retrieval don't fetch record again"""
+            exists_mock.return_value = False
+            human = HasManyHuman3.create(
+                first_name='Jeff', last_name='Kennedy',
+                email='jeff.kennedy@presidents.com')
+            HasManyDog3.create(id=101, name='John Doe', human_id=human.id)
+            HasManyDog3.create(id=102, name='Jane Doe', human_id=human.id)
+            HasManyDog3.create(id=103, name='Jinny Doe', human_id=human.id)
+
+            for _ in range(3):
+                getattr(human, 'dogs')
+            assert filter_mock.call_count == 1
diff --git a/tests/support/dog.py b/tests/support/dog.py
index d5c9bc0a..51373b81 100644
--- a/tests/support/dog.py
+++ b/tests/support/dog.py
@@ -1,6 +1,6 @@
 """Support Classes for Test Cases"""
 
-from tests.support.human import Human
+from tests.support.human import Human, HasOneHuman1, HasOneHuman2, HasManyHuman1, HasManyHuman2
 
 from protean.core import field
 from protean.core.entity import Entity
@@ -39,6 +39,24 @@ class RelatedDogModel(DictModel):
         model_name = 'related_dogs'
 
 
+class RelatedDog2(Entity):
+    """This is a dummy RelatedDog2 Entity class with reference definition
+       containing the target class name as string
+    """
+    name = field.String(required=True, unique=True, max_length=50)
+    age = field.Integer(default=5)
+    owner = field.Reference('Human')
+
+
+class RelatedDog2Model(DictModel):
+    """ Model for the RelatedDog2 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = RelatedDog2
+        model_name = 'related_dogs2'
+
+
 class DogRelatedByEmail(Entity):
     """This is a dummy Dog Entity class with an association"""
     name = field.String(required=True, unique=True, max_length=50)
@@ -53,3 +71,103 @@ class DogRelatedByEmailModel(DictModel):
         """ Meta class for model options"""
         entity = DogRelatedByEmail
         model_name = 'related_dogs_by_email'
+
+
+class HasOneDog1(Entity):
+    """This is a dummy Dog Entity class to test HasOne Association"""
+    name = field.String(required=True, unique=True, max_length=50)
+    age = field.Integer(default=5)
+    has_one_human1 = field.Reference(HasOneHuman1)
+
+
+class HasOneDog1Model(DictModel):
+    """ Model for the HasOneDog1 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = HasOneDog1
+        model_name = 'has_one_dogs1'
+
+
+class HasOneDog2(Entity):
+    """This is a dummy Dog Entity class to test HasOne Association, where the associated
+       has defined a `via` attribute to finetune linkage
+    """
+    name = field.String(required=True, unique=True, max_length=50)
+    age = field.Integer(default=5)
+    human = field.Reference(HasOneHuman2)
+
+
+class HasOneDog2Model(DictModel):
+    """ Model for the HasOneDog2 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = HasOneDog2
+        model_name = 'has_one_dogs2'
+
+
+class HasOneDog3(Entity):
+    """This is a dummy Dog Entity class to test HasOne Association"""
+    name = field.String(required=True, unique=True, max_length=50)
+    age = field.Integer(default=5)
+    human_id = field.Integer()
+
+
+class HasOneDog3Model(DictModel):
+    """ Model for the HasOneDog3 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = HasOneDog3
+        model_name = 'has_one_dogs3'
+
+
+class HasManyDog1(Entity):
+    """This is a dummy Dog Entity class to test HasMany Association"""
+    name = field.String(required=True, unique=True, max_length=50)
+    age = field.Integer(default=5)
+    has_many_human1 = field.Reference(HasManyHuman1)
+
+
+class HasManyDog1Model(DictModel):
+    """ Model for the HasManyDog1 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = HasManyDog1
+        model_name = 'has_many_dogs1'
+
+
+class HasManyDog2(Entity):
+    """This is a dummy Dog Entity class to test HasMany Association, where the associated
+       has defined a `via` attribute to finetune linkage
+    """
+    name = field.String(required=True, unique=True, max_length=50)
+    age = field.Integer(default=5)
+    human = field.Reference(HasManyHuman2)
+
+
+class HasManyDog2Model(DictModel):
+    """ Model for the HasManyDog2 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = HasManyDog2
+        model_name = 'has_many_dogs2'
+
+
+class HasManyDog3(Entity):
+    """This is a dummy Dog Entity class to test HasMany Association"""
+    name = field.String(required=True, unique=True, max_length=50)
+    age = field.Integer(default=5)
+    human_id = field.Integer()
+
+
+class HasManyDog3Model(DictModel):
+    """ Model for the HasManyDog3 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = HasManyDog3
+        model_name = 'has_many_dogs3'
diff --git a/tests/support/human.py b/tests/support/human.py
index 463d114c..4fac0481 100644
--- a/tests/support/human.py
+++ b/tests/support/human.py
@@ -1,6 +1,7 @@
 """Human Support Class for Test Cases"""
 
 from protean.core import field
+from protean.core.field import association
 from protean.core.entity import Entity
 from protean.impl.repository.dict_repo import DictModel
 
@@ -19,3 +20,113 @@ class HumanModel(DictModel):
         """ Meta class for model options"""
         entity = Human
         model_name = 'humans'
+
+
+class HasOneHuman1(Entity):
+    """This is a dummy Human Entity class to test HasOne association"""
+    first_name = field.String(required=True, unique=True, max_length=50)
+    last_name = field.String(required=True, unique=True, max_length=50)
+    email = field.String(required=True, unique=True, max_length=50)
+    dog = association.HasOne('HasOneDog1')
+
+
+class HasOneHuman1Model(DictModel):
+    """ Model for the HasOneHuman1 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = HasOneHuman1
+        model_name = 'has_one_humans1'
+
+
+class HasOneHuman2(Entity):
+    """This is a dummy Human Entity class to test HasOne association
+       with a custom attribute defined in `via` argument to field
+    """
+    first_name = field.String(required=True, unique=True, max_length=50)
+    last_name = field.String(required=True, unique=True, max_length=50)
+    email = field.String(required=True, unique=True, max_length=50)
+    dog = association.HasOne('HasOneDog2', via='human_id')
+
+
+class HasOneHuman2Model(DictModel):
+    """ Model for the HasOneHuman2 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = HasOneHuman2
+        model_name = 'has_one_humans2'
+
+
+class HasOneHuman3(Entity):
+    """This is a dummy Human Entity class to test HasOne association
+       when there is no corresponding Reference defined in the target class
+    """
+    first_name = field.String(required=True, unique=True, max_length=50)
+    last_name = field.String(required=True, unique=True, max_length=50)
+    email = field.String(required=True, unique=True, max_length=50)
+    dog = association.HasOne('HasOneDog3', via='human_id')
+
+
+class HasOneHuman3Model(DictModel):
+    """ Model for the HasOneHuman3 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = HasOneHuman3
+        model_name = 'has_one_humans3'
+
+
+class HasManyHuman1(Entity):
+    """This is a dummy Human Entity class to test HasMany association"""
+    first_name = field.String(required=True, unique=True, max_length=50)
+    last_name = field.String(required=True, unique=True, max_length=50)
+    email = field.String(required=True, unique=True, max_length=50)
+    dogs = association.HasMany('HasManyDog1')
+
+
+class HasManyHuman1Model(DictModel):
+    """ Model for the HasManyHuman1 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = HasManyHuman1
+        model_name = 'has_many_humans1'
+
+
+class HasManyHuman2(Entity):
+    """This is a dummy Human Entity class to test HasMany association
+       with a custom attribute defined in `via` argument to field
+    """
+    first_name = field.String(required=True, unique=True, max_length=50)
+    last_name = field.String(required=True, unique=True, max_length=50)
+    email = field.String(required=True, unique=True, max_length=50)
+    dogs = association.HasMany('HasManyDog2', via='human_id')
+
+
+class HasManyHuman2Model(DictModel):
+    """ Model for the HasManyHuman2 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = HasManyHuman2
+        model_name = 'has_many_humans2'
+
+
+class HasManyHuman3(Entity):
+    """This is a dummy Human Entity class to test HasMany association
+       when there is no corresponding Reference defined in the target class
+    """
+    first_name = field.String(required=True, unique=True, max_length=50)
+    last_name = field.String(required=True, unique=True, max_length=50)
+    email = field.String(required=True, unique=True, max_length=50)
+    dogs = association.HasMany('HasManyDog3', via='human_id')
+
+
+class HasManyHuman3Model(DictModel):
+    """ Model for the HasManyHuman3 Entity"""
+
+    class Meta:
+        """ Meta class for model options"""
+        entity = HasManyHuman3
+        model_name = 'has_many_humans3'

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-mock mock
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
babel==2.17.0
certifi==2025.1.31
charset-normalizer==3.4.1
check-manifest==0.37
Click==7.0
coverage==4.5.1
distlib==0.3.9
docutils==0.14
exceptiongroup==1.2.2
filelock==3.18.0
flake8==3.5.0
idna==3.10
imagesize==1.4.1
iniconfig==2.1.0
Jinja2==3.1.6
MarkupSafe==3.0.2
mccabe==0.6.1
mock==5.2.0
packaging==24.2
pkginfo==1.12.1.2
platformdirs==4.3.7
pluggy==0.13.1
-e git+https://github.com/proteanhq/protean.git@df0f9bfe4230d79678e3c402b2df1ffa67b0a01f#egg=protean
py==1.11.0
pycodestyle==2.3.1
pyflakes==1.6.0
Pygments==2.19.1
pytest==7.4.4
pytest-flake8==1.0.4
pytest-mock==3.14.0
python-dateutil==2.7.3
requests==2.32.3
requests-toolbelt==1.0.0
six==1.17.0
snowballstemmer==2.2.0
Sphinx==1.8.3
sphinxcontrib-serializinghtml==2.0.0
sphinxcontrib-websupport==1.2.4
tomli==2.2.1
tox==3.1.2
tqdm==4.67.1
twine==1.11.0
urllib3==2.3.0
virtualenv==20.29.3

name: protean
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - babel==2.17.0
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - check-manifest==0.37
      - click==7.0
      - coverage==4.5.1
      - distlib==0.3.9
      - docutils==0.14
      - exceptiongroup==1.2.2
      - filelock==3.18.0
      - flake8==3.5.0
      - idna==3.10
      - imagesize==1.4.1
      - iniconfig==2.1.0
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - mccabe==0.6.1
      - mock==5.2.0
      - packaging==24.2
      - pkginfo==1.12.1.2
      - platformdirs==4.3.7
      - pluggy==0.13.1
      - py==1.11.0
      - pycodestyle==2.3.1
      - pyflakes==1.6.0
      - pygments==2.19.1
      - pytest==7.4.4
      - pytest-flake8==1.0.4
      - pytest-mock==3.14.0
      - python-dateutil==2.7.3
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==1.8.3
      - sphinxcontrib-serializinghtml==2.0.0
      - sphinxcontrib-websupport==1.2.4
      - tomli==2.2.1
      - tox==3.1.2
      - tqdm==4.67.1
      - twine==1.11.0
      - urllib3==2.3.0
      - virtualenv==20.29.3
prefix: /opt/conda/envs/protean

tests/core/test_entity.py::TestEntity::test_init tests/core/test_entity.py::TestEntity::test_individuality tests/core/test_entity.py::TestEntity::test_required_fields tests/core/test_entity.py::TestEntity::test_defaults tests/core/test_entity.py::TestEntity::test_validate_string_length tests/core/test_entity.py::TestEntity::test_validate_data_value_against_type tests/core/test_entity.py::TestEntity::test_template_init tests/core/test_entity.py::TestEntity::test_error_messages tests/core/test_entity.py::TestEntity::test_entity_inheritance tests/core/test_entity.py::TestEntity::test_default_id tests/core/test_entity.py::TestEntity::test_to_dict tests/core/test_entity.py::TestEntity::test_get tests/core/test_entity.py::TestEntity::test_get_object_not_found_error tests/core/test_entity.py::TestEntity::test_find_by tests/core/test_entity.py::TestEntity::test_find_by_object_not_found_error tests/core/test_entity.py::TestEntity::test_find_by_multiple_attributes tests/core/test_entity.py::TestEntity::test_find_by_multiple_attributes_object_not_found_error tests/core/test_entity.py::TestEntity::test_create_error tests/core/test_entity.py::TestEntity::test_create tests/core/test_entity.py::TestEntity::test_save tests/core/test_entity.py::TestEntity::test_save_validation_error tests/core/test_entity.py::TestEntity::test_save_invalid_value tests/core/test_entity.py::TestEntity::test_update_with_invalid_id tests/core/test_entity.py::TestEntity::test_update_with_dict tests/core/test_entity.py::TestEntity::test_update_with_kwargs tests/core/test_entity.py::TestEntity::test_update_with_dict_and_kwargs tests/core/test_entity.py::TestEntity::test_that_update_runs_validations tests/core/test_entity.py::TestEntity::test_unique tests/core/test_entity.py::TestEntity::test_query_init tests/core/test_entity.py::TestEntity::test_filter_chain_initialization_from_entity tests/core/test_entity.py::TestEntity::test_filter_chaining tests/core/test_entity.py::TestEntity::test_filter_chain_results_1 tests/core/test_entity.py::TestEntity::test_filter_chain_results_2 tests/core/test_entity.py::TestEntity::test_filter_chain_results_3 tests/core/test_entity.py::TestEntity::test_filter_norm tests/core/test_entity.py::TestEntity::test_exclude tests/core/test_entity.py::TestEntity::test_exclude_multiple tests/core/test_entity.py::TestEntity::test_comparisons tests/core/test_entity.py::TestEntity::test_invalid_comparison_on_query_evaluation tests/core/test_entity.py::TestEntity::test_pagination tests/core/test_entity.py::TestEntity::test_delete tests/core/test_entity.py::TestEntity::test_filter_returns_q_object tests/core/test_entity.py::TestEntity::TestState::test_default_state tests/core/test_entity.py::TestEntity::TestState::test_state_on_retrieved_objects tests/core/test_entity.py::TestEntity::TestState::test_persisted_after_save tests/core/test_entity.py::TestEntity::TestState::test_not_persisted_if_save_failed tests/core/test_entity.py::TestEntity::TestState::test_persisted_after_create tests/core/test_entity.py::TestEntity::TestState::test_copy_resets_state tests/core/test_entity.py::TestEntity::TestState::test_changed tests/core/test_entity.py::TestEntity::TestState::test_not_changed_if_still_new tests/core/test_entity.py::TestEntity::TestState::test_not_changed_after_save tests/core/test_entity.py::TestEntity::TestState::test_destroyed tests/core/test_entity.py::TestEntityMetaAttributes::test_meta_on_init tests/core/test_entity.py::TestEntityMetaAttributes::test_declared_fields_normal tests/core/test_entity.py::TestEntityMetaAttributes::test_declared_fields_with_reference tests/core/test_entity.py::TestEntityMetaAttributes::test_declared_fields_with_hasone_association tests/core/test_entity.py::TestQuerySet::test_clone tests/core/test_entity.py::TestQuerySet::test_list tests/core/test_entity.py::TestQuerySet::test_repr tests/core/test_entity.py::TestQuerySet::test_bool_false tests/core/test_entity.py::TestQuerySet::test_bool_true tests/core/test_entity.py::TestQuerySet::test_len tests/core/test_entity.py::TestQuerySet::test_slice tests/core/test_entity.py::TestQuerySet::test_caching tests/core/test_entity.py::TestQuerySet::test_cache_reset tests/core/test_entity.py::TestQuerySet::test_total tests/core/test_entity.py::TestQuerySet::test_total_with_cache tests/core/test_entity.py::TestQuerySet::test_items tests/core/test_entity.py::TestQuerySet::test_items_with_cache tests/core/test_entity.py::TestQuerySet::test_has_next tests/core/test_entity.py::TestQuerySet::test_has_next_with_cache tests/core/test_entity.py::TestQuerySet::test_has_prev tests/core/test_entity.py::TestQuerySet::test_has_prev_with_cache tests/core/test_entity.py::TestQuerySet::test_first tests/core/test_entity.py::TestQuerySet::test_first_with_cache tests/core/test_entity.py::TestCriteriaConstruction::test_empty_query tests/core/test_entity.py::TestCriteriaConstruction::test_simple_filter tests/core/test_entity.py::TestCriteriaConstruction::test_simple_exclude tests/core/test_entity.py::TestCriteriaConstruction::test_multiple_filters tests/core/test_entity.py::TestCriteriaConstruction::test_multiple_excludes tests/core/test_entity.py::TestCriteriaConstruction::test_multiple_criteria_in_filter tests/core/test_entity.py::TestCriteriaConstruction::test_multiple_criteria_in_exclude tests/core/test_entity.py::TestCriteriaConstruction::test_combined_filter_and_exclude tests/core/test_entity.py::TestCriteriaConstruction::test_combined_exclude_and_filter tests/core/test_entity.py::TestCriteriaConstruction::test_filter_with_single_Q_object tests/core/test_entity.py::TestCriteriaConstruction::test_exclude_with_single_Q_object tests/core/test_entity.py::TestCriteriaConstruction::test_filter_with_multiple_Q_objects tests/core/test_entity.py::TestCriteriaConstruction::test_exclude_with_multiple_Q_objects tests/core/test_entity.py::TestCriteriaConstruction::test_filter_with_AND tests/core/test_entity.py::TestCriteriaConstruction::test_filter_with_OR tests/core/test_entity.py::TestCriteriaConstruction::test_filter_with_multiple_ANDs tests/core/test_entity.py::TestCriteriaConstruction::test_filter_with_multiple_ORs tests/core/test_entity.py::TestCriteriaConstruction::test_filter_with_AND_OR_combination tests/core/test_entity.py::TestQ::test_deconstruct tests/core/test_entity.py::TestQ::test_deconstruct_negated tests/core/test_entity.py::TestQ::test_deconstruct_or tests/core/test_entity.py::TestQ::test_deconstruct_and tests/core/test_entity.py::TestQ::test_deconstruct_multiple_kwargs tests/core/test_entity.py::TestQ::test_reconstruct tests/core/test_entity.py::TestQ::test_reconstruct_negated tests/core/test_entity.py::TestQ::test_reconstruct_or tests/core/test_entity.py::TestQ::test_reconstruct_and tests/core/test_entity.py::TestConjunctions::test_default_AND tests/core/test_entity.py::TestConjunctions::test_default_NEG_AND tests/core/test_entity.py::TestConjunctions::test_simple_AND tests/core/test_entity.py::TestConjunctions::test_simple_OR tests/core/test_entity.py::TestConjunctions::test_AND_with_OR tests/core/test_entity.py::TestConjunctions::test_OR_with_AND tests/core/test_entity.py::TestConjunctions::test_NEG tests/core/test_entity.py::TestConjunctions::test_empty_resultset tests/core/test_entity.py::TestAssociations::TestReference::test_init tests/core/test_entity.py::TestAssociations::TestReference::test_init_with_string_reference tests/core/test_entity.py::TestAssociations::TestReference::test_save tests/core/test_entity.py::TestAssociations::TestReference::test_unsaved_entity_init tests/core/test_entity.py::TestAssociations::TestReference::test_unsaved_entity_assign tests/core/test_entity.py::TestAssociations::TestReference::test_invalid_entity_type tests/core/test_entity.py::TestAssociations::TestReference::test_shadow_attribute tests/core/test_entity.py::TestAssociations::TestReference::test_save_after_assign tests/core/test_entity.py::TestAssociations::TestReference::test_fetch_after_save tests/core/test_entity.py::TestAssociations::TestReference::test_shadow_attribute_init tests/core/test_entity.py::TestAssociations::TestReference::test_shadow_attribute_assign tests/core/test_entity.py::TestAssociations::TestReference::test_reference_reset_association_to_None tests/core/test_entity.py::TestAssociations::TestReference::test_reference_reset_shadow_field_to_None tests/core/test_entity.py::TestAssociations::TestReference::test_reference_reset_association_by_del tests/core/test_entity.py::TestAssociations::TestReference::test_reference_reset_shadow_field_by_del tests/core/test_entity.py::TestAssociations::TestReference::test_via tests/core/test_entity.py::TestAssociations::TestReference::test_via_with_shadow_attribute_assign tests/core/test_entity.py::TestAssociations::TestReference::test_caching tests/core/test_entity.py::TestAssociations::TestHasOne::test_init tests/core/test_entity.py::TestAssociations::TestHasOne::test_init_with_via tests/core/test_entity.py::TestAssociations::TestHasOne::test_init_with_no_reference tests/core/test_entity.py::TestAssociations::TestHasOne::test_caching tests/core/test_entity.py::TestAssociations::TestHasMany::test_init tests/core/test_entity.py::TestAssociations::TestHasMany::test_init_with_via tests/core/test_entity.py::TestAssociations::TestHasMany::test_init_with_no_reference tests/core/test_entity.py::TestAssociations::TestHasMany::test_caching
src/protean/core/entity.py src/protean/utils/__init__.py src/protean/core/repository/factory.py src/protean/core/field/mixins.py src/protean/core/field/base.py src/protean/core/exceptions.py src/protean/core/field/association.py src/protean/impl/repository/dict_repo.py src/protean/core/field/__init__.py
src/protean/core/entity.py src/protean/utils/__init__.py src/protean/core/repository/factory.py src/protean/core/field/mixins.py src/protean/core/field/base.py src/protean/core/exceptions.py src/protean/core/field/association.py src/protean/impl/repository/dict_repo.py src/protean/core/field/__init__.py