DDD: Entity identity before being persisted
Asked Answered
S

5

21

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?
Sunbreak answered 21/1, 2014 at 6:22 Comment(6)
What database are you using? If you're using RavenDB or NHibernate you might be able to take advantage of the HiLo pattern, which allows you to get an id in advance, before persisting the entity to the database.Electromotor
Entity Framework 6 on top of Azure SQL Database. I don't believe it's possible to get an id before inserting something.Sunbreak
Why do you need an id before entity is persisted and why you do you have to rely on DB provided values? Please give us more details on your domain because it is possible your assumptions are just wrong.Bogeyman
@BartłomiejSzypelow: Because entities need to have identity, sometimes even from the point of instantiation. I could just rely on the reference, but I am working on a distributed system so the entity's identity needs to be qualified by an Id (as serialisation would take place). I am trying to achieve early identity generation (check out Vaughn Vernon's book: Implementing Domain-Driven Design).Sunbreak
You mentioned distributed. Yet another reason for GUID. As you mentioned IDDD, there's also a chapter about a hack with using a table mimicking a Oracle's SEQUENCE for early identity generation. I don't like this approach but how about you?Bogeyman
I'm very curious: How did you manage to let your repo set the private uniqueId field after inserting? And how does your repo access that field when updating, to know which row to update?Uptown
I
6

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.

Internationalize answered 21/1, 2014 at 22:32 Comment(0)
A
10

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.

Assistant answered 21/1, 2014 at 19:18 Comment(0)
I
7

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.

Itinerary answered 18/8, 2014 at 13:47 Comment(0)
I
6

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.

Internationalize answered 21/1, 2014 at 22:32 Comment(0)
S
5

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.

Schwa answered 21/1, 2014 at 7:16 Comment(4)
But how would the Guid value be consistent across physical boundaries? If the entity is retrieved in two different systems or at different times, their Guids would be different. They would need to be the same. I do have a separate persistent (EF) model under the repository implementations. Thanks for the answer!Sunbreak
Before the identity is persisted, it cannot be retrieved from the same store. On the other hand, there is nothing wrong for a transient entity to cross a physical boundary without even being persisted. Somewhere at some point someone can persist it and the guid would be its identity, the very same guid you gave it at the birth. Note, that it can even be persisted multiple times and get different int IDs at different systems and thus the guid still acts as the DDD identity where int IDs are only persistence IDs.Schwa
So are you saying that the same entity loaded today could have a different identity when loaded tomorrow?Sunbreak
No, the identity (guid) doesn't change. The database identity changes and it is local to the system that persists the data. You won't escape guids anyway.Schwa
U
3

Your suggested solution is perfectly valid, in my experience. I have used this approach quite a bit.

Be aware that sharing auto-increment IDs externally leaks information about your volumes. This may call for an additional GUID property sometimes - not a thing of beauty.

One-liner rewrite for your implementation

I like to neatly implement an Entity's Equals() and GetHashCode() as follows. (I'm including ToString() as I always override that as well, for easier debugging and logging.)

public override string ToString() => $"{{{this.GetType().Name} Id={this.Id}}}"; // E.g. {MyEntity Id=1} (extra brackets help when nesting)
public override bool Equals(object obj) => this.Id == default ? ReferenceEquals(this, obj) : this.Id == (obj as MyEntity)?.Id;
public override int GetHashCode() => this.Id == default ? RuntimeHelpers.GetHashCode(this) : this.Id.GetHashCode();

ReferenceEquals() vs base.Equals() is an interesting discussion. :)

Alternative solution

If you want something even better, here is another suggestion. What if you could have a value that is (for our intents and purposes) as good as a GUID, but fits into a long? What if it was also newable without the need for a repository?

I realize that your table might only currently fit an int as its PRIMARY KEY. But if you are able to alter that to long, or for your future tables, my suggestion might be of interest to you.

In Proposal: locally unique GUID alternative, I explain how to build a locally unique, newable, strictly ascending 64-bit value. It replaces the auto-increment ID + GUID combination.

I have always disliked the idea of having both a numeric ID and a GUID. It's like saying: "This is the entity's unique identifier. And... this is its other unique identifier." Sure, you can keep one out of the domain and the language, but that leaves you with the technical issue of simultaneously managing and hiding the extra numeric ID. If you prefer to have a single ID that is both domain-friendly (newable without a repository, and named ID rather than GUID) and database-friendly (small, fast, and ascending), try my suggestion.

I warn you that the implementation can be tricky to do right, especially with regards to collisions and thread-safety. I have not gotten around to posting any code for it yet.

Uptown answered 29/10, 2017 at 13:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.