I have a FileUpload entity that is child of other entities using the following association:
/**
* #ORM\ManyToOne(targetEntity="FileUpload", cascade={"persist", "remove"} )
* #ORM\JoinColumn(name="image_id", referencedColumnName="id")
*/
protected $image;
The FileUpload entity contains various information about an uploaded file as well as a boolean field to mark it for deletion (handled by a checkbox on the form). I am trying to find a good way of managing this deletion process without having to duplicate the code in every entity that has a FileUpload entity.
I tried creating a service tagged with doctrine.event_listener to remove the FileUpload in postUpdate(), however since there is still an association with the parent of the FileUpload this failed. Does anyone know a way of clearing any associations with the FileUpload when it is deleted? Or any other method of handling this process?
Sorry for the late answer. Now I understand your problem.
In the relation you described on the side has to be the owner of the relation. If let's say A is in relation with B and let's say that A is the owner of the relation, that implies that A has control over all the aspects of the relation so B can't be deleted without A say so.
Think about the foreign key relation of a database. The database won't let you delete the line as long as it is part of a relationship and is not the owner of the relation(this being your current problem).
If you get into a place where you need to delete a FileUpload without knowing the Comment that has a relation to it you may have an architectural problem in your application/database design. If you know the Comment that has a relation to the FileUpload at the point that you want to remove the file then orphanRemoval is what you need. The way you remove it is not by asking the Manager to remove it (cause it can't do it without the approval of the owner of the relation, as explained in the example above). Instead, you ask the owner of the relation to removing it something like this
//for OneToOne relation
$comment->setFileUpload(null);
//for OneToMany relation
$comment->getFileUpload()->removeElement($fileUpload);
After the above statement call flush and it should work. Also for OneToMany make sure that you initialize
$this->fileUpload = new ArrayCollection();
in the Comment entity constructor.
NOTE1: As mentioned before careful with orphan removal cause it doesn't work as you will expect in relation to refresh function of the manager. After an object was market in doctrine unit of work as an orphan, it will get removed even if you called refresh on it, or it's parent. Found a way around this (using the doctrine onFlush event) but is better to not need this and try to avoid the situation.
NOTE2: orphanRemoval has the effect of hard delete in the database. If as some point you need to do add this code to a doctrine subscriber or onFlush listener
public function onFlush(OnFlushEventArgs $args)
{
foreach ($args->getEntityManager()->getUnitOfWork()->getScheduledEntityDeletions() as $entity) {
if ($entity instanceof SoftRemovableInterface) {
$args->getEntityManager()->remove($entity);
$args->getEntityManager()->persist($entity);
$entity->remove();
}
$args->getEntityManager()->getUnitOfWork()->computeChangeSet($args->getEntityManager()->getClassMetadata(get_class($entity)), $entity);
}
}
Where the remove+persist calls are there to take the entity out of the orphan removal list in unit of work (part of the fix for refreshing entities in NOTE1, this is the only place and only way i found that you can stop the removal of an orphan after it was marked as such by doctrine), and $entity->remove(); is the method of the SoftRemovableInterface that handles the soft delete, something like
class Comment implements SoftRemovableInterface
{
/........../
function remove()
{
$this->deleted = true;
}
}
Hope this brings some light to your issue. Happy coding.
Alexandru Cosoi
Related
I have a database, where I store some fixed values like product categories. When I create a new product and I want to assign a category to it, I do it this way:
$categories = new ProductCategoryRepository();
$category = $categories->find(ProductCategory::EXAMPLE);
$product = new Product();
$product->setCategory($category);
However, I'm not sure why I have to lookup the database all the time to get static entities my app is already aware of.
It should be enough to assign the category statically. Maybe something like this:
$category = ProductCategory::EXAMPLE;
Now Doctrine should persist the relation with the correct ID (described by the ProductCategory class (which could be an entity?)) and I no longer have to lookup the database for static properties.
I don't know how to do this, yet. I could create new entities all the time, but this doesn't seem to be correct, because the values are already stored in the DB and they are always the same and not new entities.
$category = new ProductCategory::EXAMPLE;
Fetching the relation from the product however should return the property as an entity:
$category = $product->getCategory();
return $category instanceof ProductCategory; // true
Is there a way to achieve this behaviour?
It is more an architecture question than a performance tweak. I don't want to describe information multiple times (db entries, php constants, entity relations etc.).
There is something called "second level cache" in Doctrine, but the feature is considered experimental and you should maybe read the documentation carefully before using it.
A quote from the official documentation of this feature:
The Second Level Cache
The second level cache functionality is marked as experimental for now. It is a very complex feature and we cannot guarantee yet that it works stable in all cases.
Entity cache definition is done like this: (documentation)
/**
* #Entity
* #Cache(usage="READ_ONLY", region="my_entity_region")
*/
To improve performance for such entities like you are talking about in your question you should also consider to mark them as "read only", which will lead to performance increase from Doctrine 2.1, as can be found in the Doctrine documentation on improving performance:
Read-Only Entities
Starting with Doctrine 2.1 you can mark entities as read only (See metadata mapping references for details). This means that the entity marked as read only is never considered for updates, which means when you call flush on the EntityManager these entities are skipped even if properties changed. Read-Only allows to persist new entities of a kind and remove existing ones, they are just not considered for updates.
The entity should be configured like this: (documentation)
/** #Entity(readOnly=true) */
Second level cache and read only for your ProductCategory:
So after setting up second level read only caching with for example a region named read_only_entity_region your configuration for your ProductCategory would look something like this:
/**
* #Entity(readOnly=true)
* #Cache(usage="READ_ONLY", region="read_only_entity_region")
*/
class ProductCategory
{
//...your entity definition...
}
If you don't want it to hit the database every time you could just store it in the Cache:
public function getCategory(){
return Cache::rememberForever('category-'.$this->category_id, function() {
return $categories->find($this->category_id);
});
}
This will pull the info from the database if it has never been pulled, but will just grab it from the cache if it has been. You would have to use Cache::forget('category-2') to remove it, or php artisan cache:clear. Your static values would just be integer IDs and your products would have a category_id but the categories themselves would be cached.
I'm migrating the DBAL of a ZF3 application to Doctrine and want to go ahead step by step. Currently I'm using a hierarchy of Mapper objects. Each entity in the like FooEntity hierarchy has an according FooMapper. Saving of nested entities is performed by nested Mappers. Every Mappers saves its entity with Zend\Db\Sql\Insert or Zend\Db\Sql\Update and calls the proper Mappers for the sub-entities like BarMapper for BarEntity.
Now, before I start with Doctrine's convenience features like cascade={"persist"}, I want to keep the Mapper's hierarchy and just to perform the saving of the top level of the nested entity with persist(...) & flush().
But when I try it
public function save(AbstractDataObject $dataObject)
{
$newLogicalConnection = $this->logicalConnectionMapper->save($dataObject->getLogicalConnection());
$newUser = $this->userMapper->save($dataObject->getUser());
$dataObject->setLogicalConnection($this->entityManager->find(LogicalConnection::class, $newLogicalConnection->getId()));
$dataObject->setUser($this->entityManager->find(User::class, $newUser->getId()));
$this->entityManager->persist($dataObject);
$this->entityManager->flush();
return $dataObject;
}
I get an error
A new entity was found through the relationship 'MyNamespace\DataObject\AbstractEndpoint#externalServer' that was not configured to cascade persist operations for entity: MyNamespace\DataObject\ExternalServer#000000006098ccff0000000068c23676. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'MyNamespace\DataObject\ExternalServer#__toString()' to get a clue.
So, Doctrine seems to try saving the whole entity with its sub-entities, and this attempt fails on one of the lower levels. But why? I have not activated any cascade options and expect Doctrine to save only the top level.
Why does Doctrine try to save the whole entity and not only the top level? And how to get it saving only the top level of the given entity?
You get this error because you have a new entity (not persisted yet) in AbstractEndpoint->externalServer and this field is not annotated as cascade={"persist"}
In other words you have just created a new entity ExternalServer and did not persisted it and added it as a relation to AbstractEndpoint->externalServer entity which is not annotated as cascade={"persist"}
So Doctrine ends up having this new entity and does not know what to do with it. In order not to lost any data this exception is raised.
To fix this you can do two things:
Add $this->entityManager->persist($externalServer); right after you create ExternalServer entity
Annotate AbstractEndpoint->externalServer with cascade={"persist"}. Which you don't want to do because you want only top level entity to be saved to DB so you need to persist it manually or DO NOT add it is a relation.
And now answering your question:
But why? I have not activated any cascade options and expect Doctrine to save only the top level.
Somehow through relations in your object model Doctrine goes down to ExternalServer entity and finds it in unpersisted state. You can not save only top level of object hierarchy with link to unexisting record in relational database. If you do not want Doctrine to do it for you - you must handle this situations by yourself or remove not persisted entities from relations
I have two entities linked together by a ManyToMany relationship in a Doctrine/MySQL project.
A Client entity:
class Client
{
[...]
/**
* #ORM\ManyToMany(targetEntity="ClientTag")
* #ORM\JoinTable(name="clients_tags")
*/
protected $tags;
}
And a ClientTag entity:
class ClientTag
{
[...]
/**
* #ORM\Column(type="string", length=45)
*/
protected $label;
/**
* #ORM\Column(type="string", length=7)
*/
protected $color;
}
So I have the ability to associate multiple clients to one tag, and vice-versa, great.
But I can't find a way to automatically remove a tag when there is no more clients referencing it.
I tried to use orphanRemoval on the ManyToMany annotation but it doesn't do what I thought.. Orphan removal should imply exactly what I described above but it removes the tag when the reference to its parent is removed, not considering other entities like I need to.
If a client removes a tag but this tag is still used by 2 other clients, I don't consider it "orphan" as it still has one or more entities referencing it.
Of course I could solve the case by doing a query and removing it myself if I don't find any parent, but I wonder if Doctrine or MySQL have a built in way to do this (that will be far more optimized) ?
Any idea?
Thanks for your help.
Officially orphanRemoval isn't supported for ManyToMany relations in doctrine.
http://docs.doctrine-project.org/en/latest/reference/annotations-reference.html#annref-manytomany
The orphan removal in this case is ambiguous.
You can either just understand the relations (the jointable entries) to the deleted entity as the orphans or the related entity.
From a database point of view it would be the jointable entries.
From an ORM point of view it's the related entities.
Thing is both ways are correct depending on the use case. For example in an Article <-> Category relation you would want to remove the article from all associated categories on deletion, but you wouldn't want to throw away the whole category just because it's empty at this moment.
I'm guessing that's the reason why Doctrine doesn't officially mention the orphanRemoval option for ManyToMany because it's unclear and to fully support both variants the current implementation isn't enough.
Hope that was somehow understandable.
In your case though you'll probably need to clean up unused tags yourself.
I have these entities on my code.
class Review extends BaseEntity {
/** #ORM\OneToOne(targetEntity="Action", mappedBy="review") */
protected $action;
}
class Action extends BaseEntity {
/** #ORM\OneToOne(targetEntity="Review", inversedBy="action") */
protected $review;
}
As you can see it's a OneToOne relationship between Action and Review. My problem is I use soft-delete for my entities as well, so when I delete an entity is not actually removed only a deletion date is set. Later in audit trail I need to show deleted reviews and I also need information from the parent action of course. My question is, do I need to make this relationship OneToMany? or is there a better approach?
To be honest i'm not very found of the soft-delete behaviour. What you need to be aware is that soft-deleting an entity is a strong compromise in a relational database.
You may want to consider in this particular instance an event sourcing approach. I would recommend to store the information about the deletion and the (soft)deleted entity in a document based DB.
Any other approach (like OneToMany) is still fine but keep in mind that the risk here is degrading your relation by introducing a incoherent relationship.
That being said I'm fully aware that real life it's quite different than theory :) good luck.
Regards.
I have a lot of entities that have ManyToOne and OneToMany associations in Symfony2. As all know if you remove a record and it doesnt set a null value on the association in the other tables, things start to go haywire. So, what is the best way in Symfony2 to handle setting the value as null in other tables when a record is removed in Symfony2?
What do I need to set in my entities to ensure it persists across all associations.
First of all you can manually set ON DELETE behavior for the FK by using onDelete property for the joinColumn annotation in your entity:
/**
* #OneToOne(targetEntity="SomeEntity")
* #JoinColumn(name="some_entity_id", referencedColumnName="id", onDelete="cascade")
*/
private $someEntity;
Secondly, you always can implement event listener for the entity removing event. For example:
public function preRemove(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof SomeEntity) {
return;
}
// Here you can do whatever you want before
// entity record is removed from the DB.
}
You can read about the doctrine event system using following links:
Doctrine events (official manual)
How to Register Doctrine Event Listeners and Subscribers (Symfony2 cookbook)
The difference between them is that the first method works on the database layer. In other words, it would work even if you delete record using raw sql query in the console. Second one is more powerfull because you can also run any php code when entity is removed (for example you can send emails, etc.) but it will only be fired if record is removed via doctrine.