DDD: Entity identity before being persisted

孤者浪人 提交于 2019-11-30 02:14:20

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.

nwang0

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.

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.

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.

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.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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!