Eager fetching with Sonata list view - doctrine-orm

I'm using SonataAdmin and SonataDoctrineORMAdmin bundles to manage entities.
The problem is I can't figure out how to eager fetch the related entities in the list view and as the number of listed entities increase the number of queries executed increasing rapidly as well.
I tried adding `fetch="EAGER" to the relation annotations but the profiles show that Sonata executes the separate queries anyway.
Here's one relation worth of code:
Post
<?php
namespace Acme\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Table()
* #ORM\Entity
*/
class Post
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=255)
**/
private $name;
/**
* #ORM\ManyToMany(targetEntity="Acme\AppBundle\Entity\Tag", fetch="EAGER")
* #ORM\JoinTable(name="join_post_to_tag",
* joinColumns={#ORM\JoinColumn(name="post_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="tag_id", referencedColumnName="id")}
* )
**/
private $tags;
public function getId()
{
return $this->id;
}
public function setName($names)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
public function setTags($tags)
{
$this->tags = $tags;
return $this;
}
public function __toString()
{
return $this->getName();
}
}
Tag
<?php
namespace Acme\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Table()
* #ORM\Entity
*/
class Tag
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="value", type="string", length=255)
*/
private $value;
public function getId()
{
return $this->id;
}
public function setValue($value)
{
$this->value = $value;
return $this;
}
public function getValue()
{
return $this->value;
}
public function __toString()
{
return ($this->getValue()) ? : '';
}
}
The first related query that is run is fetching all the posts:
SELECT DISTINCT p0_.id AS id0, p0_.id AS id1
FROM Post p0_
LEFT JOIN join_post_to_tag j1_ ON p0_.id = j1_.post_id
LEFT JOIN Tag p1_ ON p1_.id = j1_.target_id
ORDER BY p0_.id ASC
But this does not fetch the related tags or even if it does, it still queries it again:
SELECT t0.id AS id1, t0.value AS value2
FROM Tag t0
INNER JOIN join_post_to_tag ON t0.id = join_post_to_tag.tag_id
WHERE join_post_to_tag.post_id = ?
I tried to mess with the createQuery method in the admin class but could not really find a way to make the related entities fetched correctly.
Is there a way to force the list view to eager fetch the required related entities?

You are on the right track, using the createQuery($context) method.
I have achieved eager loading as following:
public function createQuery($context = 'list')
{
$query = parent::createQuery($context); // let sonata build it's default query for the entity
$rootEntityAlias = $query->getRootAlias(); // get the alias defined by sonata for the root entity
$query->join($rootEntityAlias.'.relationFieldName', 'relationFieldAlias'); // manualy define the join you need
$query->addSelect('relationFieldAlias'); // this is the key line. It is not enough to join a table. You have to also add it to the select list of the query, so that it's actualy fetched
// $query->join(...) // repeat the process of joining and selecting for each relation field you need
// $query->addSelect(...)
return $query; // return the altered query to sonata. this will only work for the "list" action.
}
If you're having trouble using this, let me know:)
Further reads on this topic:
SO question
docs

Related

Embedding Relations In Results

I'm using API-Platform to deliver content via API. I have a Potential relationship between users and participants (not all users will have participants but all participants will have at least one user). My main goal is to embed the relationship's User data in the result set of Participants as that result will be consumed by a data table and it would be more efficient to have that data already present within the result opposed to performing an additional request for the data.
e.g.:
{
"#context": "/api/contexts/Participants",
"#id": "/api/participants",
"#type": "hydra:Collection",
"hydra:member": [
{
"#id": "/api/participants/1",
"#type": "Participants",
"id": 1,
"name": "Jeffrey Jones",
"users": [
{
"#id": "/api/users/1",
"#type": "User",
"name": "Jenny Jones"
},
{
"#id": "/api/users/2",
"#type": "User",
"name": "Jessie Jones"
}
]
}
],
"hydra:totalItems": 1
}
However, I'm not sure if this is possible. I have looked at https://api-platform.com/docs/core/serialization#embedding-relations but I am not certain it would work for multiple result sets as the example is one book to one author. However, my scenario is one participant to multiple users.
Also (and I may need to go about this in a more direct manner), I'm using a joining table so that I can assign additional metadata to the relationship. So... participants > joint table (containing additional data) > users (and vice versa). Again, I may need to consider having a direct relationship between participants and users and then using a ParticipantUserMeta table to hold the additional metadata. However, at the moment, I'm leaning towards the join table containing the association as well as the additional metadata.
Here are the basics of my entities (most unnecessary data omitted):
User:
/**
* #ApiResource
* ...
*/
class User implements UserInterface, \Serializable
{
/**
* #var int
*
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var string
* #ORM\Column(type="string")
* #Assert\NotBlank()
*/
private $name = '';
/**
* #ORM\OneToMany(targetEntity="App\Entity\ParticipantRel", mappedBy="user")
*/
private $participants;
public function __construct()
{
$this->participants = new ArrayCollection();
}
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
/**
* #return Collection|ParticipantRel[]
*/
public function getParticipants(): Collection
{
return $this->participants;
}
}
ParticipantRel:
/**
* #ApiResource
* ...
*/
class ParticipantRel
{
/**
* #var int The Participant Id
*
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var int
*
* #ORM\Column(type="boolean")
*/
private $primary_contact;
/**
* #var string Relationship notes
*
* #ORM\Column(type="text", nullable=true)
*/
private $notes;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Participants", inversedBy="users")
* #ORM\JoinColumn(nullable=false)
*/
private $participant;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="participants")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
public function getId (): int
{
return $this->id;
}
public function getPrimaryContact(): ?bool
{
return $this->primary_contact;
}
public function getNotes(): ?string
{
return $this->notes;
}
public function getParticipant(): ?Participants
{
return $this->participant;
}
public function getUser(): ?User
{
return $this->user;
}
}
Participants
/**
* #ApiResource
* ...
*/
class Participants
{
/**
* #var int The Participant Id
*
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var string Participant's first name
*
* #ORM\Column(name="name")
* #Assert\NotBlank
*/
public $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\ParticipantRel", mappedBy="participant")
*/
private $users;
public function __construct() {
$this->users = new ArrayCollection();
}
public function getId (): int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
/**
* #return Collection|ParticipantRel[]
*/
public function getUsers(): Collection
{
return $this->users;
}
}
My question: Is what I'm attempting possible within an entity and if so, what am I missing? I have researched this a lot before coming here but haven't come up w/ any solution as most of the solutions I see involve a Twig tpl but I'm simply sending the data via api-platform. Any positive direction would be greatly appreciated.
So, it turns out that I just needed to experiment more w/ the Groups option (https://api-platform.com/docs/core/serialization#embedding-relations). Associating all related fields w/ the relative groups on all relative enities did end up returning the results in the desired format.

Doctrine 2 ZF3 self referencing entity is not updating all fields of parent

I have a self referencing entity with parent and children. The strange thing is, when I add the form elements (DoctrineModule ObjectSelect) for parent and children to the form, some other fields of the parent entity doesn't update, when I persist the entity. The child entities are updating fine.
In the update query of the parent aren't these fields which I want to update. It's like doctrine doesn't recognize the changes for the parent (owning side) anymore. Before the persist I get the right entity with the actual changes from the form, but than doctrine doesn't update the changed fields in the query.
When I delete the form elements for parent and children in the form, everything works fine and the parent entity update/persist all fields.
/**
* #var string
*
* #Gedmo\Translatable
* #ORM\Column(type="text")
*/
private $teaser;
/**
* #var string
*
* #Gedmo\Translatable
* #ORM\Column(type="text")
*/
private $description;
/**
* One Category has Many Categories.
* #var Collection
* #ORM\OneToMany(targetEntity="Rental\Entity\Rental", mappedBy="parent")
*/
private $children;
/**
* Many Categories have One Category.
* #ORM\ManyToOne(targetEntity="Rental\Entity\Rental", inversedBy="children")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="houses_id", nullable=true, onDelete="SET NULL")
*/
private $parent;
public function __construct()
{
$this->children = new ArrayCollection();
}
/**
* Get teaser
*
* #return string
*/
public function getTeaser()
{
return $this->teaser;
}
/**
* Set teaser
*
* #param string $teaser
*/
public function setTeaser($teaser)
{
$this->teaser = $teaser;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set description
*
* #param string $description
*/
public function setDescription($description)
{
$this->description = $description;
}
/**
* Add $child
* #param Collection $children
*/
public function addChildren(Collection $children)
{
foreach ($children as $child) {
$this->addChild($child);
}
}
/**
* #param Rental $child
* #return void
*/
public function addChild(Rental $child)
{
if ($this->children->contains($child)) {
return;
}
$child->setParent($this);
$this->children[] = $child;
}
/**
* Remove children
* #param Rental $children
*/
public function removeChildren(Collection $children)
{
foreach ($children as $child) {
$this->removeChild($child);
}
}
/**
* Remove child.
*
* #param \Rental\Entity\Rental $child
*
* #return boolean TRUE if this collection contained the specified element, FALSE otherwise.
*/
public function removeChild(Rental $child)
{
return $this->children->removeElement($child);
}
/**
* Get children.
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getChildren()
{
return $this->children;
}
/**
* Set parent.
*
* #param \Rental\Entity\Rental|null $parent
*/
public function setParent(Rental $parent = null)
{
$this->parent = $parent;
}
/**
* Get parent.
*
* #return \Rental\Entity\Rental|null
*/
public function getParent()
{
return $this->parent;
}
Answer based (and might still change) based on discussion in comments with OP under question.
Simple use case: self-referencing Doctrine Entity does not correctly update parent/child related Entity object properties on on-save action.
OP provided Entity.
I'm assuming you have a Form setup for this Entity. I'm also assuming it's correctly setup, as you did not provide it after being asked (because, rightly so, it would mean a lot of code). However, assuming stuff with code makes for a lot of mistakes, which is why I mention it here.
As such, I'm assuming the handling of your Form in the Controller might be faulty. To check, please use the following simplified addAction function and give it a shot (Factory code below).
/**
* #var RentalForm
*/
protected $form;
/**
* #var ObjectManager
*/
protected $objectManager;
public function __construct(ObjectManager $objectManager, RentalForm $form)
{
$this->form = $form;
$this->objectManager = $objectManager;
}
public function addAction()
{
/** #var RentalForm $form */
$form = $this->getForm();
/** #var Request $request */
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
$entity = $form->getObject();
$this->getObjectManager()->persist($entity);
try {
$this->getObjectManager()->flush();
} catch (\Exception $e) {
$message = sprintf(
'Was unable to save the data. Saving threw an error. <br />Code: %s. <br />Message: %s',
$e->getCode(),
$e->getMessage()
);
$this->flashMessenger()->addErrorMessage($message);
return [
'form' => $form,
'validationMessages' => $form->getMessages() ?: '',
];
}
$this->flashMessenger()->addSuccessMessage(
$this->getTranslator()->translate('Successfully created object.')
);
// TODO replace vars with your own: return $this->redirect()->route($route, $routeParams);
}
$this->flashMessenger()->addWarningMessage(
'Your form contains errors. Please correct them and try again.'
);
}
return [
'form' => $form,
'validationMessages' => $form->getMessages() ?: '',
];
}
Factory for class with the above, modify as needed for you situation
class RentalControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var ObjectManager $objectManager */
$objectManager = $container->get(EntityManager::class);
/** #var FormElementManagerV3Polyfill $formElementManager */
$formElementManager = $container->get('FormElementManager');
/** #var RentalForm $form */
$form = $formElementManager->get(RentalForm::class);
return new RentalController($objectManager, $form);
}
}

extend UserManager to search User by relation (symfony3)

I installed FOSUserbundle and HWI Oauth bundle.
My problem is: I want to access data from my user entity that is stored in a relation. I'd like to access the data from the fields social_network_slug and social_identifier from UserInSocialNetworks within the FOSUserProvider.
The idea was, that one user can have more that one social network logins. (1:n)- When I log in with my google/facebook etc login, I want to check the table user_in_social_networks if the Id with the social network already exists.
/*
* This is the User class, depending on fos_userBundle
*/
namespace AppBundle\Entity\Registration;
use Doctrine\Common\Collections\ArrayCollection as ArrayCollection;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* One User can have many social networks
* #ORM\OneToMany(targetEntity="UserInSocialNetworks", mappedBy="user", cascade={"remove"})
*/
private $socialnetworks; ....
the Entity Class to store all User's social media logins:
<?php
namespace AppBundle\Entity\Registration;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* UserInSocialNetworks
*
* #ORM\Table(name="user_in_social_networks")
* #ORM\Entity(repositoryClass="AppBundle\Repository\Registration\UserInSocialNetworksRepository")
*/
class UserInSocialNetworks
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* Many Socialnetwork Logins have one User
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Registration\User", inversedBy="socialnetworks")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*
*/
private $user;
/**
* #var int
*
* #ORM\Column(name="social_network_slug", type="string", length=255, nullable=true)
*/
private $socialNetworkSlug;
/**
* #var string
*
* #ORM\Column(name="social_identifier", type="string", length=255, nullable=true)
*/
private $socialIdentifier;
The extended FOSUBUserProvider class:
<?php
namespace AppBundle\Entity\Registration;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\FOSUBUserProvider as BaseClass;
use Symfony\Component\Security\Core\User\UserInterface;
class FOSUBUserProvider extends BaseClass
{
/**
* {#inheritDoc}
*/
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
// get user_id and socialnetworkname from response
$userIdInSocialNetwork = $response->getUsername();
$socialnetwork = $response->getResourceOwner()->getName();
// Here I'd like to search for an existing $userIdInSocialNetwork
What I checked since now: I can't access the entitymanager in FOSUBUserProvider class, and I can't search that way:
$user = $this->userManager->findUserBy(array(
'socialIdentifier' => $userIdInSocialNetwork,
'social_network_slug' => $socialnetwork)
because it's a relation.
Thanks for any idea!
As you mentioned that you have extended FOSUBUserProvider i assume you have defined a new service for this, If so then you can pass doctrine's entity manager to your class #doctrine.orm.entity_manager. Following HWIOAuthBundle documentation for FOSUserBundle you can pass entity manager as
services:
my.custom.user_provider:
class: MyBundle\Security\Core\User\MyFOSUBUserProvider
arguments: ['#fos_user.user_manager', { facebook: facebook_id }, #doctrine.orm.entity_manager]
And then in your class you can use this service as
use Doctrine\ORM\EntityManager;
use FOS\UserBundle\Model\UserManagerInterface;
//.... other use statements
class FOSUBUserProvider extends BaseClass
{
private $em;
public function __construct(UserManagerInterface $userManager, array $properties, EntityManager $em)
{
$this->em = $em;
parent::__construct($userManager, $properties); /* pass dependencies to parent */
}
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
$this->em->getRepository('AppBundle\Entity\Registration\UserInSocialNetworks')->findBy(....);
/* Do your stuff here */
}
}
Get error on my implementation but resolve this by adding quote to the third argument like bellow:
services:
my.custom.user_provider:
class: MyBundle\Security\Core\User\MyFOSUBUserProvider
arguments: ['#fos_user.user_manager', { facebook: facebook_id }, '#doctrine.orm.entity_manager']

zf2 doctrine 2 - Output OnetoOne Unidirectional

After annotating an OneToOne Unidirectional, i want to output the joined column, without using a form.
One People Entity has got a column to store the id of Country Entity.
What i can do: I can store the id of the country into the People Entity using a form with a dropdown select, which is bound to the Country Entity.
Problem: I can not enter the value country of the, hopefully correct, joined table.
People Entity:
<?php
namespace People\Entity;
use Doctrine\ORM\Mapping as ORM;
// ...
/**
* A people entity.
*
* #ORM\Entity
* #ORM\Table(name="icd_people")
* #property int $id
// ...
* #property string $ic_hq_country
*/
class People implements InputFilterAwareInterface
{
protected $inputFilter;
/**
* #ORM\Id
* #ORM\Column(type="integer");
*/
protected $id;
/**
* #ORM\Column(type="integer")
* #ORM\OneToOne(targetEntity="Country")
* #ORM\JoinColumn(name="ic_hq_country", referencedColumnName="id")
*/
protected $ic_hq_country;
// getter and setter
}
The Country Entity:
<?php
namespace People\Entity;
use Doctrine\ORM\Mapping as ORM;
//...
/**
* A Country entity.
*
* #ORM\Entity
* #ORM\Table(name="pre_country")
* #property int $id
* #property string $country
*/
class Country implements InputFilterAwareInterface
{
protected $inputFilter;
/**
* #ORM\Id
* #ORM\Column(type="integer");
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $country;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set country
*
* #param string $country
* #return Country
*/
public function setCountry($country)
{
$this->country = $country;
return $this;
}
/**
* Get country
*
* #return string
*/
public function getCountry()
{
return $this->country;
}
/**
* Convert the object to an array.
*
* #return array
*/
public function getArrayCopy()
{
return get_object_vars($this);
}
public function setInputFilter(InputFilterInterface $inputFilter)
{
throw new \Exception("Not used");
}
public function getInputFilter()
{
throw new \Exception("Not used");
}
}
The Controller Action:
public function indexAction()
{
$userid = $this->zfcUserAuthentication()->getIdentity()->getId();
return new ViewModel(array(
'pea' => $this->getEntityManager()->find('People\Entity\People', $userid),
));
}
The View giving the id of the country, but not the name:
<?php echo $this->escapeHtml($pea->ic_hq_country);?>
I actually expected something like this being possible, to output the country name and not the id:
<?php echo $this->escapeHtml($pea->country);?>
Thank you for reading, and for any help, which could lead me into the right direction!
You should not use the #Column anotation in the $ic_hq_country field of the Peopleentity.
/**
* #ORM\OneToOne(targetEntity="Country")
* #ORM\JoinColumn(name="ic_hq_country", referencedColumnName="id")
*/
protected $ic_hq_country;
like this, hopefully ic_hq_country will be a proxy to the entity instead of the id.
so in your view you can use:
<?php echo $pea->ic_hq_country->getId();?>
and also
<?php echo $this->escapeHtml($pea->ic_hq_country->getCountry());?>

Doctrine HasLifecycleCallbacks PrePersist|PreUpdate don't fire

I'm using Doctrine 2 with ZF2 and the Doctrine-Module.
I've written an Entity that needs a PreUpdate|PrePersist, because Doctrine doesn't allow
Date|Datetime in a primary key:
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
*
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
* #ORM\Table(name="sample")
*/
class Sample
{
/**
*
* #ORM\Id
* #ORM\Column(type="string")
* #var integer
*/
protected $runmode;
/**
*
* #ORM\Id
* #ORM\Column(type="date")
* #var DateTime
*/
protected $date;
public function getRunmode()
{
return $this->runmode;
}
public function setRunmode($runmode)
{
$this->runmode = $runmode;
return $this;
}
public function getDate()
{
return $this->date;
}
public function setDate($date)
{
$this->date = $date;
return $this;
}
/**
*
* #ORM\PreUpdate
* #ORM\PrePersist
*/
protected function formatDate()
{
die('preUpdate, prePersist');
if ($this->date instanceof \DateTime) {
$this->date = $this->date->format('Y-m-d');
}
return $this;
}
}
The Problem is now, if i set a DateTime as a Date I get the message:
"Object of class DateTime could not be converted to string"
because it doesn't walk into the formatDate.
First of all, since you mapped the field Sample#date as datetime, it should always be either null or an instance of DateTime.
Therefore, you should typehint your setDate method as following:
public function setDate(\DateTime $date = null)
{
$this->date = $date;
return $this;
}
Also, your lifecycle callback is not invoked because the visibility of method formatDate is protected, therefore not accessible by the ORM. Change it into public and it will work. No conversion should be necessary anyway, so you can get rid of it.