dddshelf / last-wishes

Last Wishes is a PHP application written following Domain-Driven Design approach. It's one of the sample applications where you can check the concepts explained in the "Domain-Driven Design in PHP" book.
https://leanpub.com/ddd-in-php
659 stars 120 forks source link

Violation of Doctrine Rule about foreign keys #16

Closed maks-rafalko closed 8 years ago

maks-rafalko commented 8 years ago

Hi,

you are mapping foreign keys to object properties which is forbidden from Doctrine point of view.

Foreign keys have no meaning whatsoever in an object model. Foreign keys are how a relational database establishes relationships. Your object model establishes relationships through object references. Thus mapping foreign keys to object fields heavily leaks details of the relational model into the object model, something you really should not do.

Could you comment it? How to deal with it without violation this rule?

Why do you need userId inside Wish entity if you have one-to-many through join table?

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/best-practices.html#don-t-map-foreign-keys-to-fields-in-an-entity

josecelano commented 8 years ago

@borNfreee I think It could be because the sample uses two approaches:

1.- User is the aggregate root for Wishes. 2.- Wishes have a different life cycle than User (not aggregated version).

In the second case you need that UserId to kmow who is the User who has that wish.

But why do not we store the complete User reference inside the Wish instead of only the UserId?

If something I do not know. I asked something similar some time ago in the DDDinPHP google group:

https://groups.google.com/forum/#!searchin/dddinphp/jose$20celano%7Csort:relevance/dddinphp/KRH7WK-PjOA/mIrDcXjlAgAJ

and @carlosbuenosvinos replied me this:

If two entities (CoAuthor and Post) do not conform an Aggregate, you should:

1.- Reference each other using their ids (PostId, CoAuthorId) 2.- Create a repository for each entity and pass it to your Application Service (this is the recommended way in the DDD community, check the Vernon's videos talking about Aggregate Design with feedback from Greg Young and Eric Evans) 3.- Add to those repositories findersBy the other EntityId.

Ocramius commented 8 years ago

There's no rule like that in the ORM: it's a choice.

maks-rafalko commented 8 years ago

@Ocramius If we are mapping foreign key to the object property, should we also define a relation? If no, how to let Doctrine know that this property is a foreign key (constraint in DB)

Ocramius commented 8 years ago

If no, how to let Doctrine know that this property is a foreign key

Does it need to be a FK? Is eventual consistency acceptable?

maks-rafalko commented 8 years ago

@Ocramius What do you mean by Eventual Consistency here?

  1. delay between some updates in DB
  2. or absence of FKs?

This question above is not related to ES/CQRS btw.

Let's have a simple example. User is a separate Aggregate (root), Wish is an Aggregate (root). According to Vaughn Vernon's talks, we can only have a UserId inside Wish aggregate and we can't have object reference.

Doing all this stuff in Doctrine, I don't understand how to achieve this - how to have UserId but not User object inside Wish and at the same time have user_id as a FK in wishes table. For this example let's say that this is a requirement to have user_id as a FK to avoid inconsistent state.

So the question is simple: does Doctrine support a mapped object property to be a FK?

From what I have read in SO, Hibernate does support it.

Ocramius commented 8 years ago

or absence of FKs?

This. This is usually acceptable when building references throughout BCs or separate aggregates.

Doing all this stuff in Doctrine, I don't understand how to achieve this

You just map the field as an integer, or as a UserId embeddable, or as a UserId custom type, then define the FK at DB-level only.

For this example let's say that this is a requirement to have user_id as a FK to avoid inconsistent state.

That's exactly what is usually denied when referencing across aggregates. It should be treated as a separate, different-address-space DB.

keyvanakbary commented 8 years ago

Really cool discussion here guys, thanks a lot for your contributions.

I think @Ocramius has answered pretty well your question. The Domain Model shouldn't be aware of the mechanics of the underlying persistence mechanism, it does not matter if is Doctrine or raw SQL. The UserId defines the identity of the User related to a specific Wish at the Domain level, it has nothing to do with the persistence mechanism. You might persist it over an In-Memory DB, Mongo or even a file where you don't have the concept of a Foreign Key. You need that so your SQL queries are optimal. Remember that he Domain does not care about low-level implementation details.

At the Infrastructure level, you can map the UserId Value Object to a SQL Foreign Key easily with Doctrine custom types. Check out this example.

I hope we've shed some light on the subject. Closing the issue now :)

maks-rafalko commented 8 years ago

Thank you all for descriptive and useful inputs!