-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
[WIP] Nullable embedded objects. #1275
Conversation
There's a lot of things to be improved (mostly on UnitOfWork), but it's fully working with basic tests. The idea was to have a simple (and clean) way to override the nullable property of embedded objects, so I've added the nullable attribute on the Embedded annotation and it have 3 possible values: - NULL: The nullable option that was defined on the attributes of the embeddable class won't be overriden; - TRUE: All attributes of the embeddable class will be marked as nullable; - FALSE: All attributes of the embeddable class will be marked as non-nullable. The usage is quite simple and can be seen at the ValueObjectsTest::testCRUDOfNullableEmbedded() test case.
Hello, thank you for creating this pull request. I have automatically opened an issue http://www.doctrine-project.org/jira/browse/DDC-3529 We use Jira to track the state of pull requests and the versions they got |
@@ -242,6 +243,48 @@ public function testThrowsExceptionOnInfiniteEmbeddableNesting($embeddableClassN | |||
)); | |||
} | |||
|
|||
public function testCRUDOfNullableEmbedded() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lcobucci consider splitting this test into smaller tests depending on each other via @depends
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect, I followed the other test case (testCRUD). But using @depends
is way better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll also change the name of the classes that I've created on that file to match the issue on JIRA (DDC-3529)
* @param array $data | ||
* @param ClassMetadata $class | ||
*/ | ||
private function removeNullableEmbeddedReferences(array &$data, ClassMetadata $class) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Ocramius I've tried to keep the things simple here, but if we have a huge amount of data or a large result set I think we might have a little delay to process. I just don't have any idea to improve this =/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about something like this:
public function createEntity($className, array $data, &$hints = array())
{
// ...
//$this->removeNullableEmbeddedReferences($data, $class);
foreach ($data as $field => $value) {
if (null === $value && $this->isNullableEmbeddedField($class, $field)) {
continue;
}
if (isset($class->fieldMappings[$field])) {
$class->reflFields[$field]->setValue($entity, $value);
}
}
// ...
}
private function isNullableEmbeddedField(ClassMetadata $class, $field)
{
return isset(
$class->fieldMappings[$field]['declaredField'],
$class->embeddedClasses[$name = $class->fieldMappings[$field]['declaredField']]
) ? true === $class->embeddedClasses[$name]['nullable'] : false;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @1ed, its a good alternative. I'll play a lit bit now 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Ocramius, @1ed and @beberlei what do you think to extract that logic from UnitOfWork
into ClassMetadata
? It seems to be proper location, considering that UnitOfWork
is already huge.
I thought something like ClassMetadata::updateEntity($entity, array $data)
or ClassMetadata::applyData($entity, array $data)
.
👍 that would be great if it could fit in 2.5 |
… entity to the class metadata.
I have implemented @1ed suggestion with that minor refactor. |
@lcobucci What is the status of this? |
@@ -3310,4 +3316,29 @@ public function getSequencePrefix(AbstractPlatform $platform) | |||
|
|||
return $sequencePrefix; | |||
} | |||
|
|||
public function applyData($entity, array $data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method naming is a bit weird: can you document it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll rename it. It is really weird.
What do you think about populateEntity
?
It is "working" @alsar, but we have to clean some stuff to make it better. I'll send some commits right now. |
this case looks weird to me. It changes an optional field to a required one in the embedded object |
@stof please give us more arguments, let's debate 😄. In 6340eec I've added a test for that cenario and I believe that in the "country" context is the only way to make the embeddable required, but when dealing with complex value object I agree that it can look weird. The main question that comes to me is: should we have really complex value objects? |
I tend to agree with @stof about the |
*/ | ||
public function inlineEmbeddable($property, ClassMetadataInfo $embeddable) | ||
public function inlineEmbeddable($property, ClassMetadataInfo $embeddable, $nullable) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that extra parameters really required? Can't you use $this->embeddedClasses[$property]['nullable']
in this method instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, my mistake.
@deeky666 the feature that I need is the I can remove the |
So, do we remove the |
@lcobucci lets remove |
I've removed the |
This is a chicken-egg problem, folks. Not going to happen unless somebody finds a solution to the data consistency problem first. |
Also: using the |
I think it might be best to use an extra column that has a 1 or 0 to check whether an embedded property should be null or not. As a side note, you can already do this in user-land to implement this functionality. |
@schmittjoh How do I do it transparently in the userland? Could you give me a hint? |
Something along these lines should work: class Foo
{
private $embeddable;
private $isEmbeddableSet;
public function getEmbeddable()
{
if ( ! $this->isEmbeddableSet) { return null; }
return $this->embeddable;
}
} |
We actually don't need a modified Hydrator as you can see in the files I've changed. The MetadaFactory is easily injected on EntityManager configuration as you can see here: https://github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/EntityManager.php#L147 You can also create an extension of the AnnotationDriver and pass it to your configuration too. |
@lcobucci thanks, I will look into this. |
Does somebody know how they resolve this issue in Hibernate? |
hmmm... very tricky :| is there any known existing extension for this problem? |
@Ocramius I don't understand the argument that even with someone manually modifying database the consistency of application should still be valid. To make an analog, you could in your domain specify that the only allowed values of an object are 1, 2 and 3, and then someone goes to database and update the value to 4. It would still produce invalid state, and could still have catastrophic consequences. That is not related to nulls or multiple columns to represent one value, but to modifying application storage with inconsistent data. Its also almost the same as saying that if someone delete a mapped column it should still work. As @lcobucci said, it would be perfect fine to define it nullable in one place and not nullable in other places, and if we eventually follow the idea of having this as a separate table, it would still need to follow the same schema, probably meaning that all columns would be nullable or following the mapping of the embeddable. This basically means that the only way to store embeddables in relational databases is by using columns, not tables. What I see much more as inconsistency is how it is today, that doctrine builds a object for you even if when you persisted your root object the property was null. Since the embeddable release I always wanted this feature. |
I've been testing this branch and I noticed that it doesn't work with multi-level embeddables. It creates the object no matter if all columns of all nested embeddables are null. |
What about allowing nullable embedables only for DBs that have constraints? |
Nope.
|
@FabioBatSilva I didn't tested that kind of thing but I don't think would be something difficult to achieve. I also believe that @Ocramius's statement was good enough to end this (as a standard feature):
About @fprochazka's comment: We cannot expect to push a lot of logic to doctrine's shoulders (it already have too much to carry now). I believe that we need to find a way to solve #4568 and also that we should treat this feature as an extension (if it would be really needed after solving that issue). |
@lcobucci I think you are accepting this argument too fast. Allowing mappings to happen doesn't mean that this will make a SQL DBMS to be used as noSQL DBMS. By fixing this problem you don't start storing documents or objects in database. Its still tables and rows. The thing is that the mapper as we have now, does not support proper mapping of optional objects. The goal of the mapper as I understand is to map any design you can possibly have in your code to relational databases, and having optional (nullable) properties in our objects is far too common to be ignored. Its the same as when we didn't have the embeddables feature and we would choose to design everything with scalar types just to fit the mapper. Another valid approach, would be allowing the storage of objects that follow the same interface in those embeddable mapped properties (something similar as we have with inheritance mapping for entities) and have either a differentiation column or a class name and then use the null object pattern, but I'd vote for your current approach. PS: Maybe you mispinged me in the message above? |
Oops you're correct @fabiocarneiro sorry hehehe. No, I'm not accepting it too fast (it was a long discussion already 😄). With #4568 we will be able to use the |
@lcobucci as far as I understood, we could use attribute overrides to force all mappings of embeddables to be null, meaning that it could be different configuration in different columns, but that's only 20% of the problem. How does that prevent the hydration of the object that has both columns nullable? For my value objects, null are inconsistent state, but for the entity that has it as property its not. What I really want is the object not to be created rather than its properties having null.
I'd treat this as a BUG. The mapping is valid, you persist the object with a null property and when you retrieve it from the repository, doctrine has created a version that is different from the persisted version. Instead of null in that same property you will get an object in a inconsistent state. |
But @fabiocarneiro, that's exactly what's being stated on #4568. I think we should see with @Ocramius || @guilhermeblanco || @schmittjoh what we could do about that (but on the scope of that issue and not on this PR). |
There are database features that allow these kind checks in various ways at db level. Just because doctrine can not infer a schema that facilitates such features, doesnt mean it should assume these features are not available. also the mapping to schema conversion is just a secondary feature of doctrine to ease database creation and not to replace each and every (vendor-dependant) db feature. i would really like this feature to be added. the eal problem comes when all fields in the embeddable are nullable and the embeddable itself is nullable, because we can not distinquish between a null embeddable and an embeddable containing only null values. a separate boolean-like column is required in this case. |
That is a sensible solution, although a joined one-to-one is simpler and better for this scenario (and adding a 1-to-1 join is a low cost operation, if indexed correctly) |
Sorry to up this, I read back a lot, but it is possible I have missed something. What about this?: If all fields are null, check the embeddable configuration. If the configuration specifies that it is nullable, set is as $address = new Address(); // all fields nullable, and null
$order->setShippingAddress($address);
$em->persist($order);
$em->flush();
$em->refresh($order);
// null (assuming the configuration specifies nullable=true)
$order->getShippingAddress()
// instanceof Address, empty (assuming the configuration specifies nullable=null)
$order->getShippingAddress() $order->setShippingAddress(null);
$em->persist($order);
$em->flush(); // exception if nullable=null About enforcing consistency: If a non null value is received, check the individual field mappings, assert that every required field is set. Otherwise throw an exception. // not nullable field city is missing
$address = new Address();
$address->setStreet('something');
$order->setShippingAddress($address);
$em->persist($order);
$em->flush(); // exception I know we can't enforce it on a DB level, but unless you are tampering with the data outside of the ORM, it will be fine. This of course should be mentioned in the docs, but otherwise I don't see it as a problem. There is already a similar issue with single table inheritance... |
It is a possible implementation, but nullability on all embeddable fields would then be disallowed (need at least one non-null field). That's a good idea for 3.x, so if anybody wants to help out, start from |
The idea was to have a simple (and clean) way to override the nullable property of embedded objects, so I've
added the
nullable
attribute on the@Embedded
annotation and it have 3 possible values:FALSE: All attributes of the embeddable class will be marked as non-nullable.There's a lot of things to be improved (mostly on UnitOfWork), but it's fully working with basic tests as you can see on ValueObjectsTest::testCRUDOfNullableEmbedded() case.
I would appreciate a lot your opinions!