Iterate efficiently on large OneToMany – ManyToOne associations - symfony

I have two entities User and Period, they have a ManyToMany association: a user belongs to many periods, and a period has multiple users. This association uses a UserPeriod entity.
class Period
{
/**
* #ORM\OneToMany(targetEntity="UserPeriod", mappedBy="period")
*/
private $users;
}
class UserPeriod
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="User", inversedBy="periods")
*/
private $user;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Period", inversedBy="users")
*/
private $period;
}
class User extends BaseUser
{
/**
* #ORM\OneToMany(targetEntity="UserPeriod", mappedBy="user")
*/
protected $periods;
}
What I'm trying to achieve is getting a list of all users from a defined period. Since there is a lot of users, I can't load them all in memory and must iterate on them (batch processing). Here is what I tried:
public function getUsersOfQuery($period)
{
return $this->_em->createQueryBuilder()
->select('u')
->from('SGLotteryUserBundle:LotteryUser', 'u')
->innerJoin('u.periods', 'p')
->where('p.period = :id')
->setParameter('id', $period->id())
->getQuery();
}
$it = $repo->getUsersOfQuery($period)->iterate();
But, this exception is raised:
[Doctrine\ORM\Query\QueryException]
Iterate with fetch join in class UserPeriod using association user not allowed.
I cannot use native queries since User uses table inheritance.

Github issue
This happens when using either MANY_TO_MANY or ONE_TO_MANY join in
your query then you cannot iterate over it because it is potentially
possible that the same entity could be in multiple rows.
If you add a distinct to your query then all will work as it will
guarantee each record is unique.
$qb = $this->createQueryBuilder('o');
$qb->distinct()->join('o.manyRelationship');
$i = $qb->iterator;
echo 'Profit!';

Related

Mapping a property based on a join query

I'm using Symfony 5.4 with Entities for:
Org
UserConfig
User
Users are associated with Orgs through the UserConfig entity because Users can belong to more than 1 Org, with different configurations for each.
The UserConfig has the following relations:
/**
* #ORM\ManyToOne(targetEntity=Org::class)
* #ORM\JoinColumn(nullable=false)
*/
private $org;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="configs")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
Then the User entity relates with a OneToMany
/**
* #ORM\OneToMany(targetEntity=UserConfig::class, mappedBy="user", orphanRemoval=true)
*/
private $configs;
Most of my queries are in the context of a single Org, and I often need the related UserConfig when working with a User.
Is it possible to add a config property to the User entity which I can hydrate with a repository query which joins User and UserConfig on a specific Org?
For example, in my UserRepository I may fetch a group of users based on IDs. (This is a contrived example as I just want to show the config dependency)
public function findByIds(Org $org, array $ids): array
{
return $this->createQueryBuilder('user')
->innerJoin('user.configs', 'config', 'WITH', 'config.org = :org')
->setParameter('org', $org)
->andWhere('user.id IN (:ids)')->setParameter('ids', $ids)
->andWhere('config.active = :true')->setParameter('true', true)
->getQuery()
->getResult();
}
I'd like to call this method and access the related UserConfig as $user->getConfig() when iterating over the result of findByIds(). Is that possible?
It's not just for usability as I am serializing the user objects to JSON for API output and I'd like config to be nested in each user.

The identifier generation strategy for this entity requires the ID field to be populated before EntityManager#persist() is called

I'm tying to create one to many relations
A have class
class Interview {
/**
* #OneToMany(targetEntity="Question", mappedBy="question")
*/
private $questions;
public function __construct() {
$this->questions = new ArrayCollection();
}
public function __toString() {
return $this->id;
}
/**
* #return Collection|Question[]
*/
public function getQuestions() {
return $this->questions;
}
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
......
}
another
class Question {
/**
* #ManyToOne(targetEntity="Interview", inversedBy="interview")
* #JoinColumn(name="interview_id", referencedColumnName="id")
*/
private $interview;
public function getInterview() {
return $this->interview;
}
public function setInterview(Interview $interview) {
$this->interview = $interview;
return $this;
}
/**
* #ORM\Column(type="integer")
* #ORM\Id
*/
private $interview_id;
......
}
and Controller for all this
if ($form->isSubmitted() && $form->isValid()) {
$interview = new Interview();
$question = new Question();
$em->persist($interview);
$question->setInterview($interview);
$question->setTitle($request->get('title'));
$em->persist($question);
$em->flush();
return $this->redirectToRoute('homepage');
}
i'm receiving an error:
Entity of type AppBundle\Entity\Question is missing an assigned ID for
field 'interview_id'. The identifier generation strategy for this
entity requires the ID field to be populated before
EntityManager#persist() is called. If you want automatically generated
identifiers instead you need to adjust the metadata mapping
accordingly.
Don't understand what the problem and how to fix it.
To enforce loading objects from the database again instead of serving them from the identity map. You can call $em->clear(); after you did $em->persist($interview);, i.e.
$interview = new Interview();
$em->persist($interview);
$em->clear();
It seems like your project config have an error in doctrine mapped part.
If you want automatically generated identifiers instead you need to
adjust the metadata mapping accordingly.
Try to see full doctrine config and do some manipulation with
auto_mapping: false
to true as example or something else...
Also go this , maybe it will be useful.
I am sure, its too late to answer but maybe someone else will get this error :-D
You get this error when your linked entity (here, the Interview entity) is null.
Of course, you have already instantiate a new instance of Interview.But, as this entity contains only one field (id), before this entity is persited, its id is equal to NULL. As there is no other field, so doctrine think that this entity is NULL. You can solve it by calling flush() before linking this entity to another entity

Doctrine inserts twice when adding one related entity and using $em->clear()

When I try to add new info item it either inserts two of them or none. In the state of code below it inserts two. If i comment something it inserts none. When I comment the line with $em->clear() it inserts exactly one, as I need it. What I don't understand and what I'm doing wrong?
$limit = 10;
$offset = 0;
do {
$products = $selectedModelQuery->getQuery()
->setFirstResult($limit * $offset)
->setMaxResults($limit)->getResult();
$offset++;
$count = count($products);
/** #var \Nti\ApiBundle\Entity\Product $product */
foreach ($selectedModelQuery->getQuery()->getResult() as $product) {
if (!$product->getCollections()->contains($productCollection)) {
$product->addCollection($productCollection);
$productInfo = new ProductInfo;
$productInfo->setProduct($product);
$productInfo->setData($productCollection->getMessage());
$productInfo->setInfoType(ProductInfoInterface::TYPE_CUSTOM);
//$em->merge($product);
//$em->persist($productInfo);
}
}
$em->flush();
$em->clear();
} while ($count);
Main Entity:
class Product {
/**
* #ORM\OneToMany(targetEntity="ProductInfo", mappedBy="product", cascade={"persist", "remove"})
* #var ProductInfoInterface[]|ArrayCollection
*/
protected $infoList;
....
}
Related Entity:
class ProductInfo {
/**
* #ORM\ManyToOne(targetEntity="\Nti\ApiBundle\Entity\Product", inversedBy="infoList")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id", onDelete="CASCADE", nullable=false)
*/
protected $product;
...
}
Doctrine is based on Identity Map that keeps reference to all handled object (ie.: if you try to query two times for the same record by id, first time you hit the db but the second not and object is loaded from Identity Map).
When you use $em->clear() you are "discarding" all objects into Identity Map so, next iteration of do ... while will result in a brand new object (from ORM point of view) that will be flagged for writing operation.

Many to many relation can get associated object

I have a user and a school entity. This both entities have a many to many relation.
class User
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\School")
* #ORM\JoinTable(name="user_school",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="school_id", referencedColumnName="id")}
* )
*/
private $school;
public function __construct()
{
$this->school = new ArrayCollection();
}
}
class School
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
public function getId()
{
return $this->id;
}
}
If i call the method for retriving the school id in my controller like:
public function indexAction(Request $request)
{
$user = $this->getUser();
$school = $user->getSchool();
echo $school->getId();
}
I get the error message
Attempted to call an undefined method named "getId" of class "Doctrine\ORM\PersistentCollection"
Can someone give me hint , what i'm doing wrong?
Even though $user->getSchool(); is singular in method name, it returns a PersistentCollection - a collection of schools (since the relationship is ManyToMany). If you want to get a specific school's id, you would have to iterate through the schools, like so:
$scools = $user->getSchool()->toArray();
foreach ($scools as $scool) {
// do something with $school
}
In the mapping you defined, User::$school is a ManyToMany, which means the result of getSchool will be a Collection of Schools, not a single School entity.
Two scenarios:
A User can have multiple Schools. Then you probably should rename $school to $schools and getSchool to getSchools. And you can't call $user->getSchools()->getId() since $user->getSchools() is a Collection and does not have a getId method. Check #NoyGabay's answer for a way to access Schools ids.
A User can only have one School. Then you did not define your mapping correctly; you wanted a ManyToOne instead of ManyToMany. Check Doctrine's documentation for association mapping.

Symfony2 doctrine ManyToMany relation

I'm a beginner on Synfony2 and doctrine usage. I've created two entities as following :
For EndUser Entity (extension of FOSUserBundle ):
/**
* EndUser
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="Core\CustomerBundle\Entity\EndUserRepository")
*/
class EndUser extends BaseUser {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="Core\GeneralBundle\Entity\Discipline", mappedBy="endusers")
*/
private $discipline;
and for discipline entity
class Discipline {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="Core\CustomerBundle\Entity\EndUser", inversedBy="discipline")
*/
private $endusers;
When I did "php app/console doctrine:schema:update -- force,
EndUser, Discipline and discipline_enduser tables have been created.
When I run a standard SQL request through phpmyadmin as :
select t0.*
from
discipline t0,
discipline_enduser t2
where
t2.enduser_id = 1
and
t2.discipline_id = t0.id
I obtain the expected result as the list of discipline for a specific user.
My concern is how to implement the same using the Entity with Symfony2 & Doctrine
First I would recommend to use more natural language names for you relationships, like disciplines and endUsers since it is a manyToMany bond. Also, you need to create the get and set method for each. After you have prepared all your properties for an entity you should run the command to Generate your getters and setters
//this will generate all entities
php app/console doctrine:generate:entities BundleNamespace
After that, you can do things like :
$endUser->getDisciplines(); //return all disciplines of this user
$endUser->addDiscipline($someDiscipline); //add another discipline
$endUser->removeDiscipline($iAmABadDiscipline); //remove this discipline from this user
array $disciplines = [ ... ];
$endUser->setDisciplines($disciplines); // set multiple disciplines
Same logic applies to Discipline entity, ofcourse you should update them with an EntityManager for which you can read more in the answer here.
All you really need to do is amend your EndUser entity to include the following:
use Doctrine\Common\Collections\ArrayCollection;
class EndUser extends BaseUser
{
private $discipline;
public function __construct()
{
$this->discipline = new ArrayCollection();
}
public function setDiscipline($discipline)
{
$this->discipline = $discipline;
return $this;
}
public function getDiscipline()
{
return $this->discipline;
}
}
Then similarly amend Discipline, substituting endUser for discipline.
There is no need to create the intermediate entity. Doctrine has already figured it out. Neat, huh?!
To make things more coherent, rename your class member to $disciplines (it's many-to-many, so there may be multiple disciplines in it). Then you need setters and getters in your entity to access the disciplines.
One way is to do this on the command line:
php app/console doctrine:generate:entities YourBundle:EndUser
This will add the required methods to your class. The unmodified version of the class file is backed up in EndUser.php~ in case anything is overwritten.
After doing this, you can just call the getter to obtain the disciplines:
$disciplines = $anEndUser->getDisciplines();
For example in a controller method:
public function someAction()
{
$anEndUser = $this
->getDoctrine()
->getManager()
->getRepository('YourBundle:EndUser')
->find(1)
;
$disciplines = $anEndUser->getDisciplines();
}

Resources