Why I can't delete entry in database using Doctrine 2 Entity manager?
I have next controller and entity with whom I have a problem.
I get in controller object form entity manager and i can't delete this object. Why?
// /Controller/Controller.php
/**
* Handler delete checkbox
* #Route("/administrator/services/delete/{id}", requirements={"id" = "\d+"}, defaults={"id" = 0}, name="service_delete")
* #Template()
*/
public function serviceDeleteAction(Request $request, $id){
$em = $this->getDoctrine()->getEntityManager();
$repoServices = $em->getRepository(CoworkingService::class);
$services = $repoServices->findOneBy(['id' => $id]);
$em->remove($services);
$em->persist($services);
$em->flush();
return [];//$this->redirectToRoute('administrator');
}
// /Entity/CoworkingService.php
class CoworkingService
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=50)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="SentviBundle\Entity\Language")
* #ORM\JoinColumn(name="language_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $language;
/**
* #ORM\Column(name="common_identifier", type="text")
*/
private $commonIdentifier;
Thanks!
#Matteo’s comment has already solved the issue, but let me explain what has happened.
You’re executing 3 entity manager operations:
$em->remove($services);
$em->persist($services);
$em->flush();
You must know that, before you call $em->flush(), all operations are registered in a service called the “unit of work” (UOW).
The UOW keeps track of all modifications in your entities (including adding/deleting entities), and only applies them to the database when you call flush().
When calling $em->remove($services), you told the UOW that you want to delete the entity. However, when calling $em->persist($services) directly afterwards, you told the UOW that you want to create (or, effectively: keep) the entity. (Note that, in Doctrine, “persist” doesn’t mean that a connection is made to the database, but instead, you pass an entity to the EM/UOW to calculate the modifications.)
So, in conclusion, the persist operation cancelled the remove out, and, at that point, flush had nothing to do.
For more details on the entity lifecycle and EM states see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html
Related
I simplified my 3 entities as much as possible below, it shows a simple relationship of Currency <- 1:1 -> Balance <- 1:N -> BalanceLog
Entity/Currency.php
/**
* #ORM\Entity(repositoryClass=CurrencyRepository::class)
*/
class Currency
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=3)
*/
private ?string $code;
/**
* #ORM\OneToOne(targetEntity="Balance", mappedBy="currency")
**/
private ?Balance $balance;
// ...
}
Entity/Balance.php
/**
* #ORM\Entity(repositoryClass=BalanceRepository::class)
*/
class Balance
{
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="Currency", inversedBy="balance")
* #ORM\JoinColumn(name="currency", referencedColumnName="code", nullable=false)
**/
private ?Currency $currency;
/**
* #ORM\OneToMany(targetEntity="App\Entity\BalanceLog", mappedBy="balance")
*/
private Collection $balance_logs;
// ...
}
Entity/BalanceLog.php
/**
* #ORM\Entity(repositoryClass=BalanceLogRepository::class)
*/
class BalanceLog
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Balance", inversedBy="balance_logs")
* #ORM\JoinColumn(name="balance_currency", referencedColumnName="currency")
**/
private ?Balance $balance;
// ...
}
The issue happens when I call:
$balanceLog = $this->getDoctrine()
->getRepository('App:BalanceLog')->findAll();
This hydrates the BalanceLog::$balance to the proper instance of Balance type, but it does not hydrate the BalanceLog::$balance->currency to Currency instance. Instead it wants to use string only
Resulting in error:
Typed property App\Entity\Balance::$currency must be an instance of App\Entity\Currency or null, string used
The dirty fix is to make Balance::$currency without fixed type of ?Currency. Then it will accept string and the code "works". But it is not correct. The Balance::$currency should be of Currency type, not sometimes string, sometimes currency.
I tried to make my own method in BalanceLogRepository, and for whatever reason this works just fine:
public function findByBalance(Balance $balance) : iterable
{
$query = $this->createQueryBuilder('bl');
$query->andWhere('bl.balance = :balance')
->setParameter('balance', $balance);
return $query->getQuery()->getResult();
}
So I am even more perplexed as to why the default findAll or findBy does not do recursive hydration
After further investigation I found a very weird behavior:
if I prepend this code:
$balance = $this->getDoctrine()->getRepository('App:Balance')->find('USD');
in front of
$balanceLog = $this->getDoctrine()->getRepository('App:BalanceLog')->findAll();
in my controller, then the error is gone. Its as if the App:Balance ORM schema of Balance with dependencies were not properly loaded until I try to fetch the Balance object directly apriori.
I did some debugging and it looks that BalanceLog does not create a full Balance Entity instance, but instead a Proxy. The solution was to add eager loading to the BalanceLog class
class BalanceLog
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Balance", inversedBy="balance_logs", fetch="EAGER")
* #ORM\JoinColumn(name="balance_currency", referencedColumnName="currency")
**/
private ?Balance $balance;
// ...
}
The UnitOfWork.php then does not use Proxy but instead loads the Entity as a whole.
If somebody wonders why querying Balance beforehand made the code work, its because of sophisticated caching mechanism of Doctrine. It saved Balance instance for primary key USD and then when BalanceLog was populated, it used this instance instead of creating a Proxy.
I still think that Proxy should not enforce strictly typed property from Entity though, but this is something for Doctrine developers to decide.
I've got the following classes (only show partials to reduce amount to read)
class Page {
/**
* #ORM\Column(type="string", unique=true, nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="UUID")
* #var string
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Url", mappedBy="content")
* #var Url[]
*/
protected $urls;
public function __construct()
{
$this->urls = new ArrayCollection();
}
}
And:
class Url
{
/**
* #ORM\Id #ORM\Column(type="string", unique=true, nullable=false)
* #ORM\GeneratedValue(strategy="UUID")
* #var string The unique identifier for the Url
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Page", inversedBy="urls", cascade={"persist", "merge"})
* #ORM\JoinColumn(name="content_id", referencedColumnName="id")
* #var int The UUID of the content
*/
protected $content;
public function __construct(Page $content, $link)
{
$this->content = $content;
$this->content->addUrl($this);
}
}
Each of these has a manager class with a save() function which just uses persist() and flush(). Saving them is then done as:
$pageManager->save($post);
$url = new Url($post, 'link goes here');
$urlManager->save($url);
I've also tried:
$url = new Url($post, 'link goes here');
$pageManager->save($post);
$urlManager->save($url);
Though in both instances I get:
( ! ) Fatal error: Uncaught exception 'Doctrine\ORM\ORMInvalidArgumentException' with message 'A managed+dirty entity Page#000000003d5a4ca10000000133ba3c3e can not be scheduled for insertion.'
I've tried this both with and without using AnnotationReader being used with EntityManager::create()
Doctrine's schema validator doesn't report any errors either:
php vendor/bin/doctrine orm:validate-schema
[Mapping] OK - The mapping files are correct.
[Database] OK - The database schema is in sync with the mapping files.
Any ideas how to get the persist() to work?
figured it out:
I had to reverse the order they were saved in, so the Url is saved first, and then Page (though in doing so it's opened up another issue I need to resolve where it wants to persist the author entity but can't (it thinks it's new, but it's not). Thought maybe it was detached, but even a merge() doesn't solve it.
/**
* #ORM\ManyToOne(targetEntity="User")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
* #var string The UUID of the author for the {#link Page}
*/
protected $author;
You must persist the $post and the $url objects before flush(). Otherwise you're going to have this error message.
Try to do this...
$entityManager->persist($post);
$url = new Url($post, 'link goes here');
$entityManager->persist($url);
$entityManager->flush();
Or you can create a flag param on your managers, to not flush if false...
$flush = false;
$pageManager->save($post, $flush);
$url = new Url($post, 'link goes here');
$urlManager->save($url);
It will probably work without any additional resources.
I have an entity Project that is related with some files via one-to-many relationship like this:
class projects{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="files", mappedBy="project", cascade= {"persist","merge","remove"},orphanRemoval=true)
*/
protected $project_files;
}
And File entity:
class files{
private $files;
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="projects", inversedBy="project_files")
*/
protected $project;
}
So when I edit a project, I want to be able to add some new files to it or delete some olds. I found a workaround to implement this but I can't figure out the way Symfony and Doctrine really handle this kind of operations. More precisely, my code (that works fine) that handles the deletion of some old files on edit is:
private function persistProject(Form $form) {
$project = $form->getData();
$raw_files = $project->getProjectFiles()->toArray();
$ready_files = self::setUpFiles($raw_files, $project);
$project->setProjectFiles($ready_files);
$em->merge($project);
$em->flush();
}
private function setUpFiles($project_files, $project) {
$ready_files = new ArrayCollection();
foreach ($project_files as $project_file){
if ((!empty($project_file->getId())) && ($project_file->getDescription() != '--del')){
$ready_files->add($project_file);
}
else if ((!empty($project_file->getId())) && ($project_file->getDescription() == '--del')){
unlink($this->uploadDir . $project_file->getPath());
// --- controversial line of code --- //
$project->removeProjectFile($project_file);
}
}
return $ready_files;
}
So in brief the situation is about this: if I remove line "$project->removeProjectFile($project_file);", the files are not deleted, even if my ArrayCollection $ready_files, that is ultimately stored to the object in line "$project->setProjectFiles($ready_files);", is exactly as it should be and does not contain the deleted files.
So my question is dual:
Shouldn't symfony persist the exact ArrayCollection (and only this) that I set and delete all the others files stored? Why does not the code work properly without the line "$project->removeProjectFile($project_file);"?
How does the line "$project->removeProjectFile($project_file);" affects the $project object in function persistProject() since the object is passed as a simple argument and not by reference? Isn't it obvious to expect that everything that happens to $project object in function setUpFiles() will do nothing to the original $project in function persistProject() as it is in different scope?
Can anyone help me to clear this up?
I have two entities - User and UserSettings. In User entity, I want to have UserSettings as an attribute. That would be OK, I would add a OneToOne relation but there's a problem - because UserSettings is an owning side of the relation, every time I load User entity, Doctrine has to load the UserSettings entity too.
Is there a way how to load User but not UserSettings?
I made maybe a weird solution - there's no relation between these entities and the settings are loaded by method of Facade. For example:
/**
* #ORM\Entity
*/
class User
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(type="string")
*/
private $name;
/** #var UserSettings */
private $settings;
public function __construct()
{
$this->settings = new UserSettings();
}
public function setSettings(UserSettings $settings)
{
$this->settings = $settings;
}
}
/**
* #ORM\Entity
*/
class UserSettings
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(name="user_id", type="integer")
*/
private $userId;
}
class UserFacade
{
/**
* #var EntityManager
*/
private $em; // is injected automatically by DI
public function loadSettings(User $user)
{
$settings = $this->em->getRepository("UserSettings")->findOneBy(array("userId" => $user->id));
$user->setSettings($settings);
}
}
$user = $em->find("User", 1);
// if I want user's settings
$userFacade->loadSettings($user); // now I can use $user->getSettings()->something;
Side note: UserFacade is a service class that manipulates with users' data like adding new user, editing, deleting etc. In my MVC application, controller classes communicate with Facades, not with EntityManager directly.
That's OK - settings are loaded only when I want to. However, there are two possible problems:
a) I don't think this is a clear way
b) When I want a list of users, I cannot JOIN a table where settings are, because entities are not associated, so I have to make an extra SQL for each user.
My question is - how to solve the problem with OneToOne relation? I don't have much experience with Doctrine, so it may be a stupid question - sorry for that.
Thanks!
I have a problem with lazy loading in symfony2/doctrine2.
I have a normal object (for example: type item) and this object has an id. If I look at the object at runtime I see that the id is set. Every other parameters like icon and amount are empty. I know, this is how lazy loading works but when I call the getters (getIcon) nothing happens. The icon attribute is still empty. I also tried to call the __load method but it doesn't help.
Sorry, forgot the code
class Character {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Entity\Item", mappedBy="character")
*/
protected $item;
/*********************************************************************
* Custom methods
*/
public function getItem() {
return $this->item;
}
}
And this is the object where the lazy loading not works.
class Item {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="integer")
*/
protected $amount;
/**
* #ORM\Column(type="string")
*/
protected $icon;
}
EDIT2:
Constructor of character class
public function __construct()
{
$this->item = new \Doctrine\Common\Collections\ArrayCollection();
}
So what the previos comments to your initial post are pointing at, is, that you need to implemend a ManyToOne relation in your Item entity to get all your stuff working.
In yout Character Entity you have this lines of code
/**
* #ORM\OneToMany(targetEntity="Entity\Item", mappedBy="character")
*/
protected $item;
This says you have a relation to an Entity Item which mappes the relation in the attribute "character". In this attribute the relation is stored. If you look into the database, you won't find any stored relations, because you class Item does not have the described mapping attribute character. Like gp_sflover pointed out, a OneToMany relations needs to be Bidirectional an required a ManyToOne relation in the "owning" side. So what you have to do is, add the following code to your Item Entity
/**
* #ORM\ManyToOne(targetEntity="Entity\Character", inversedBy="item")
*/
protected $character;
The inversedBy attribute creates a bidirectional relation. Without this statement, you wouldn't be able to load getItems from your Character entity.
If you have changed your code you have to update your database and to restore the elements. After this, everything will work fine.