Doctrine won't save one-to-many relationship - php

I'm being bugged by an issue that seems very very puzzling. FYI - I know and I have read most of the doctrine questions around here, so I know the basics of doctrine and specifying relationships.
Below is how my data model looks (posting relevant sections of code)
class Sample
{
/**
* #ORM\OneToMany(targetEntity="Analysis", mappedBy="sample", cascade={"persist"})
*
protected $analyses
public function addAnalysis(Analysis $analysis)
{
$analyses->setSample($this);
$this->analyses[] = $analyses;
}
}
And Analysis
class Analysis
{
/**
* #ORM\ManyToOne(targetEntity="Sample", inverseBy="analyses", cascade={"persist"})
* #ORM\JoinColumn(name="sample_id", referencedColumnName="id")
*
protected $sample
public function setSample(Sample $sample)
{
$this->sample = $sample;
}
}
So one Sample can have multiple Analysis. While creating a new Analysis however, it is not letting me create one. It is throwing a NOT NULL constraint exception.
Below is the code I tried.
$analysis = new Analysis
$analysis->setUUID("seeebbbfg");
$analysis->setStatus(Analysis::STATUS_DONE);
$sample = $sample->addAnalysis($analysis)
$em->persist($sample);
$em->flush();
I have gone through many links and the doctrine documentation
Doctrine One-To-Many Relationship Won't Save - Integrity Constraint Violation
many-relationship-wont-save-integrity-constraint-violation
Symfony 2 doctrine persist doesn't work after updating Relationship Mapping
Doctrine entities relationship
After going through this Doctrine "A new entity was found through the relationship" error, I tried to persist $analysis before persisting sample, but it gave an 'a new entity was found' error and then this official doctrine documentation
http://doctrine-orm.readthedocs.io/projects/doctrine-orm/en/latest/reference/association-mapping.html
Not sure what I'm missing. Can anyone shed any light on this?
UPDATE 1
[Doctrine\DBAL\Exception\NotNullConstraintViolationException]
An exception occurred while executing
INSERT INTO analysis (id, uuid, type, status, submission_at, sample_id) VALUES (?,?,?,?,?,?) with params [202066, "seeebbbfg", "temp", 2, "2016-5-22 12:16:39", null]
null value in column "sample_id" violates not-null constraint

I think you should add analysisto the Analyses collection before set Sample.
I guess $this->analyses is an ArrayCollection so, use the ArrayCollection::add() method to add a new object.
Please, try this part of code and let me know the result
class Sample
{
/**
* #ORM\OneToMany(targetEntity="Analysis", mappedBy="sample", cascade={"persist"})
*/
protected $analyses
public function __construct()
{
$this->analyses = new ArrayCollection();
}
public function addAnalysis(Analysis $analysis)
{
$this->analyses->add($analysis);
$analysis->setSample($this);
return $this;
}
}

Credreak...
Thanks for this... Doctrine is not very clear how these are constructed.
I actually modified your code to create a instance of "related field" (analyese) as an Array Collection # _construct and then you can populate that Array with the actual Entity by passing it to the addAnalysis() function.
So the order of operations is: (as I understand it)
Construct an instance of Sample w/ an "related field" (analyese) Array
Construct an instance of Analysis and populate the Array
Pass the Analysis Array to the Sample analyese Array
Then flush the sample which will save the Analysis data to the database.
This worked for me.
class Sample
{
/**
* #ORM\OneToMany(targetEntity="Analysis", mappedBy="sample", cascade={"persist"})
*/
protected $analyses
public function __construct()
{
$this->analyses = new ArrayCollection();
}
public function addAnalysis(Analysis $analysis)
{
$this->analyses->add($analysis);
$analyses->setSample($this);
return $this;
}
}

Related

How should I check for dupplicate entity in an object collection?

I've been reading this post : Doctrine - how to check if a collection contains an entity
But I actually don't like the solution, as, doctrine already provide the contains() method, which have the advantage to keep logic directly into the object, and then to not load EXTRA_LAZY collections entirely.
So here a Cart Entity own a CartProduct collection as is :
/**
* ...
* #ORM\Entity(repositoryClass="App\Repository\CartRepository")
*/
abstract class Cart implements InheritanceInterface{
...
/**
* #ORM\OneToMany(targetEntity="CartProduct", mappedBy="cart", fetch="EXTRA_LAZY", cascade={"persist"})
*/
private Collection $cartProducts;
...
public function __construct()
{
$this->cartProducts = new ArrayCollection();
}
...
}
(CartProduct have to be an Entity look at this simplify EA model. That's a standard way to proceed for related entity holding extra fields)
Now I want to add a new ProductCart Entity to my Cart class.
So I'm adding this method (generated by Symfony make:entity) :
abstract class Cart implements InheritanceInterface{
...
public function addCartProduct(CartProduct $cartProduct): self
{
if(!$this->getCartProducts()->contains($cartProduct)) {
$this->cartProducts->add($cartProduct);
$cartProduct->setCart($this);
}
return $this;
}
...
And then I test this code :
public function testAddCartProduct()
{
$cart = new ShoppingCart($this->createMock(ShoppingCartState::class));
$cart_product = new CartProduct();
$cart_product->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product);
$cart_product2 = new CartProduct();
$cart_product2->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product2);
$this->assertCount(1, $cart->getCartProducts());
}
But when I run this test, it fail :
Failed asserting that actual size 2 matches expected size 1.
So I check, and the Cart.cartProducts Collection have two product which are exactly the same objects.
As it's an ArrayCollection, I suppose that it just use this method :
namespace Doctrine\Common\Collections;
class ArrayCollection implements Collection, Selectable {
...
public function contains($element)
{
return in_array($element, $this->elements, true);
}
So well, of course in this case it is just return false, And the objects are considered to be different.
So now, I wish I could use PersistentCollection instead of ArrayCollection when implementing the Collection object , because the PersistentCollection.contains() method looks better.
abstract class Cart implements InheritanceInterface{
...
public function __construct()
{
-- $this->cartProducts = new ArrayCollection();
++ $this->cartProducts = new PersistentCollection(...);
}
}
But this require an EntityManager as a parameter, so, seams a little bit overkill to give an EntityManager to an Entity object...
So I finally, I don't know what is the better way to check for a dupplicate entity inside a collection.
Of course, I could implement myself a thing like :
abstract class Cart implements InheritanceInterface{
...
public function addCartProduct(CartProduct $cartProduct): self
{
if(!$this->getCartProducts()->filter(
function (CartProduct $cp)use($cartProduct){
return $cp->getId() === $cartProduct->getId();
})->count()) {
$this->cartProducts->add($cartProduct);
$cartProduct->setCart($this);
}
return $this;
}
...
But it'll require to load every Entity and I really don't like the idea.
Personally I agree with your comment, I don't think the entity itself should have the responsibility to ensure there is no duplicate.
The entity cannot make a request like a repository could, and I don't see how you can be sure there is no duplicate in the database without querying it.
Calling contains will not trigger a fetch in your case, this means the collection will stay as is, which is not what you want anyway because you could have a previously persisted duplicate that will not be part of the collection because you marked it as EXTRA_LAZY.
You also don't want to fetch all the entities of the collection (and transform the results into objects) just to check if you have a collision.
So IMHO you should create a method in the repository of the entity to check for duplicates, a simple SELECT COUNT(id).
Then there is your real problem.
The way you make your test will never find a collision. When you do:
$cart = new ShoppingCart($this->createMock(ShoppingCartState::class));
$cart_product = new CartProduct();
$cart_product->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product);
$cart_product2 = new CartProduct();
$cart_product2->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product2);
$this->assertCount(1, $cart->getCartProducts());
You are creating two instances of CartProduct, that's why the call to contains doesn't find anything.
Because contains checks for the object reference, not the content, like you can see in its implementation:
public function contains($element)
{
return in_array($element, $this->elements, true);
}
So in your test case what you're really testing is:
in_array(new CartProduct(), [new CartProduct()], true);
which will always return false.

symfony/doctrine: cannot use object without refreshing

It's the first time I run into this problem. I want to create a doctrine object and pass it along without having to flush it.
Right after it's creation, I can display some value in the object, but I can't access nested object:
$em->persist($filter);
print_r($filter->getDescription() . "\n");
print_r(count($filter->getAssetClasses()));
die;
I get:
filter description -- 0
(I should have 19 assetClass)
If I flush $filter, i still have the same issue (why oh why !)
The solution is to refresh it:
$em->persist($filter);
$em->flush();
$em->refresh($filter);
print_r($filter->getDescription() . " -- ");
print_r(count($filter->getAssetClasses()));
die;
I get:
filter description -- 19
unfortunately, you can't refresh without flushing.
On my entities, I've got the following:
in class Filter:
public function __construct()
{
$this->filterAssetClasses = new ArrayCollection();
$this->assetClasses = new ArrayCollection();
}
/**
* #var Collection
*
* #ORM\OneToMany(targetEntity="FilterAssetClass", mappedBy="filterAssetClasses", cascade={"persist"})
*/
private $filterAssetClasses;
public function addFilterAssetClass(\App\CoreBundle\Entity\FilterAssetClass $filterAssetClass)
{
$this->filterAssetClasses[] = $filterAssetClass;
$filterAssetClass->setFilter($this);
return $this;
}
in class FilterAssetClass:
/**
* #var Filter
*
* #ORM\ManyToOne(targetEntity="App\CoreBundle\Entity\Filter", inversedBy="filterAssetClasses")
*/
private $filter;
/**
* #var Filter
*
* #ORM\ManyToOne(targetEntity="AssetClass")
*/
private $assetClass;
public function setFilter(\App\CoreBundle\Entity\Filter $filter)
{
$this->filter = $filter;
return $this;
}
Someone else did write the code for the entities, and i'm a bit lost. I'm not a Doctrine expert, so if someone could point me in the good direction, that would be awesome.
Julien
but I can't access nested object
Did you set those assetClasses in the first place?
When you work with objects in memory (before persist), you can add and set all nested objects, and use those while still in memory.
My guess is that you believe that you need to store objects to database in order for them to get their IDs assigned.
IMHO, that is a bad practice and often causes problems. You can use ramsey/uuid library instead, and set IDs in Entity constructor:
public function __construct() {
$this->id = Uuid::uuid4();
}
A database should be used only as a means for storing data. No business logic should be there.
I would recommend this video on Doctrine good practices, and about the above mentioned stuff.
Your problem is not related to doctrine nor the persist/flush/refresh sequence; the problem you describe is only a symptom of bad code. As others have suggested, you should not be relying on the database to get at your data model. You should be able to get what you are after entirely without using the database; the database only stores the data when you are done with it.
Your Filter class should include some code that manages this:
// Filter
public function __contsruct()
{
$this->filterAssetClasses = new ArrayCollection();
}
/**
* #ORM\OneToMany(targetEntity="FilterAssetClass", mappedBy="filterAssetClasses", cascade={"persist"})
*/
private $filterAssetClasses;
public function addFilterAssetClass(FilterAssetClass $class)
{
// assuming you don't want duplicates...
if ($this->filterAssetClasses->contains($class) {
return;
}
$this->filterAssetClasses[] = $class;
// you also need to set the owning side of this relationship
// for later persistence in the db
// Of course you'll need to create the referenced function in your
// FilterAssetClass entity
$class->addFilter($this);
}
You may have all of this already, but you didn't show enough of your code to know. Note that you should probably not have a function setFilterAssetClass() in your Filter entity.

Saving Related Entities Doctrine/Symfony2

I have one specific issue. I have two entities:
class MyPlaylist {
...
/**
* #var Array
* #ORM\OneToMany(targetEntity="MyPlaylistContent", mappedBy="myPlaylist", orphanRemoval=true)
* #ORM\OrderBy({"position" = "DESC"})
*/
private $myPlaylistItems;
and
class MyPlaylistContent {
....
/**
* #ORM\ManyToOne(targetEntity="MyPlaylist", inversedBy="myPlaylistItems")
*/
private $myPlaylist;
Now I have this in my service
....
$myPlaylist = new MyPlaylist();
$myPlaylist->setUser($user);
$myPlaylist->setActive(true);
// add tracks
foreach ($playlist->getMyPlaylistItems() as $item) {
$entity = new MyPlaylistContent();
$entity->setTrack($item->getTrack());
$entity->setMyPlaylist($myPlaylist);
$this->em->persist($entity);
}
$this->em->persist($myPlaylist);
$this->em->flush();
\Doctrine\Common\Util\Debug::dump($myPlaylist);
return $myPlaylist;
so, I return a new playlist. If I look at the database, all works fine. I have both entities and in MyPlaylistContent - 3 tracks. But
\Doctrine\Common\Util\Debug::dump($myPlaylist); shows next
["active"]=> bool(true) ["myPlaylistItems"]=> array(0) { }
On the page, the app shows the empty playlist (no tracks). If I refresh the page, I can see all tracks.
The point is, if you open the page, the controller will call the service, build the content and return the list as a response.
It looks as the same example, but it does not work for me
http://symfony.com/doc/current/book/doctrine.html#saving-related-entities
What is wrong here? Why don't I get tracks for the current entity?
You forget to add MyPlaylistContent to MyPlaylist.
Use this snippet into foreach
$myPlaylist->addMyPlaylistContent($myPlaylistContent);
Of course change name or implement method accordingly
First note: this is because objects are "normal" php objects, they have nothing to do with doctrine so, relationships are only a doctrine concept. EntityManager in doctrine will handle this kind of processes, not php itself. If you take a look to your classes methods you will probably notice that no "connection" (assignments) are made between those objects. If you would like, you can modify MyPlaylistContent to add itself to MyPlaylist once assigned.
Something like
class MyPlaylistContent
{
[...]
public function setMyPlaylist(MyPlaylist $mp)
{
$this->myPlaylist = $myPlaylist;
$mp->addMyPlaylistContent($this);
return $this;
}
Second note: hope your names are more consistents of these ones :)

Removing OneToMany elements, Doctrine2

I've got this model;
Itinerary, Venue, ItineraryVenue.
I needed many to many relation between itineraries and venues but also I wanted to store some specific data about the relation (say notes, own photo, etc.), so I decided to introduce a new entity named ItineraryVenue.
So Itinerary has collection of ItineraryVenues which in turn, refer to Venues.
My problem is that I can't remove ItineraryVenue from a Itinerary object.
$itinerary->itineraryVenues->removeElement($itineraryVenue);
$em->flush();
removes element from the php collection, but doesn't remove this $itineraryVenue from database.
I've managed to force Doctrine2 to remove $itineraryVenue, but only when I annotate the Itinerary::$itineraryVenues with orphanRemoval=true.
Since orphan removal treats Venue as a private property it also removes Venue entity, I don't want that.
Is there an relation configuration option or is removing "by hand" the olny way to make it work as I want?
Hard to believe it, it's a common relation pattern.
Entities definitions:
class Itinerary
{
/**
* #ORM\OneToMany(targetEntity="ItineraryVenue", mappedBy="itinerary", cascade={"persist", "remove"})
*/
private $itineraryVenues;
function __construct()
{
$this->itineraryVenues = new ArrayCollection();
}
}
class ItineraryVenue
{
/**
* #ORM\ManyToOne(targetEntity="Itinerary", inversedBy="itineraryVenues")
*/
private $itinerary;
/**
* #ORM\ManyToOne(targetEntity="Venue")
*/
private $venue;
function __construct()
{
}
}
class Venue
{
}
You are doing things right: orphanRemoval - is what you need. So, you should override default Itinerary::removeItineraryVenue like
public function removeItineraryVenue(\AppBundle\Entity\ItineraryVenue $itineraryVenue)
{
$itineraryVenue->setItinerary(null);
$this->itineraryVenues->removeElement($itineraryVenue);
}
The full working example is here https://github.com/kaduev13/removing-onetomany-elements-doctrine2.

How to avoid duplicate entries in a many-to-many relationship with Doctrine?

I'm using an embed Symfony form to add and remove Tag entities right from the article editor. Article is the owning side on the association:
class Article
{
/**
* #ManyToMany(targetEntity="Tags", inversedBy="articles", cascade={"persist"})
*/
private $tags;
public function addTag(Tag $tags)
{
if (!$this->tags->contains($tags)) // It is always true.
$this->tags[] = $tags;
}
}
The condition doesn't help here, as it is always true, and if it wasn't, no new tags would be persisted to the database at all. Here is the Tag entity:
class Tag
{
/**
* #Column(unique=true)
*/
private $name
/**
* #ManyToMany(targetEntity="Articles", mappedBy="tags")
*/
private $articles;
public function addArticle(Article $articles)
{
$this->articles[] = $articles;
}
}
I've set $name to unique, because I want to use the same tag every time I enter the same name in the form. But it doesn't work this way, and I get the exception:
Integrity constraint violation: 1062 Duplicate entry
What do I need to change to use article_tag, the default join table when submitting a tag name, that's already in the Tag table?
I have been battling with a similar issue for months and finally found a solution that seems to be working very well in my application. It's a complex application with quite a few many-to-many associations and I need to handle them with maximum efficiency.
The solution is explained in part here: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/faq.html#why-do-i-get-exceptions-about-unique-constraint-failures-during-em-flush
You were already halfway there with your code:
public function addTag(Tag $tags)
{
if (!$this->tags->contains($tags)) // It is always true.
$this->tags[] = $tags;
}
Basically what I have added to this is to set indexedBy="name" and fetch="EXTRA_LAZY" on the owning side of the relationship, which in your case is Article entity (you may need to scroll the code block horizontally to see the addition):
class Article
{
/**
* #ManyToMany(targetEntity="Tags", inversedBy="articles", cascade={"persist"}, indexedBy="name" fetch="EXTRA_LAZY")
*/
private $tags;
You can read up about the fetch="EXTRA_LAZY" option here.
You can read up about indexBy="name" option here.
Next, I modified my versions of your addTag() method as follows:
public function addTag(Tag $tags)
{
// Check for an existing entity in the DB based on the given
// entity's PRIMARY KEY property value
if ($this->tags->contains($tags)) {
return $this; // or just return;
}
// This prevents adding duplicates of new tags that aren't in the
// DB already.
$tagKey = $tag->getName() ?? $tag->getHash();
$this->tags[$tagKey] = $tags;
}
NOTE: The ?? null coalesce operator requires PHP7+.
By setting the fetch strategy for tags to EXTRA_LAZY the following statement causes Doctrine to perform a SQL query to check if a Tag with the same name exists in the DB (see the related EXTRA_LAZY link above for more):
$this->tags->contains($tags)
NOTE: This can only return true if the PRIMARY KEY field of the entity passed to it is set. Doctrine can only query for existing entities in the database/entity map based on the PRIMARY KEY of that entity, when using methods like ArrayCollection::contains(). If the name property of the Tag entity is only a UNIQUE KEY, that's probably why it's always returning false. You will need a PRIMARY KEY to use methods like contains() effectively.
The rest of the code in the addTag() method after the if block creates a key for the ArrayCollection of Tags either by the value in the PRIMARY KEY property (preferred if not null) or by the Tag entity's hash (search Google for "PHP + spl_object_hash", used by Doctrine to index entities). So, you are creating an indexed association, so that if you add the same entity twice before a flush, it will just be re-added at the same key, but not duplicated.
Two main solutions
First
Use a data transformer
class TagsTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* used to give a "form value"
*/
public function transform($tag)
{
if (null === $tag) {
//do proper actions
}
return $issue->getName();
}
/**
* used to give "a db value"
*/
public function reverseTransform($name)
{
if (!$name) {
//do proper actions
}
$issue = $this->om
->getRepository('YourBundleName:Tag')
->findOneBy(array('name' => $name))
;
if (null === $name) {
//create a new tag
}
return $tag;
}
}
Second
Use lifecycle callback. In particular you can use prePersist trigger onto your article entity? In that way you can check for pre-existing tags and let your entity manager manage them for you (so he don't need to try to persist causing errors).
You can learn more about prePersist here
HINT FOR SECOND SOLUTION
Make a custom repository method for search and fetch old tags (if any)

Categories