-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Blending ORM and ODM but keeping the Domain Model clean #2
Comments
Regarding your mapping suggestion @nidup: I'm trying the other way around: So I need an extra "Document(s)" Mapping type, right? Do you have a (XML?) mapping example from your use case? |
Thank you for opening this discussion and for sharing these links! Pretty sure that @jjanvier will be very interested by this issue too, he very well know the topic and he's preparing a talk about our hybrid storage for a sfpot this week http://www.meetup.com/fr-FR/afsy-sfpot/events/230197553/ ;) To give a bit more details, we use a hybrid storage in the context of Akeneo Product Information Management system (https://www.akeneo.com/). Depending on the amount of product data, we can use,
When we implemented the hybrid MongoDB storage, the difficulty was to,
To do so we introduced a StorageUtilsBundle and we configure the storage to be used like this https://github.com/akeneo/pim-community-dev/blob/master/app/config/pim_parameters.yml#L35 Let's take the product MongoDB mapping https://github.com/akeneo/pim-community-dev/blob/master/src/Pim/Bundle/CatalogBundle/Resources/config/model/doctrine/Product.mongodb.yml This product document is linked to a single family (managed by Doctrine ORM),
It's also linked to several categories (managed by Doctrine ORM),
These new We store entity ids in the MongoDB document and we use MongoDB ODM events to convert these ids into lazy entities, To build collection of lazy loaded referenced entities, we use https://github.com/akeneo/pim-community-dev/blob/master/src/Akeneo/Bundle/StorageUtilsBundle/Doctrine/MongoDBODM/Collections/ReferencedCollection.php We also use the following listener to be able to use an interface resolved as a Doctrine ORM entity in a Doctrine MongoDB ODM mapping (for instance The StorageUtilsBundle is already open source and not coupled to our PIM business code but unfortunately, for now, it's part of our main dev repository. If after your first tests, you're interested to use it, don't hesitate to ping me, we can extract this bundle in a dedicated repository with a subtree split and register it on packagist to make it easily usable in others projects. |
Thanks @nidup for this detailled answer! Looking at your mapping example: So you still have an "extra Field / Collection" to keep the IDs that have to be converted in some way? This is what I was concerned about from a Domain-Driven Design POV. Isn't the Coming from twitter discussion w/ @carlosbuenosvinos @eneko here: |
My current workaournd looks like this: I store my
The namespaces look like this:
I think the idea is similar to the "Aggregates and Event Sourcing A+ES" chapter of the red book by @VaughnVernon . With ORM an Event could update a Query (READ) Model that writes the historical data / Aggregate into a database VIEW. Using embed references in ODM is great to take a final snapshot. If I ever need more data from the Customer I would inject the CustomerRepository into my Query (READ) Model and get it from the ORM. By storing the ID inside the reference I can still query for "OrdersByCustomer". Actually I do have an extra column too but it does not feel like polluting the DomainModel for a persistence purpose. Though I will still have to use custom mappers or types like @nidup did to convert those IDs into Entities when calling collections on my DomainModel e.g.: class Customer
{
private $orders; // some EXTRA-LAZY Doctrine ArrayCollection
public function getOrder(OrderId $orderId)
{
return $this->orders[$orderId];
}
} What do you think of this approach? |
Hello @webdevilopers Maybe this presentation will interest you. https://speakerdeck.com/jjanvier/products-storage-at-akeneo-pim It describes how we store our products at Akeneo PIM. At the end of the presentation, you have the most interesting part of the code that are used to link ORM entities to MongoDB documents. |
Thanks @jjanvier ! At the bottom line you are using an approach to the DoctrineExtensions by @Atlantic18: But you also support converting collections to Entities? Still you have an extra Field e.g. This is what I am mainly concerned about from a DDD POV and that's why I asked @carlosbuenosvinos what he thinks about this approach. Maybe the other contributors @eddmann, @keyvanakbary and @theUniC have an opinion on this. Technically I agree with your approach and would handle it the same way! |
One final way to solve this and keep the |
Currently looking at a CQRS post by @martinfowler: He states:
If I regard my Domain Model being a representation I could build my Order Document this way instead: class Order
{
private $customerId; // Used to get the related Entity - just the ID or the CustomerId Value Object?
private $customer; // The related Entity
private $customerName; // snapshot / aggregate / historical data - bad example since `name` will mostly stay the same :)
private $billingAddress; // snapshot / aggregate / historical data
} |
Hi @webdevilopers! I'm very interested in the DDD but I don't know it enough for now, I'm actively documenting on this topic :) (so this answer may contain some misuses of the terminology or weird approach 😜 ).
Absolutely, I do agree with you, the categoryIds field is a persistence concern and should not pollute the domain model. When we wrote the original implementation (in 2014, times fly!), we tried to find a very technical solution about how to "build" and "use" this relation between Product Document and Category Entity without taking too much care about the impact on the domain models. One of the biggest difficulty we had was to build an efficient and abstract way to do complex queries on these objects from the business layer POV. This point also pushed us to pollute the Product model by adding a "normalizedData" field to help us to write MongoDB queries https://github.com/akeneo/pim-community-dev/blob/master/src/Pim/Bundle/CatalogBundle/Resources/config/model/doctrine/Product.mongodb.yml#L78 (practice quite common when using a MongoDB storage to store the data in a way which simplifies the usage). In a perfect world, imvho, you should not even care of the persistence of your Domain models when defining and coding them because persistence is a pure technical implementation detail. In real life, it's not that easy to decouple these objects from Doctrine ORM, I'm not even sure that's a good idea to try to directly store these domain models as entity/document.
I think this approach is really really interesting, your persistence classes can rely on these Entity/Document for their very specific concerns (fields used to keep a relation between storage, reference collection, etc) keeping a clean DomainModel. Your business code could define and rely on a "repository" interface allowing to fetch domain models. I recently did experiments around Hexagonal Architecture (Ports & Adapters) on a reporting project and achieved to easily decouple Domain from Persistence using a quite similar strategy. A DatabaseAdapter taking care of implementing querying and converting data from storage "objects" into Domain models (I didn't used Doctrine ORM for this project, I used https://github.com/pomm-project in fact only the foundation part + custom code for converting objects). Thanks again for opening this discussion, I'm looking forward to see other opinions / ideas. |
I'm as excited as you are @nidup . BTW just found this quote here by @vkhorikov: ;) Not sure if the approach of a Database related Wrapper like Wrapper seem to be an appraoch indeed: |
BTW the extra IDs are mentioned in the DDD in PHP book too. There they are used as surrogateIds. The book also suggests to using a Layer SuperType: Maybe the "polluting extra id" could be moved to an abstract class e.g. "HybridDomainObject" which then can be extended by the |
Nice article here btw:
|
Hi, thanks for the invitation, @webdevilopers If I understood correctly from the conversation, you suggest creating sub-classes to keep domain classes clean, smth like this: IMO, that's a good solution. In fact, that is exactly how ORMs like Hibernate approach the problem: they create sub-classes (proxies) for your domain classes at runtime and use them to handle persistence logic. The only concern I have about this solution is complexity. Implementing such proxies manually can become a hurdle and one should weight pros and cons of this approach really carefully. It might be that purity you will get out of it doesn't worth the amount of work it requires. But this is worth trying anyway, at least as a research project. BTW, I agree with your opinion on Ids in domain classes. Here I wrote a post on that: http://enterprisecraftsmanship.com/2016/03/08/link-to-an-aggregate-reference-or-id/ |
Welcome @vkhorikov ! Are there Events and Event Subscribers in (N)Hibernate too? Do you have any experiences with ORM and ODM (OGM) hybrids in Java or .NET? I really liked your article on this topic. Good to know I'm not the only one concerned about it! :) Here is my current PHP approach with Doctrine: As I described before in my use case I have a In my example I kept the property on the Now I changed the Event Subscriber from the Cookbook to "convert" / "transform" this field into a Document Collection instead of using the extra field: This works fine so far. It's just a rapid prototype. I will heavily test it. But it looks promising in the first place. |
I've improved the ORM listener and added an ODM listener to get a referenced Entity So far everything seems to work fine without any extra ID. What do you think? Any bad practice using Doctrine Events this way? Any Hibernate experts here? @thmuch and @brmeyer maybe? :) |
Actually it looks like it is very similar in @hibernate: No wonder since / that @doctrine was heavily inspired by it. |
@webdevilopers Yes, the concept of events and event listeners is the same in Hibernate and NHibernate. The code looks good to me. I don't have experience in PHP, though, so take my words with caution. The thing I think you need to verify is how your solution works with deep class graphs. You need to see if it would load the full object graph at once when you fetch a single entity or would load them lazily, just-in-time, as you traverse the graph. |
Definitely @vkhorikov ! Lazy loading looks fine so far. Currently these listeners run on all ORM Entities and ODM Documents but can be catched. @doctrine ORM offers a listener that can be mapped to the Entity directly via XML for instance: I'm still looking for an equivalent for ODM Documents: |
Just discovered this project by @ElectricMaxxx: Maybe he can give a short comment to our discussion too. |
@ElectricMaxxx Can you tell us if your solution Also discussed here: |
No. There is no extra Field. My solution tries to connect entitys/documents as One-to-One relation, that live in different doctrines. The reference is a pure configuration on the entity/document very close to the configuration we used to do in every doctrine. And there is a configuration to know the manager on the oposite side. So if a entity with an referenced document is loaded the document will be added as an Proxy and will awake on an get*() call. The information about the loading process comes by an simple event hook.
I tried to do it all by hooks, but that fails as every find() in one manager triggers a find() on the other side. So you can see my Solution as a kind of LazyLoading for cross doctrine references.
I tried it in production for ORM <-> PHPCR and it works fine. Tests are working too on both. And it should work on each doctrine that behaves like the ORM.
|
Thanks for the update on your approach. Though @stof stated that using the same field is not a clean design I prefer this solution from a DDD DomainModel POV - at least if you have to really blend Entities w/ Documents: Personally I use the same workaround @stof mentioned here and create an extra Still tried to check out the Doctrine References Extension but couldn't make the XML mapping working: |
There is an interesting article by @mathiasverraes: In "Persistence" he needs an "annoying" extra-column too:
|
I would love to hear @Ocramius opinion on our approaches e.g. without an extra field and from a DDD POV. Since I've seen him on the He is familiar with @doctrine best practices and he knows its limits well. Would you lend us an ear Marco? |
Hi guys, I haven't been following this conversation closely enough, but I will just Unfortunately don't know enough about PHP, but if you can use private Best,
|
@webdevilopers fields are not a big deal, in my opinion. As long as the public API stays clean, you are safe from complications. As @VaughnVernon says, you can just hide it as a detail. |
Did @VaughnVernon himself just comment on our issue? :) Actually I feel a little bit relieved. Indeed there are more interesting "problems"! "Marry" is a good term here since a marriage can be some kind of compromise sometimes too. And thank you very much for @Ocramius confirming this approach for @doctrine. Normally these extra fields are just the IDs of the related Entity e.g. an |
I'm not worried about the internal state. Again, focus on the public API. You can either map a VO or just an integer and then convert back/forth to VO inside the API |
When trying to use Doctrine ORM Entities with ODM Documents (e.g. MongoDB) most solutions adding extra fields:
For me this feels like "polluting" the Domain Model with extra properties to fit the infrastructure.
So I am currently looking for a way to keep the Domain Models clean:
A possible workaround could be a custom mapper:
A different solution could be treating referenced entities as Value Objects which at the same time could serve as a historical "query database".
For instance:
Instead of linking an ODM
Order
Document (still an "Entity" in DDD though) with an ORMCustomer
you introduce aCustomer
Value Object that keeps Id and full name. Maybe the delivery and / or invoice address too.Remember kids:
The text was updated successfully, but these errors were encountered: