i'm playing a little bit with Symfony2 and Doctrine2.
I have an Entity that has a unique title for example:
class listItem
{
/**
* #orm:Id
* #orm:Column(type="integer")
* #orm:GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #orm:Column(type="string", length="255", unique="true")
* #assert:NotBlank()
*/
protected $title;
now i'm fetching a json and updating my database with those items:
$em = $this->get('doctrine.orm.entity_manager');
foreach($json->value->items as $item) {
$listItem = new ListItem();
$listItem->setTitle($item->title);
$em->persist($listItem);
}
$em->flush();
works fine the first time. but the second time i'm getting an sql error (of course): Integrity constraint violation: 1062 Duplicate entry
sometimes my json file gets updated and some of the items are new, some are not.
Is there a way to tell the entity manager to skip the duplicate files and just insert the new ones?
Whats the best way to do this?
Thanks for all help. Please leave a comment if something is unclear
Edit:
what works for me is doing something like this:
$uniqueness = $em->getRepository('ListItem')->checkUniqueness($item->title);
if(false == $uniqueness) {
continue;
}
$listItem = new ListItem();
$listItem->setTitle($item->title);
$em->persist($listItem);
$em->flush();
}
checkUniqueness is a method in my ListItem Repo that checks if the title is already in my db.
thats horrible. this are 2 database queries for each item. this ends up about 85 database queries for this action.
How about retrieving all the current titles into an array first and checking the inserting title against the current titles in that array
$existingTitles = $em->getRepository('ListItem')->getCurrentTitles();
foreach($json->value->items as $item) {
if (!in_array($item->title, $existingTitles)) {
$listItem = new ListItem();
$listItem->setTitle($item->title);
$em->persist($listItem);
}
}
$em->flush();
getCurrentTitles() would need to be added to ListItem Repo to simply return an array of titles.
This only requires one extra DB query but does cost you more in memory to hold the current titles in an array. There maybe problems with this method if your dataset for ListItem is very big.
If the number of items your want to insert each time isn't too large, you could modify the getCurrentTitles() function to query for all those items with the titles your trying to insert. This way the max amount of $existingTiles you will return will be the size of your insert data list. Then you could perform your checks as above.
// getCurrentTitles() - $newTitles is array of all new titles you want to insert
return $qb->select('title')
->from('Table', 't')
->in('t.title = ', $newTitles)
->getArrayResult();
If you are using an entity that may already exists in the manager you have to merge it.
Here is what I would do (did not test it yet) :
$repository = $this->get('doctrine.orm.entity_manager');
foreach($json->value->items as $item) {
$listItem = new ListItem();
$listItem->setTitle($item->title);
$em->merge($listItem); // return a managed entity
// no need to persist as long as the entity is now managed
}
$em->flush();
Related
I know that in Doctrine (as a general rule) it is better to flush() after persisting all the entities/objects to the database, but in the following case I think it could be useful to do the opposite.
Example:
Imagine that you are cycling through a list of sport results like this one:
playerA_unique_tag (string), playerB_unique_tag (string), result
In the database, playerA and playerB are FOREIGN KEYS (that point to a User entity). So, the database structure would be similar to this one:
Match record
id, playerA_fk, playerB_fk, result
User records
id, playerA_unique_tag, (etc... many other fields)
id, playerB_unique_tag, (etc... many other fields)
Example of a script
$sportResultsArray = array();
foreach($sportResultsArray as $sportResult){
$playerA_tag = $sportResult["$playerA_unique_tag"];
$db_playerA = db->getRepository("App:User")->findOneByTag($playerA);
if(!$db_playerA){
$db_playerA = new User();
$db_playerA ->setPlayer_unique_tag($playerA_tag);
$em->persist($db_playerA );
}
$match = new Match();
$match ->setplayerA($db_playerA );
/*Same thing would be done for playerB*/
$em->persist($match );
}
Problem:
Of course playerA will play MULTIPLE matches, and each time I have to somehow retrieve the corresponding User object and pass it to the new Match object.
But how can I do that if I haven't flushed playerA User object yet.
The only two alternatives I can think of are:
1- Flushing the User entity (and ONLY the User entity) after it is created
2- Create a temporary array of objects like this:
array('playerA_unique_tag' => playerA_Object, etc.)
Problem with option_1:
I have tried $em->flush($db_playerA); but every entity that was persisted to the Entity Manager also gets flushed (contrary to what written here: http://www.doctrine-project.org/api/orm/2.5/source-class-Doctrine.ORM.EntityManager.html#338-359). Basically, the result is the same as $em->flush();
Problem with option_2:
Isn't it a bad and inefficient workaround?
Consider to work with in-memory registry of players as following:
// init registry
$players = [];
foreach ($sportResultsArray as $sportResult) {
$players[$sportResult["$playerA_unique_tag"]] = null;
$players[$sportResult["$playerB_unique_tag"]] = null;
}
// fetch all at once
$existing = $db->getRepository("App:User")->findBy(['tag' => array_keys($players)]);
// fill up the registry
foreach ($existing as $player) {
$players[$player->getTag()] = $player;
}
// match them up
foreach ($sportResultsArray as $sportResult) {
$playerA_tag = $sportResult["$playerA_unique_tag"];
if ($players[$playerA_tag] === null) {
$players[$playerA_tag] = new User();
$players[$playerA_tag]->setPlayer_unique_tag($playerA_tag);
$em->persist($players[$playerA_tag]);
}
$match = new Match();
$match->setplayerA($players[$playerA_tag]);
/*Same thing would be done for playerB*/
$em->persist($match);
}
// finally
$em->flush();
I've got two tables: step and links joined 1:n. I'm aiming to maintain the links through the step objects. I retrieve all steps from the database and populate the relation with the links table. I persist the step object containing a collection of links to JSON and return it to the front end using REST.
That means that if a step is linked or unlinked to another step in the front end I send the entire step back to the backend including a collection of links. In the back end I use the following code:
public function put($processStep) {
if (isset($processStep['Processesid']) && isset($processStep['Coordx']) && isset($processStep['Coordy'])) {
$p = $this->query->findPK($processStep['Id']);
$p->setId($processStep['Id']);
$p->setProcessesid($processStep['Processesid']);
if (isset($processStep['Flowid'])) $p->setFlowid($processStep['Flowid']);
if (isset($processStep['Applicationid'])) $p->setApplicationid($processStep['Applicationid']);
$p->setCoordx($processStep['Coordx']);
$p->setCoordy($processStep['Coordy']);
$links = $p->getLinksRelatedByFromstep();
$links->clear();
foreach ($processStep['Links'] as $link) {
if (!isset($link['Linkid'])) {
$newLink = new \Link();
$newLink->setFromstep($link['Fromstep']);
$newLink->setTostep($link['Tostep']);
$links->prepend($newLink);
}
}
$p->save();
return $p;
} else {
throw new Exceptions\ProcessStepException("Missing mandatory fields.", 1);
}
}
I'm basically deleting every link from a step and based upon the request object I recreate the links. This saves me the effort to compare what links are deleted and added. The insert work like a charm Propel automatically creates the new links. Thing is it doesn't delete like it inserts. I've checked the object that is being persisted ($p) and I see the link being deleted but in the MySQL log there is absolutely no action being performed by Propel. It looks like a missing member from the link collection doesn't trigger a dirty flag or something like that.
Maybe I'm going about this the wrong way, I hope someone can offer some advice.
Thanks
To delete records, you absolutely always have to use delete. The diff method on the collection is extremely helpful when determining which entities need added, updated, and deleted.
Thanks to Ben I got on the right track, an explicit call for a delete is not needed. I came across a function called: setRelatedBy(ObjectCollection o) I use this function to provide a list of related objects, new objects are interpreted as inserts and omissions are interpreted as deletes.
I didn't find any relevant documentation regarding the problem so here's my code:
$p = $this->query->findPK($processStep['Id']);
$p->setId($processStep['Id']);
$p->setProcessesid($processStep['Processesid']);
$p->setCoordx($processStep['Coordx']);
$p->setCoordy($processStep['Coordy']);
if (isset($processStep['Flowid'])) $p->setFlowid($processStep['Flowid']);
if (isset($processStep['Applicationid'])) $p->setApplicationid($processStep['Applicationid']);
//Get related records, same as populaterelation
$currentLinks = $p->getLinksRelatedByFromstep();
$links = new \Propel\Runtime\Collection\ObjectCollection();
//Check for still existing links add to new collection if so.
//This is because creating a new Link instance and setting columns marks the object as dirty creating an exception due to duplicate keys
foreach ($currentLinks as $currentLink) {
foreach ($processStep['Links'] as $link) {
if (isset($link['Linkid']) && $currentLink->getLinkid() == $link['Linkid']) {
$links->prepend($currentLink);
break;
}
}
}
//Add new link objects
foreach ($processStep['Links'] as $link) {
if (!isset($link['Linkid'])) {
$newLink = new \Link();
$newLink->setFromstep($link['Fromstep']);
$newLink->setTostep($link['Tostep']);
$links->prepend($newLink);
}
}
//Replace the collection and save the processstep.
$p->setLinksRelatedByFromstep($links);
$p->save();
Some quick context:
I have a sql table and a eloquent model for JobCards and each JobCard has several Operations belonging to it. I have a table and model for Operations. The users of my application browse and edit JobCards, but when I say editing a Jobcard this can include editing Operations associated with a JobCard. I have a page where a user can edit the Operations for a certain JobCard, I submit the the data as an array of Operations.
I want a clean way to update the data for the Operations of a JobCard. There are 3 different actions I may or may not need to do:
Update an existing Operation with new data
Create a new Operation
Delete an Operatation
I tried dealing with the first 2 and things are getting messy already. I still need a way of deleting an Operation if it is not present in the array sent in the request.
Heres my code:
public function SaveOps(Request $a)
{
$JobCardNum = $a -> get('JobCardNum');
$Ops = $a -> get('Ops');
foreach ($Ops as $Op) {
$ExistingOp = JobCardOp::GetOp($JobCardNum, $Op['OpNum'])->first();
if(count($ExistingOp)==0) {
$NewOp = new JobCardOp;
$NewOp -> JobCardNum = $JobCardNum;
$NewOp -> fill($Op);
$NewOp -> save();
$this->UpdateNextOpStatus($JobCardNum, $NewOp);
}
else {
$ExistingOp -> fill($Op);
$ExistingOp -> save();
}
}
Can anyone help with the deletion part and/or help make my code tidier.
This is how your method should look like. Please note that, I added a new method getCache($JobCardNum) this method will get an array of operations per job card (assuming that your model is designed to be related this way) this method will go to the DB only once, to get all the Operations that you need for this method call instead of getting them one-by-one (in the foreach loop), this way you make sure that the expensive call to the DB is done only once, on the other hand you got your JobCard's operations in the form of an array ready to compare with the new ones (coming in the request), the return of this method will be in the form of (key=>value with the key being the operation number and the value being the operation object it self).
/**
* This function will get you an array of current operations in the given job card
* #param $JobCardNum
* #return array
*/
public function getCache($JobCardNum)
{
/**
* asuming that the relation in your model is built that way. if not you should then
* use JobCardOp::all(); (Not recommended because it will get a lot of unnecessary
* data )
*/
$ExistingOps = JobCardOp::where('job_card_id', '=', $JobCardNum);
$opCache = array();
foreach ($ExistingOps as $Op) {
$opCache[(string)$Op->OpNum] = $Op;
}
return $opCache;
}
public function SaveOps(Request $a)
{
$strOpNum = (string)$Op['OpNum'];
$JobCardNum = $a->get('JobCardNum');
$Ops = $a->get('Ops');
$opCache = $this->getCache($JobCardNum);
foreach ($Ops as $Op) {
if (!isset($opCache[$strOpNum])) {
$NewOp = new JobCardOp;
$NewOp->JobCardNum = $JobCardNum;
$NewOp->fill($Op);
$NewOp->save();
$this->UpdateNextOpStatus($JobCardNum, $NewOp);
} else {
$ExistingOp = $opCache[$strOpNum];
$ExistingOp->fill($Op);
$ExistingOp->save();
}
unset($opCache[$strOpNum]);
}
/*
* at this point any item in the $opCache array must be deleted because it was not
* matched in the previous for loop that looped through the requested operations :)
*/
foreach ($opCache as $op) {
$op->delete();
}
}
I would like to insert 10 000 rows to database with batch processing.
In first step I need select some objects from databse, then interate these objects and for each of them persist another object to database.
Here is code example:
$em = $this->getDoctrine()->getManager();
$products = $em->getRepository('MyBundle:Product')->findAll(); // return 10 000 products
$category = $em->getRepository('MyBundle:Category')->find(1);
$batchsize = 100;
foreach ($products as $i => $product) {
$entity = new TestEntity();
$entity->setCategory($category);
$entity->setProduct($product); // MyEntity And Product is OneToOne Mapping with foreign key in MyEntity
$em->persist($entity);
if ($i % $batchsize === 0) {
$em->flush();
$em->clear();
}
}
$em->flush();
$em->clear();
It returns this error:
A new entity was found through the relationship 'Handel\GeneratorBundle\Entity\GenAdgroup#product' that was not configured to cascade persist operations for entity
I think problem is in clear(), that remove all objects in memory including $products and $category.
If I use cascade={"persist"} in association, doctrine insert new category row in db.
After some attempts I made some dirty entity errors.
Am I doing sometihng wrong? What is solution and best practice for this job?
Thanks a lot for answer
solution is just to clear only those objects that are changing/creating. Those one that are constant should be left within EntityManager.
Like this
$em->clear(TestEntity::class);
$em->clear(...);
If you left clear without param it will detach all objects that are current under entity manager. Meaning that if you try to reuse them it will throw error as you get. For instance unique filed will be duplicated and trow that error.
After calling
$em->clear();
Category object becomes unpersisted.
You can try calling $em->merge($category) method on it. But probably the most guaranteed way is to fetch it again.
if ($i % $batchsize === 0) {
$em->flush();
$em->clear();
$category = $em->getRepository('MyBundle:Category')->find(1);
}
i wrote an Importer script, which read entries from an csv file,
and iterate the rows. To handle big files without performance loss,
i insert new data within doctrine batch(bulks).
My problem at the moment is, i have an "Category" entity, which should be expanded
only within new entries. So i have to check if entries are available given category names.
My first question is, i've read that doctrines "prePersist" event will be called on call
"$entityManager->persist()" and inside the "flush" method (http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#prepersist).
So how can i check if the event was inside an flush?
The next thing, how can i update the actually entity within the identity datas?
I try to set the id, but without any effect.
<?php
/**
* #return \Doctrine\Commong\Collections\ArrayCollection
*/
public function getCategories()
{
if (null === $this->categories) {
$this->categories = $this->getServiceCategory()->findAll();
}
return $this->categories;
}
public function prePersist(LifecycleEventArgs $event)
{
$entity = $event->getEntity();
$objectManager = $event->getObjectManager();
if ($entity instanceof \Application\Entity\Category) {
$categories = $this->getCategories();
$entityCriteria = // buildCriteria from Entity;
$matched = $categories->matching($entityCriteria);
if ($matched->count() > 0) {
$entity->setId($matched->first()->getId();
}
}
}
So, here i dont know how to update the persisted categorie entity?
Is this the right event, or should be an other event a better solution for my situation?
I developed the import within zf2 and doctrine2.
regards
First I would recommend to use DQL instead of ORM entities within you import script, because it makes you code much more simple.
Your import process increases the performance, if you first (1.) read all existing "Categories" from yor database, keep them within a member variable and second (2.) iterate each csv row and compare its category foreign key with the initially read set of categories.
If the category already exists in you database, create a new entity row with the existing corresponding foreign key, else create a new category and create a new entity row associated to the new category.
<?php
// read categories from database
$categories = "..."; /* e.g. array('A' => 1,
'B' => 2, ...); */
// ...
// Iterate each csv row
foreach($csvRows as $csvRow) {
// check category name
if(!array_key_exists($csvRow['category']), $categories) {
// Create new Category
// Remember id of the new created category
$categoryId = "...";
} else {
// Use already existing category id
$categoryId = $categories[$csvRow['category']];
}
// Create new csv row entity with category foreign key
// ...
}
?>