I have a one-to-one relationship in my Symfony2 Project where a Question
has a reference to a Video
- both have a created and updated Gedmo\Timestampable behaviour which is basically working as expected. But a little bit too much:
When deserializing the Question
with the attached Video
(as ID only to avoid other changes in the Video metadata) the Video
document always gets an update both on the created
and the updated
field. This doesn't seem right. I may understand why the updated field gets a new date - even if actually nothing has changed on the object itself, but why created ?
This is my code (simplyfied):
Class Question:
<?php
/**
* Class Question
*
* @Serializer\AccessorOrder("alphabetical")
* @MongoDB\Document(
* collection="Quiz",
* repositoryClass="MyNamespace\Bundle\QuizBundle\Repository\QuestionRepository",
* )
* @package MyNamespace\Bundle\QuizBundle\Document
*/
class Question
{
/**
* @var \MongoId
* @MongoDB\Id(strategy="auto")
* @Serializer\Type("string")
* @Serializer\Groups({
* "quiz_admin_list",
* "quiz_admin_detail"
* })
*/
protected $id;
/**
* @var \DateTime
*
* @Assert\Date(
* message = "quiz:constraints.model.question.created.invalid"
* )
* @Serializer\Type("DateTime<'U'>")
* @Serializer\Accessor(getter="getCreated", setter="setCreatedEmpty")
* @Serializer\Groups({
* "quiz_admin_list",
* "quiz_admin_detail"
* })
* @Gedmo\Timestampable(on="create")
* @MongoDB\Date
*/
protected $created;
/**
* @var \DateTime
*
* @Assert\Date(
* message = "quiz:constraints.model.question.updated.invalid"
* )
* @Serializer\Type("DateTime<'U'>")
* @Serializer\Accessor(getter="getUpdated", setter="setUpdatedEmpty")
* @Serializer\Groups({
* "quiz_admin_list",
* "quiz_admin_detail"
* })
* @Gedmo\Timestampable(on="update")
* @MongoDB\Date
*/
protected $updated;
/**
* @var Video
*
* @Serializer\Type("MyNamespace\Bundle\CoreMediaAdminBundle\Document\Video")
* @Serializer\Groups({
* "quiz_admin_list",
* "quiz_admin_detail"
* })
* @MongoDB\ReferenceOne(
* targetDocument="MyNamespace\Bundle\CoreMediaAdminBundle\Document\Video",
* cascade={"all"}
* )
*/
protected $answerVideo;
}
Class Video:
<?php
/**
* Class Video
* @Serializer\AccessorOrder("alphabetical")
* @MongoDB\Document(
* collection="CoreMediaAdminVideo",
* repositoryClass="MyNamespace\Bundle\CoreMediaAdminBundle\Repository\VideoRepository",
* )
* @Vich\Uploadable
* @package MyNamespace\Bundle\CoreMediaAdminBundle\Document
*/
class Video
{
/**
* @MongoDB\Id(strategy="auto")
* @Serializer\Type("string")
* @Serializer\Groups({
* "core_media_list",
* "core_media_search",
* "core_media_video_list",
* "core_media_video_detail"
* })
*/
protected $id;
/**
* @Vich\UploadableField(
* mapping = "core_media_admin_video",
* fileNameProperty = "fileName"
* )
* @Serializer\Exclude
* @var File $file
*/
protected $file;
/**
* @MongoDB\Field(type="string")
* @Serializer\Type("string")
* @Serializer\Groups({
* "core_media_list",
* "core_media_search",
* "core_media_video_list",
* "core_media_video_detail"
* })
*/
protected $mimeType;
/**
* @var String
*
* @Assert\NotBlank(
* message = "core.media.admin:constraints.model.base.title.not_blank"
* )
* @Serializer\Type("string")
* @Serializer\Groups({
* "core_media_list",
* "core_media_search",
* "core_media_video_list",
* "core_media_video_detail"
* })
* @MongoDB\Field(type="string")
*/
protected $title;
/**
* @var \DateTime
*
* @Assert\Date(
* message = "core.media.admin:constraints.model.base.date.invalid"
* )
* @Serializer\Type("DateTime<'U'>")
* @Serializer\Accessor(getter="getCreated", setter="setCreatedEmpty")
* @Serializer\Groups({
* "core_media_list",
* "core_media_search",
* "core_media_video_list",
* "core_media_video_detail"
* })
* @Gedmo\Timestampable(on="create")
* @MongoDB\Date
*/
protected $created;
/**
* @var \DateTime
*
* @Assert\Date(
* message = "core.media.admin:constraints.model.base.date.invalid"
* )
* @Serializer\Type("DateTime<'U'>")
* _Serializer\Accessor(getter="getUpdated", setter="setUpdatedEmpty")
* @Serializer\Groups({
* "core_media_list",
* "core_media_search",
* "core_media_video_list",
* "core_media_video_detail"
* })
* @Gedmo\Timestampable(on="update")
* @MongoDB\Date
*/
protected $updated;
/**
* @var \DateTime
*
* @Assert\Date(
* message = "core.media.admin:constraints.model.base.date.invalid"
* )
* @Serializer\Type("DateTime<'U'>")
* @Serializer\Groups({
* "core_media_list",
* "core_media_search",
* "core_media_video_list",
* "core_media_video_detail"
* })
* @Gedmo\Timestampable(on="update", field={"title", "tags", "comment", "dataOrigin", "description", "videoMetaData", "mimeType", "fileName", "file" })
* @MongoDB\Date
*/
protected $updatedContent;
}
The interesting thing is, no changes are made on Video
objects during deserialization - there is only the update Query to set the created
and updated
fields of the Video.. I also tested the field parameter for Timestampable to force an update only when one of those fields get an update but this seems to be ignored completely.
Here is also the JSON which is deserialized and the corresponding MongoDB queries:
{
"id": "547f31e650e56f2c26000063",
"question_id": 12,
"question_text": "Wer einen Gemüsegarten hat, sollte wissen, dass Schnecken…?",
"answer_text": "test",
"answer_video": {
"id": "547f31d850e56f2c26000031"
},
"tags": [
"Schnecken",
"Basilikum",
"Thymian",
"Garten"
]
}
Queries:
db.QuizQuestion.find({
"_id": ObjectId("547f31e650e56f2c26000063")
}).limit(1).limit();
db.CoreMediaAdminVideo.update({
"_id": ObjectId("547f31d850e56f2c26000031")
},
{
"$set": {
"created": newISODate("2014-12-03T21:30:02+01:00"),
"updated": newISODate("2014-12-03T21:30:02+01:00"),
"updatedContent": newISODate("2014-12-03T21:30:02+01:00")
}
});
db.ARDBuffetQuizQuestion.update({
"_id": ObjectId("547f31e650e56f2c26000063")
},
{
"$set": {
"created": newISODate("2014-12-03T21:30:02+01:00"),
"updated": newISODate("2014-12-03T21:30:02+01:00"),
"questionText": "Wer einen Gemüsegarten hat, sollte wissen, dass Schnecken…?",
"answerText": "test",
"answerVideo": {
"$ref": "CoreMediaAdminVideo",
"$id": ObjectId("547f31d850e56f2c26000031"),
"$db": "my-database"
}
}
});
db.ARDBuffetQuizQuestion.update({
"_id": ObjectId("547f31e650e56f2c26000063")
},
{
"$set": {
"tags": [
{
"value": "Schnecken",
"normalized": "schnecken"
},
{
"value": "Basilikum",
"normalized": "basilikum"
},
{
"value": "Thymian",
"normalized": "thymian"
},
{
"value": "Garten",
"normalized": "garten"
}
]
}
});
db.ARDBuffetQuizQuestion.find({
"_id": ObjectId("547f31e650e56f2c26000063")
}).limit(1).limit();
db.CoreMediaAdminVideo.find({
"_id": ObjectId("547f31d850e56f2c26000031")
}).limit(1).limit();
Gedmo\Timestampable will set (new) values for
$created
and$updated
because that data isn't present when flushing the ObjectManager.Although the annotations in class
Video
define that$created
and$updated
should be included when serializing such an object, the JSON you show doesn't contain those keys/properties.Here's what happens:
created
andupdated
.null
values for$created
and$updated
.merge()
-ing the object into the ObjectManager, nothing happens with it. The object simply becomes "managed", which means that during a flush, the ObjectManager will calculate the change-set for it and update it if necessary.PreUpdate
event listener). It will see that$created
and$updated
containnull
values, so it will assign new values.find()
-ing an object, it won't do this because the data is already retrieved during thatfind()
.$created
and$updated
are now different from those retrieved from the database, it will update them.So you'll have 2 options:
find()
the object first, then change it according to the JSON.created
andupdated
).Also, I noticed
setter="setCreatedEmpty"
andsetter="setUpdatedEmpty"
. I'm not sure what those methods do (because you don't show us), but the method-names indicate something other that simply assigning values.Answer to your comments
When
merge()
-ing an object into the ObjectManager, it is marked as "dirty", which will trigger the calculation of a change-set. And because the reference of the DateTime objects has changed (the instances that Doctrine got from the db are always different from the instances that were created by deserializing JSON), the object will be updated. Then Gedmo\Timestampable will kick in and change the$updated
property accordingly.If you don't want this to happen, you'll need to
find()
the current object, and only change value objects when the value they represent actually changed. Scalar values are no problem: you can set the same value and Doctrine won't see it as a change. But with value objects (like DateTime) objects, Doctrine will see a change when there reference changes (when a different instance is set).