I am trying to save an Entity that may or may not already exist and may or may not have one or two relations (code below). My current method results in errors, I am pretty sure I could come up with some workarounds/hacks on my own, but I am interested in the proper, "official" way to do it.
What I am trying now:
$entity = new myEntity();
if ( !empty($id) )
{
$entity->setId($id);
}
$entity->setLocationId($relation_id); //may or may not be null, if not null it's always an already existing location entry in a different table, i.e. not a new
$entity = $entity_manager->merge($entity);
$entity_manager->flush();
Currently Doctrine complains about Location being a new Entity with no ID and the policy does not allow generating IDs automatically. Policy is indeed such, but I do not add Location entity at all, I use setLocationId() auto generated method which adds precisely existing Location's ID so I am a bit puzzled.
edit: I get the doctrine error when location id is not null but a real, existing (in db) location's id.
And the model:
Location:
type: entity
table: locationstable
id:
my-id-column:
type: string
fields:
some fields
oneToMany:
myEntities:
targetEntity: myEntity
mappedBy: location
cascade: ["persist", "merge"]
If you are going to use Doctrine 2's ORM then you really need to use it as designed. That means working with objects and not id's. If you are not comfortable with this approach then switch to some sort of active record implementation.
//For your case, start by seeing if there is an existing entity:
// This eliminates all the merge nonsense
$entity = $entityManager->getRepository('EntityClassName')->find($entityId);
if (!$entity)
{
$entity = new Entity();
/* Only do this is you are not autogenerating id's
* However, it does raise the question of how you know what the id should be if the entity
* if the entity does not yet exist?
*/
$entity->setId($id);
// Persist the new entity
$entityManager->persist($entity);
}
// Now get a reference to the location object which saves loading the entire location
// which optimizes things a tiny (probably unnoticeable) bit
$location = $entityManager->getReference('LocationClassName',$locationId);
$entity->setLocation($location);
// And flush changes
$entityManager->flush();
And again, if you feel this is too complex or uses too many queries then don't use Doctrine 2. You will be fighting it constantly. In practice, Doctrine 2 turns out to perform quite well. No real need to worry about micro-optimizations.
Try to find the location record and add to the entity:
$entity = new myEntity();
if ( !empty($id) )
{
$entity->setId($id);
}
//$entity->setLocationId($relation_id); //may or may not be null, if not null it's always an already existing
$location = $locationRepository->find($relation_id);
$entity->setLocation($location);
location entry in a different table, i.e. not a new
$entity = $entity_manager->merge($entity);
$entity_manager->flush();
Related
imagine I have some doctrine Entity and I can have some records of this entity in the database which I dont want to be deleted, but I want them to be visible.
In general I can have entities, for which I have default records, which must stay there - must not be deleted, but must be visible.
Or for example, I want to have special User account only for CRON operations. I want this account to be visible in list of users, but it must not be deleted - obviously.
I was searching and best what I get was SoftDeletable https://github.com/Atlantic18/DoctrineExtensions/blob/v2.4.x/doc/softdeleteable.md It prevents fyzical/real deletion from DB, but also makes it unvisible on the Front of the app. It is good approach - make a column in the Entity's respective table column - 1/0 flag - which will mark what can not be deleted. I would also like it this way because it can be used as a Trait in multiple Entities. I think this would be good candidate for another extension in the above Atlantic18/DoctrineExtensions extension. If you think this is good idea (Doctrine filter) what is the best steps to do it?
The question is, is this the only way? Do you have a better solution? What is common way to solve this?
EDIT:
1. So, we know, that we need additional column in a database - it is easy to make a trait for it to make it reusable
But
2. To not have any additional code in each repository, how to accomplish the logic of "if column is tru, prevent delete" with help of Annotation? Like it is in SoftDeletable example above.
Thank you in advance.
You could do this down at the database level. Just create a table called for example protected_users with foreign key to users and set the key to ON DELETE RESTRICT. Create a record in this table for every user you don't want to delete. That way any attempt to delete the record will fail both in Doctrine as well as on db level (on any manual intervention in db). No edit to users entity itself is needed and it's protected even without Doctrine. Of course, you can make an entity for that protected_users table.
You can also create a method on User entity like isProtected() which will just check if related ProtectedUser entity exists.
You should have a look at the doctrine events with Symfony:
Step1: I create a ProtectedInterface interface with one method:
public function isDeletable(): boolean
Step2: I create a ProtectionTrait trait which create a new property. This isDeletable property is annotated with #ORM/Column. The trait implements the isDeletable(). It only is a getter.
If my entity could have some undeletable data, I update the class. My class will now implement my DeleteProtectedInterface and use my ProtectionTrait.
Step3: I create an exception which will be thrown each time someone try to delete an undeletable entity.
Step4: Here is the tips: I create a listener like the softdeletable. In this listener, I add a condition test when my entity implements the ProtectedInterface, I call the getter isDeleteable():
final class ProtectedDeletableSubscriber implements EventSubscriber
{
public function onFlush(OnFlushEventArgs $onFlushEventArgs): void
{
$entityManager = $onFlushEventArgs->getEntityManager();
$unitOfWork = $entityManager->getUnitOfWork();
foreach ($unitOfWork->getScheduledEntityDeletions() as $entity) {
if ($entity instanceof ProtectedInterface && !$entity->isDeletable()) {
throw new EntityNotDeletableException();
}
}
}
}
I think that this code could be optimized, because it is called each time I delete an entity. On my application, users don't delete a lot of data. If you use the SoftDeletable component, you should replace it by a mix between this one and the original one to avoid a lot of test. As example, you could do this:
final class ProtectedSoftDeletableSubscriber implements EventSubscriber
{
public function onFlush(OnFlushEventArgs $onFlushEventArgs): void
{
$entityManager = $onFlushEventArgs->getEntityManager();
$unitOfWork = $entityManager->getUnitOfWork();
foreach ($unitOfWork->getScheduledEntityDeletions() as $entity) {
if ($entity instanceof ProtectedInterface && !$entity->isDeletable()) {
throw new EntityNotDeletableException();
}
if (!$entity instance SoftDeletableInterface) {
return
}
//paste the code of the softdeletable subscriber
}
}
}
Well the best way to achieve this is to have one more column in the database for example boolean canBeDeleted and set it to true if the record must not be deleted. Then in the delete method in your repository you can check if the record that is passed to be deleted can be deleted and throw exception or handle the situation by other way. You can add this field to a trait and add it to any entity with just one line.
Soft delete is when you want to mark a record as deleted but you want it to stay in the database.
I'm trying to persist a TradeEntity. A TradeEntity has a OneToOne relationship with a CurrencyEntity.
/**
* #ORM\OneToOne(targetEntity="Repositories\Currency\CurrencyEntity")
* #ORM\JoinColumn(name="currency", referencedColumnName="id")
*
* #var CurrencyEntity
*/
protected $currency;
I have recieved a CurrencyEntity from another object which I'm trying to insert in this new TradeEntity and persist it to the database but get an exception:
Type: Doctrine\ORM\ORMInvalidArgumentException
Message: Expected value of type "Repositories\Currency\CurrencyEntity"
for association field "Repositories\Trade\TradeEntity#$currency", got "integer" instead.
Is there no other way of me persisting TradeEntity without fetching the CurrencyEntity from the database and setting it that way?
In light of my recent discovery, i felt the need to update this answer.
Reading about Doctrine's advanced configuration, i came across Reference Proxies.
The method EntityManager#getReference($entityName, $identifier) lets you obtain a reference to an entity for which the identifier is known, without loading that entity from the database.
This is useful, for example, as a performance enhancement, when you want to establish an association to an entity for which you have the identifier. You could simply do this:
<?php
// $em instanceof EntityManager, $cart instanceof MyProject\Model\Cart
// $itemId comes from somewhere, probably a request parameter
$item = $em->getReference('MyProject\Model\Item', $itemId);
$cart->addItem($item);
Old answer:
What you can do is (it defeats the purpose of an ORM, but it is possible):
$conn = $entityManager->getConnection();
$conn->insert(
'table_name',
array(
'column_name' => 'column_value',
// define all the columns here
)
);
See Doctrine's Data Retrieval And Manipulation
I have two entities: Projects and Task. I can implements this object as Value Object but I wonder about the whether that is good approach? Task might change own title or status and VO should be immutable. How implements this object?
I wonder about the in Project entity I should add addTask method or I should add Tasks via TaskController? Whether TaskController is necessary when Project entity has addTask method ?
Read this documentation on Doctrine Associations / Relations:
http://symfony.com/doc/current/doctrine/associations.html
It should explain what you need to do.
Essentially, your Project Entity should have an addTask() method where you add the task. Your Project will have an ArrayCollection of Tasks. Then you can use you getTask() method (you create this) to get the Task (if you need it).
The documentation gives good examples, so take a at that first.
EDIT #2 Based on comments.
So, it's seems you don't understand the article very well. You would have separate methods in each of your Entities to do what you need that is related to that particular Entity. I'm not certain what methods you actually want.
So for example, you gave in the comments two type of methods: changeTask and changeNameTask.
In you code, you could do something like this:
$project = new Project();
$task1 = new Task();
$task1->setName("My Task Name");
... // Do other things with task1
$project->addTask($task1);
$em = $this->getDoctrine()->getManager();
$em->persist($project); // Save to db.
$em->persist($task1);
$em->flush();
// Now let's add a new Task (different name).
$task2 = new Task();
$task2->setName("Another Task");
...
$project->addTask($task2);
// Remove the old Task...
$em->remove($task1);
$em->persist($project); // Save to db.
$em->persist($task2);
$em->flush();
// You can also get the Task if you need it.
$task2 = $project->getTask(); // Presumes that this is an object not an array.
The above should make sense...
I'm working in a project that use Doctrine 2 in Symfony 2 and I use MEMCACHE to store doctrine's results.
I have a problem with objects that are retrieved from MEMCACHE.
I found this post similar, but this approach not resolves my problem: Doctrine detaching, caching, and merging
This is the scenario
/**
* This is in entity ContestRegistry
* #var contest
*
* #ORM\ManyToOne(targetEntity="Contest", inversedBy="usersRegistered")
* #ORM\JoinColumn(name="contest_id", referencedColumnName="id", onDelete="CASCADE"))
*
*/
protected $contest;
and in other entity
/**
* #var usersRegistered
*
* #ORM\OneToMany(targetEntity="ContestRegistry", mappedBy="contest")
*
*/
protected $usersRegistered;
Now imagine that Contest is in cache and I want to save a ContestRegistry entry.
So I retrieve the object contest in cache as follows:
$contest = $cacheDriver->fetch($key);
$contest = $this->getEntityManager()->merge($contest);
return $contest;
And as last operation I do:
$contestRegistry = new ContestRegistry();
$contestRegistry->setContest($contest);
$this->entityManager->persist($contestRegistry);
$this->entityManager->flush();
My problem is that doctrine saves the new entity correctly, but also it makes an update on the entity Contest and it updates the column updated. The real problem is that it makes an update query for every entry, I just want to add a reference to the entity.
How I can make it possible?
Any help would be appreciated.
Why
When an entity is merged back into the EntityManager, it will be marked as dirty. This means that when a flush is performed, the entity will be updated in the database. This seems reasonable to me, because when you make an entity managed, you actually want the EntityManager to manage it ;)
In your case you only need the entity for an association with another entity, so you don't really need it to be managed. I therefor suggest a different approach.
Use a reference
So don't merge $contest back into the EntityManager, but grab a reference to it:
$contest = $cacheDriver->fetch($key);
$contestRef = $em->getReference('Contest', $contest->getId());
$contestRegistry = new ContestRegistry();
$contestRegistry->setContest($contestRef);
$em->persist($contestRegistry);
$em->flush();
That reference will be a Proxy (unless it's already managed), and won't be loaded from the db at all (not even when flushing the EntityManager).
Result Cache
In stead of using you own caching mechanisms, you could use Doctrine's result cache. It caches the query results in order to prevent a trip to the database, but (if I'm not mistaken) still hydrates those results. This prevents a lot of issues that you can get with caching entities themselves.
What you want to achieve is called partial update.
You should use something like this instead
/**
* Partially updates an entity
*
* #param Object $entity The entity to update
* #param Request $request
*/
protected function partialUpdate($entity, $request)
{
$parameters = $request->request->all();
$accessor = PropertyAccess::createPropertyAccessor();
foreach ($parameters as $key => $parameter) {
$accessor->setValue($entity, $key, $parameter);
}
}
Merge requires the whole entity to be 100% fullfilled with data.
I haven't checked the behavior with children (many to one, one to one, and so on) relations yet.
Partial update is usually used on PATCH (or PUT) on a Rest API.
I'm on Doctrine 2.3. I have the following query:
$em->createQuery('
SELECT u, c, p
FROM Entities\User u
LEFT JOIN u.company c
LEFT JOIN u.privilege p
WHERE u.id = :id
')->setParameter('id', $identity)
I then take that, get the result (which is an array, I just take the first element), and run detach $em->detach($result);.
When I go to fetch from the cache (using Doctrine's APC cache driver), I do:
$cacheDriver = new \Doctrine\Common\Cache\ApcCache();
if($cacheDriver->contains($cacheId))
{
$entity = $cacheDriver->fetch($cacheId);
$em->merge($entity);
return $entity;
}
My hope was that this would re-enable the relationship loading on the entity as there are many other relationships tied to the User object other than what's shown in that query.
I'm trying to create a new entity like such:
$newEntity = new Entities\ClientType();
$newEntity['param'] = $data;
$newEntitiy['company'] = $this->user['company'];
$em->persist($newEntity);
$em->flush();
When I do this, I get an error:
A new entity was found through the relationship 'Entities\ClientType#company' that was not configured to cascade persist operations for entity:
Entities\Company#000000005d7b49b500000000dd7ad743.
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 'Entities\Company#__toString()' to get a clue.
This works just fine when I don't use the company entity under the user entity that I got from cache. Is there any way to make this work so I don't have to refetch the company entity from the database every time I want to use it in a relationship with a new entity?
Edit:
This is what I have in my User entity dealing with these two relationships:
/**
* #ManyToOne(targetEntity="Company" , inversedBy="user", cascade={"detach", "merge"})
*/
protected $company;
/**
* #ManyToOne(targetEntity="Privilege" , inversedBy="user", cascade={"detach", "merge"})
*/
protected $privilege;
I am still getting the same error.
Second edit:
Trying a $em->contains($this->user); and $em->contains($this->user['company']); both return false. Which sounds... wrong.
When merging a User, you want the associated Company and Privilege to be merged as well, correct?
This process is called cascading:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-associations.html#transitive-persistence-cascade-operations
In your User entity put cascade={"merge"} in the #ManyToOne annotation (or another type of association-definition you are using) for $company and $privilege.
And if you want the detach call to be cascaded too (which is recommended), put in cascade={"detach", "merge"}.
p.s.: Don't put such cascades on both sides of one association, you'll create an endless loop ;)
Edit:
This piece of code:
$entity = $cacheDriver->fetch($cacheId);
$em->merge($entity); // <-
return $entity;
should be:
$entity = $cacheDriver->fetch($cacheId);
$entity = $em->merge($entity); // <-
return $entity;
The thing with merge() is that it leaves the entity you pass as an argument untouched, and returns a new object that represents the managed version of the entity. So you want to use the return-value, not the argument you passed.