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.
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 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
Multiple entities in my application have a gallery associated, defined this way
/**
* #ORM\ManyToOne(targetEntity="Gallery")
* #ORM\JoinColumn(name="gallery_id", referencedColumnName="id")
*/
protected $gallery;
I like to perform some custom logic when those entities are loaded, using postLoad doctrine event. I eventually learnt that Doctrine doesn't advice to play with associations on postLoad event, as they are not fully loaded...
In order to improve performance, I added a fetch="EAGER" on this association. Which is nice to reduce the number of requests while fetching a collection.
But now the getGallery() is empty in the postLoad event.
Is there any way to trigger something after the associations have been performed ? I could wrap a service and/or model around my entity to do the logic but I would prefer a simple event.
Thanks !
I am using DDD now for quiet some time, it took some time to get used to and separate everything. But am now stuck at updating my entity ...
Setup
Now I have a repo interface that defines the following methods
/**
* #param AccountEntity $account
* #return AccountEntity
*/
public function create(AccountEntity $account);
/**
* #param AccountEntity $account
* #return mixed
*/
public function update(AccountEntity $account);
My repo does exactly 0 in the function because it passes it to the mapper, which in turn creates/updates the data. So far so good.
The application service has the method create which accepts an array, the array is validated and if valid it will use an EntityBuilder to create the entity. (Entity requires the data by __construct). If the data is invalid it will throw an exception.
Now my issue is how to handle my update in the application service.
I get an array of data, just like the create, in the update and an id of the entity.
Should I use an hydrator to loop over the array and update the fields in the entity and then throw it to my repo.
cons, I then have an entity hydrator and builder.
pros, it is easy in use
Should I just validate the data and throw the array + id to the repo and let him figure out what to do
cons, just looks wrong
pros, again easy to make
Should I do toArray or something similar on my entity, merge that with the form data & then use the entity builder to build the entity again and throw the entity then to the repo
cons, don't think an entity should have a toArray function
pros, sounds like it encapsulates the task of building the entity in one place so that seems right.
?
So in short, how to convert an array with data to an entity, or how to save it. I am not using Doctrine in any way, and not planning to
First of all, your input data (array) should be validated somewhere at the controller level, to ensure is in the correct format (we're not talking about business rules, just formatting).
Then your entity can have something like this
class MyEntity
{
public function update($data)
{
//update properties, enforce the relevant business rules
//perhaps events are generated
}
}
The controller will probably use a service method to do the updating. The service will ask the repository for the entity, eventually creates the input format the entity expects it (if there is a difference) then calls the update method .
Then you send the entity to the repository which takes care of persisting it. Remember that the Repository is there to save/restore your entities not to change them.
This should probably have been a comment, but that requires 50 rep...
You should have a look at this article about datamappers: http://www.sitepoint.com/integrating-the-data-mappers/
I have been exactly in your situation, and the articles by that writer (Alejandro Gervasio) helped me immensely.