Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DDD: Entity identity before being persisted

In Domain Driven Design, one of the defining characteristic of an Entity is that it has an identity.

Problem:

I am not able to provide a unique identity to Entities on instance creation. This identity is only provided by the repository once the entity is persisted (this value is provided from the underlying database).

I cannot begin to use Guid values at this point. The existing data is stored with int primary key values and I cannot generate a unique int on instantiation.

My solution:

  • Each Entity has an identity value
  • The identity is only set to a real identity once persisted (provided by the database)
  • The identity is set to default when instantiated before persistence
  • If the identity is default, entities are comparable through reference
  • If the identity is not default, entities are comparable through identity values

Code (the abstract base class for all entities):

public abstract class Entity<IdType>
{
    private readonly IdType uniqueId;

    public IdType Id
    {
        get 
        { 
            return uniqueId; 
        }
    }

    public Entity()
    {
        uniqueId = default(IdType);
    }

    public Entity(IdType id)
    {
        if (object.Equals(id, default(IdType)))
        {
            throw new ArgumentException("The Id of a Domain Model cannot be the default value");
        }

        uniqueId = id;
    }

    public override bool Equals(object obj)
    {
        if (uniqueId.Equals(default(IdType)))
        { 
            var entity = obj as Entity<IdType>;

            if (entity != null)
            {
                return uniqueId.Equals(entity.Id);
            }
        }

        return base.Equals(obj);
    }

    public override int GetHashCode()
    {
        return uniqueId.GetHashCode();
    }
}

Question:

  • Would you consider this to be a good alternative to generating Guid values on instance creation?
  • Are there better solutions out there for this problem?
like image 951
Dave New Avatar asked Jan 21 '14 06:01

Dave New


4 Answers

You can use a sequence generator to generate unique int/long identifiers when you instantiate an entity object.

The interface looks like:

interface SequenceGenerator {
    long getNextSequence();
}

A typical implementation of a sequence generator uses a sequence table in the database. The sequence table contains two columns: sequenceName and allocatedSequence.

When getNextSequence is called first time, it writes a large value (say 100) to the allocatedSequence column and return 1. The next call will return 2 without need to access the database. When the 100 sequences runs out, it reads and increments the allocatedSequence by 100 again.

Have a look at the SequenceHiLoGenerator in Hibernate source code. It basically does what I described above.

like image 153
nwang0 Avatar answered Nov 14 '22 23:11

nwang0


I am not able to provide a unique identity to Entities on instance creation. This identity is only provided by the repository once the entity is persisted (this value is provided from the underlying database).

How many places do you have where you create a list of entities of the same type and you have more then one entity with default id?

Would you consider this to be a good alternative to generating Guid values on instance creation?

If you do not use any ORM your approach is good enough. Especially, when an implementation of identity map and unit of work is your respomsibility. But you have fixed Equals(object obj) only. GetHashCode() method does not check if uniqueId.Equals(default(IdType)).

I suggest you look into any open-source "Infrastructure Boilerplate" like Sharp-Architecture and check their implementation of the base class for all domain entities.

I am used to writing custom implementations of Equals() for domain entities, but it can be superfluous when it comes to usage of ORM. If you use any ORM it provides implementations of identity map and unit of work patterns out of the box and you can rely on them.

like image 37
Ilya Palkin Avatar answered Nov 14 '22 23:11

Ilya Palkin


I believe the solution to this is actually rather straightforward:

  • As you mention, entities must have an identity,

  • Per your (perfectly valid) requirements, the identity of your entities is assigned centrally by the DBMS,

  • Hence, any object that hasn't yet been assigned an identity is not an entity.

What you're dealing with here is a kind of Data Transfer Object type, which doesn't have an identity. You should think about it as transferring data from whatever input system you use to the domain model via the repository (which you need as an interface here for the identity assignment). I suggest you create another type for these objects (one that doesn't have a key), and pass it to the Add/Create/Insert/New method of your repository.

When the data doesn't need much preprocessing (i.e. doesn't need to be passed around much), some people even omit DTOs and pass the various pieces of data via the method arguments directly. This is really how you should be looking at such DTOs: as convenient argument objects. Again, notice the lack of a "key" or "id" argument.

If you need to manipulate the object as an entity before inserting it in the database, then DBMS sequences are your only option. Note that this is usually relatively rare, the only reason you might need to do this is if the results of these manipulations end up modifying the object state such that you'd have to make a second request to update it in the database, which you'd surely prefer to avoid.

Very often, "creation" and "modification" functionality in applications are distinct enough that you'll always add the records for the entities in the database first before retrieving them again later to modify them.

You'll undoubtedly be worried about code-reuse. Depending on how you construct your objects, you'll probably want to factor-out some validation logic so that the repository can validate the data before inserting it into the database. Note that this is usually unnecessary if you're using DBMS sequences, and might be a reason why some people systematically use them even if they don't strictly need them. Depending on your performance requirements, take the comments above into consideration, as a sequence will generate an additional round trip that you'll often be able to avoid.

  • Example: Create a validator object that you use in both the entity and the repository.

Disclaimer: I don't have in-depth knowledge of canonical DDD, I wouldn't know if this was really the recommended approach, but it makes sense to me.

I'll also add that in my opinion, changing the behavior of Equals (and other methods) based on whether the object represents an entity or a simple data object is simply not ideal. With the technique you use, you also need to ensure that the default value you use for the key is properly excluded from the value domain in all domain logic.

If you still want to use that technique, I suggest using a dedicated type for the key. This type would box/wrap the key with additional state indicating whether or not the key exists. Note that this definition resembles Nullable<T> so much that I'd consider using it (you can use the type? syntax in C#). With this design, it's clearer that you allow the object not to have an identity (null key). It should also be more obvious why the design is not ideal (again, in my opinion): You're using the same type to represent both entities and identity-less data transfer objects.

like image 6
tne Avatar answered Nov 15 '22 00:11

tne


I cannot begin to use Guid values at this point.

Yes you can and that would be an alternative. Guids would not be your database primary keys but rather would be used at the domain model level. In this approach you could even have two separate models - a persistence model with ints as primary keys and guids as attributes and another model, the domain model, where guids play the role of identifiers.

This way your domain objects can get their identities once created and persistence is just one of minor business concerns.

The other option known to me is the one you described.

like image 5
Wiktor Zychla Avatar answered Nov 14 '22 22:11

Wiktor Zychla