What is the best way to avoid unique constraint collisions with doctrine? - php

The inner drawback with senquenced identifier for a SQL table is the possiblity for a end user to easily go all over your tables. Sometimes it is a problem.
One solution is to create a non-sequenced id, something non guessable for every row.
This id must be a unique field, obviously. I can use a random function to generate thoses uniques ids for every row but there is a probability that it collides with a previously set id. If it collides, the end user will perceive it as random bug...
Here is one simple solution to overcome this problem:
$keyValid = false;
while(!$keyValid) {
// Create a new random key
$newKey = generateKey();
// Check if the key already exists in database
$existingPotato = $em->getRepository('Potato')->findOneBy(array('key' => $newKey));
if (empty($existingPotato)) {
$keyValid = true;
}
}
// Hooray, key is unique!
It forces me to make at least one SELECT statement everytime I want a new id.
So, is there a better, widely-accepted solution to this problem?
Alternatively, is there an optimised length for the id that make this problem irrelevant by making the collision probability negligable (for a 3,000,000 rows table)?

You can add a Custom id generation strategy to do it. You can implement it by creating a class that extends AbstractIdGenerator:
use Doctrine\ORM\Id\AbstractIdGenerator;
class NonSequencedIdGenerator extends AbstractIdGenerator
{
public function generate(\Doctrine\ORM\EntityManager $em, $entity)
{
$class = $em->getClassMetadata(get_class($entity));
$entityName = $class->getName();
do {
// You can use uniqid(), http://php.net/manual/en/function.uniqid.php
$id = generateKey();
} while($em->find($entityName, $id));
return $id;
}
}
Then add it using annotations in your entity class:
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="NonSequencedIdGenerator")
*/
private $id;
But if your generateKey don't return an unique identifier you should check if it already exists anyway. To avoid this, you can use an UUID generator for the primary keys in your entity as well.
/**
* #ORM\Id
* #ORM\Column(type="guid", unique=true)
* #ORM\GeneratedValue(strategy="UUID")
*/
private $id;
If you don't like this, you can create a new custom id generation that use UUID_SHORT, and use function like this to make it shorter.
use Doctrine\ORM\Id\AbstractIdGenerator;
class UuidShortGenerator extends AbstractIdGenerator
{
public function generate(EntityManager $em, $entity)
{
$conn = $em->getConnection();
return $conn->query('SELECT UUID_SHORT()')->fetchColumn(0);
}
}
The problem here is that I don't think it's provides full portability.

Related

ODM MongoDB autoincrement doesn't work

I am, trying to increment the field, and get no results;
Here is my code:
class Query
{
/**
* #ODM\Id
*/
protected $id;
/**
* #ODM\Field(type="int", strategy="increment")
*
*/
protected $my_id = 0;
public function incrementMyId()
{
$this->my_id++;
}
}
When in action i try:
$query = new Query();
$query->incrementMyId();
$this->documentManager->persist($query);
$this->documentManager->flush();
The field my_id is always equals to int(1);
Can u help me with this issue? Thanks.
I use ZF3,
"alcaeus/mongo-php-adapter": "^1.
"doctrine/doctrine-mongo-odm-module": "^0.11.
"doctrine/mongodb-odm": "^1.1"
Strategy increment for a normal field means that the query updating document in the database will use $inc operator instead of $set, exactly as per documentation.
You seem to want to have both auto-generated ObjectId identifier and unique auto-incremented one for the entire collection. For such usage please see my other answer to a similar question

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.

How to set start value for Id with strategy Increment on Doctrine MongoDB document on Symfony?

i have the following document:
class Purchase
{
/**
* #MongoDB\Id(strategy="INCREMENT")
*/
protected $id;
...
I'm using this document in a Symfony 2.8.4 project.
In this case, the ID for the first document that i persist is '1', the next one will be '2' and so on.
I'd like to start the counter from 1000, but i can't figure how i can do it inside the "Model part".
Thanks
Unfortunately there is now way to set counter in "Model part", but as the current counters are stored in the database you may alter their values there. For more details how this work you can inspect how IncrementGenerator::generate works.
It could be a perfect use of the SequenceGenerator, but not supported by the platform.
One work-around (out of the model, unfortunately) consists in setting the first ID manually.
In your controller:
$object = new Purchase();
$em = $this->getDoctrine()->getManager():
$metadata = $em->getClassMetadata(get_class($object));
$metadata->setIdGeneratorType($metadata::GENERATOR_TYPE_NONE);
$object->setId(1000);
$em->persist($object);
$em->flush();
print $object->getId(); // 1000
Don't forget to add the setter function in your Purchase document:
/**
* #param int $id
*
* #return Purchase
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
Then, remove this logic (or wrap it in a check to verify that the object is the first being persisted).

Symfony Doctrine One to Many does not insert foreign key

I am having annoying problems with persisting an entity with one or more OneToMany-Childs.
I have a "Buchung" entity which can have multiple "Einsatztage" (could be translated to an event with many days)
In the "Buchung entity I have
/**
* #param \Doctrine\Common\Collections\Collection $property
* #ORM\OneToMany(targetEntity="Einsatztag", mappedBy="buchung", cascade={"all"})
*/
private $einsatztage;
$einsatztage is set to an ArrayCollection() in the __constructor().
Then there is the "Einsatztag" Entity which has a $Buchung_id variable to reference the "Buchung"
/**
* #ORM\ManyToOne(targetEntity="Buchung", inversedBy="einsatztage", cascade={"all"})
* #ORM\JoinColumn(name="buchung_id", referencedColumnName="id")
*/
private $Buchung_id;
Now If I try to persist an object to the database the foreign key of the "Einsatztag" Table is always left empty.
$buchung = new Buchung();
$buchung->setEvent( $r->request->get("event_basis"));
$buchung->setStartDate(new \DateTime($r->request->get("date_from")));
$buchung->setEndDate(new \DateTime($r->request->get("date_to")));
$von = $r->request->get("einsatz_von");
$bis = $r->request->get("einsatz_bis");
$i = 0;
foreach($von as $tag){
$einsatztag = new Einsatztag();
$einsatztag->setNum($i);
$einsatztag->setVon($von[$i]);
$einsatztag->setBis($bis[$i]);
$buchung->addEinsatztage($einsatztag);
$i++;
}
$em = $this->getDoctrine()->getManager();
$em->persist($buchung);
foreach($buchung->getEinsatztage() as $e){
$em->persist($e);
}
$em->flush();
Firstly, you have to understand that Doctrine and Symfony does not work with id's within your entities.In Einsatztag entity, your property should not be called $Buchung_id since it's an instance of buchung and not an id you will find out there.
Moreover, in your loop, you add the Einsatztag to Buchung. But do you process the reverse set ?
I do it this way to always reverse the set/add of entities.
Einsatztag
public function setBuchung(Buchung $pBuchung, $recurs = true){
$this->buchung = $pBuchung;
if($recurs){
$buchung->addEinsatztag($this, false);
}
}
Buchung
public function addEinsatztag(Einsatztag $pEinsatztag, $recurs = true){
$this->einsatztages[] = $pEinsatztag;
if($recurs){
$pEinsatztag->setBuchung($this, false);
}
}
Then, when you will call
$buchung->addEinsatztag($einsatztag);
Or
$einsatztag->set($buchung);
The relation will be set on both side making your FK to be set. Take care of this, you'll have some behavior like double entries if you do not use them properly.
SImplier , you can use default getter/setters and call them on both sides of your relation, using what you already have, like following:
$einsatztag->set($buchung);
$buchung->addEinsatztag($einsatztag);
Hope it helped ;)
First of all, don't use _id properties in your code. Let it be $buchung. If you want it in the database, do it in the annotation. And this also the reason, why it's not working. Your are mapping to buchung, but your property is $Buchung_id
<?php
/** #ORM\Entity **/
class Buchung
{
// ...
/**
* #ORM\OneToMany(targetEntity="Einsatztag", mappedBy="buchung")
**/
private $einsatztage;
// ...
}
/** #ORM\Entity **/
class Einsatztag
{
// ...
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="einsatztage")
* #JoinColumn(name="buchung_id", referencedColumnName="id")
**/
private $buchung;
// ...
}
You don't have to write the #JoinColumn, because <propertyname>_id would the default column name.
I'm going to ignore the naming issue and add a fix to the actual problem.
You need to have in the adder method a call to set the owner.
//Buchung entity
public function addEinsatztage($einsatztag)
{
$this->einsatztags->add($einsatztag);
$ein->setBuchung($this);
}
And to have this adder called when the form is submitted you need to add to the form collection field the by_reference property set to false.
Here is the documentation:
Similarly, if you're using the CollectionType field where your underlying collection data is an object (like with Doctrine's ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.
http://symfony.com/doc/current/reference/forms/types/collection.html#by-reference

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