Dynamically eager loading deep relationships with Doctrine - symfony

I'm currently working on an API using the following stack;
Symfony (3)
FOSRestBundle
Fractal
I'm wanting to integrate the ability to specify, via query parameter, which relationships to include when retrieving an entity/collection, e.g;
[GET] /users?include=friends.addresses
Fractal comes with the ability to handle includes however, as this happens around the serialization point of the response building, each related entity is retrieved via lazy loading, thus triggering additional queries.
Is there a way to tell Doctrine, when retrieving a collection, to dynamically also retrieve relationships specified? Ive seen the following from the Doctrine docs which shows how to dynamically change the fetch mode however this only seems to work with associations on the target entity (friends in the example above) and not deeper relations (addresses of friends in the example).
Thanks!

If I remember correctly you can "preload" relations by joining them in rather than letting the lazy loading mechanism handle it. An idea could be to create a service that creates a query builder based on your criteria. This is a crude snippet of what I mean:
class EagerService
{
protected $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function resolveIncludes($class, $alias, $includes)
{
// Parse includes into an array
if (strpos($includes, '.') !== false) {
$relations = explode('.', $includes);
} else {
$relations = [$includes];
}
// The next relation is owned by the previous one, so we keep track of the previous relation
$previousRelation = $alias;
$qb = $em->getRepository($class)->getQueryBuilder($previousRelation);
foreach ($relations as $relation) {
// Add inner joins to the query builder referencing the new relation
$qb->innerJoin("{$previousRelation}.{$relation}", $relation);
$previousRelation = $relation;
}
// Return query builder or the result of the query
return $qb;
}
}

Related

Receive all kind of Entity as an Argument in Symfony

I have a method to get a change set of Entity from the unit of work (entity manager) in Symfony and I would like it to receive all Entities (Post, User...) instead of specific Entity.
public function getChanges(Post $event): array
{
$uow = $this->entityManager->getUnitOfWork();
$uow->computeChangeSets();
return $uow->getEntityChangeSet($event);
}
Do you have any idea to do it?
One solution is getting the object as the argument but I prefer to get only the Symfony Entity objects in the function.
Look for doctrine entity listener.
https://symfony.com/doc/current/doctrine/events.html#doctrine-entity-listeners
And do not filter entity inside it, remove this part from the example:
// if this listener only applies to certain entity types,
// add some code to check the entity type as early as possible
if (!$entity instanceof Product) {
return;
}
if (!$this->entityManager->contains($entity)) {
throw new Exception('The given object must be doctrine entity');
}

What is the best way to create a singleton entity in Symfony 4?

I want to create a settings page, which only has a form in it. If the form is submitted it only updates settings entity but never creates another one. Currently, I achieved this like:
/**
* #param SettingsRepository $settingsRepository
* #return Settings
*/
public function getEntity(SettingsRepository $settingsRepository): Settings
{
$settings = $settingsRepository->find(1);
if($settings == null)
{
$settings = new Settings();
}
return $settings;
}
In SettingsController I call getEntity() method which returns new Settings entity (if the setting were not set yet) or already existing Settings entity (if setting were set at least once).
However my solution is quite ugly and it has hardcoded entity id "1", so I'm looking for a better solution.
Settings controller:
public function index(
Request $request,
SettingsRepository $settingsRepository,
FlashBagInterface $flashBag,
TranslatorInterface $translator,
SettingsService $settingsService
): Response
{
// getEntity() method above
$settings = $settingsService->getEntity($settingsRepository);
$settingsForm = $this->createForm(SettingsType::class, $settings);
$settingsForm->handleRequest($request);
if ($settingsForm->isSubmitted() && $settingsForm->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($settings);
$em->flush();
return $this->redirectToRoute('app_admin_settings_index');
}
return $this->render(
'admin/settings/index.html.twig',
[
'settings_form' => $settingsForm->createView(),
]
);
}
You could use Doctrine Embeddables here.
Settings, strictly speaking, should not be mapped to entities, since they are not identifiable, nor meant to be. That is, of course, a matter of debate. Really, a Settings object is more of a value object than an entity. Read here for more info.
So, in cases like these better than having a one to one relationship and all that fuzz, you probably will be fine with a simple Value Object called settings, that will be mapped to the database as a Doctrine Embeddable.
You can make this object a singleton by creating instances of it only in factory methods, making the constructor private, preventing cloning and all that. Usually, it is enough only making it immutable, meaning, no behavior can alter it's state. If you need to mutate it, then the method responsible for that should create a new instance of it.
You can have a a method like this Settings::createFromArray() and antoher called Settings::createDefaults() that you will use when you new up an entity: always default config.
Then, the setSettings method on your entity receieves only a settings object as an argument.
If you don't like inmutablity, you can also make setter methods for the Settings object.

Doctrine 2, prevent getting unjoined entities

given a user and his coupons, I want to get a user and all of his coupons:
foreach ($this->createQueryBuilder('x')->select('u, c')->where('x.email = ?0')->setParameter(0, $email)->leftJoin('u.coupons', 'c')->getQuery()->getResult() as $entity)
{
$entity->getCoupons();
}
this is very good until I forget to join the coupons:
foreach ($this->createQueryBuilder('x')->select('u')->where('x.email = ?0')->setParameter(0, $email)->getQuery()->getResult() as $entity)
{
$entity->getCoupons();
}
sadly this still works even though no coupons were joined. Here it does an other SELECT. In additional, this 2nd select will be wrong. Id rather want to get a exception or AT LEAST an empty array instead. Is there any workaround for this?
What you're experiencing is expected doctrine behavior.
When you select a User entity, Doctrine will get the record from the database. If you aren't explicitly joining the Coupon entity (or any other entities with relationship to User), Doctrine will create a Proxy object. Once you access this proxy object by calling $user->getCoupons(), Doctrine will fire a new query to the database to get the coupons for your User entity. This is called lazy-loading.
I'm not sure if there is a way to change this in the way you described.
What you can do is to create a method in your UserRepository called findUserAndCoupons($email) and have your query there. Whenever you need to find a user and his coupons, you could simply retrieve it in your controller using:
class MyController extends Controller {
public function myAction(){
$user = $this->getDoctrine()->getRepository('UserRepository')->findUserAndCoupons($email);
foreach($user->getCoupons() as $coupon) {
// ....
}
}
}
This way you won't need to remember the actual query and copy/paste it all over the place. :)

symfony2 + doctrine2 - how lazy loading affect our applications?

This will be a sort of Q/A for everyone that wonder about symfony2 + doctrine2 and performance.
I'm writing this Q/A because I've started to playing around SF2+doctrine for a new project and I wanted to do some performance tests. I've surfed the net but didn't find a complete answer for my doubts so I've decided to make my own one. Hope this will be useful for someone
Let's say we have two entities like the following
//all ORM declaration here
class Lodging
{
//some declaration here
/**
* #ORM\ManyToOne(targetEntity="Currency")
* #ORM\JoinColumn(name="currency_iso_code", referencedColumnName="iso_code")
*/
protected $currency;
//some methods here
}
//all ORM declaration here
class Currency
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=3)
*/
protected $iso_code;
/**
* #ORM\Column(type="string")
*/
protected $description;
//some methods here
}
Now you will made an entity type for Lodging where you will be able to create or - if the entity passed to the type has some prefetched values - to edit a Lodging object (let's concentrate on that case). In that form you also need the description of Currency, not only the $iso_code.
Question: is better to use ->findOneById() doctrine2 method or to write a DQL (or use query builder facility)?
Why?
PREMABLE
This answer has sense only if you're experiencing a slow page that's using lazy loads. For more common actions (load just one entity, load a "static page" or a cached one and so on) you could continue to use built in function provided by doctrine
Well, let's analyze some of the different methodologies you can follow
1 - findOneById()
Into controller you have chosen to follow more "common" and "fast" way to procede: use pre-existent findOneById() method. Wonderful.
Let's check performance
Number of queries done: three query were done as we need one to retrieve Lodging object, the second one to fetch currently associated Currency (lazy loaded) and one to fetch all Currency entities (because you can change from one currency to other)
Page loading time: about 500ms
Memory usage: about 32MB
2 - Write a custom repository method
2.1 "Basic" DQL repository function
public function findLodgingById($id)
{
$lodging = null;
$q = $this->createQueryBuilder('lodging')
->select('lodging')
->where('lodging.id = :id')
->setParameter('id', $id)
->getQuery();
$lodging_array = $q->getResult(); //will not fetch a single object but array
if ($lodging_array) {
$lodging = reset($lodging_array);
}
return $lodging;
}
Let's check performace
Number of queries done: it shouldn't be a surprise for you but ... number of query done is always three! Of course you're not doing anything but same of findOneById() (an, maybe, even in a worst way!). You're taking advantage of lazy loading again.
Page loading time: about 500ms. Loading time didn't change
Memory usage: about 34MB. Memory usage is increased of 6,25 % (due to array?)
2.2 DQL with JOIN
public function findLodgingById($id)
{
$lodging = null;
$q = $this->createQueryBuilder('lodging')
->select('lodging')
->leftJoin('lodging.currency', 'currency')
->where('lodging.id = :id')
->setParameter('id', $id)
->getQuery();
$lodging_array = $q->getResult(); //will not fetch a single object but array
if ($lodging_array) {
$lodging = reset($lodging_array);
}
return $lodging;
}
Let's check performace
Number of queries done: The number of queries dind't change! But ... why? We are telling explicitly to doctrine2 to join currency entity but it seems to ignore that instruction. The answer is that we're not selecting currency entity also so doctrine2 will use, again, lazy loading facility.
Page loading time: about 500ms. Loading time didn't change 2.1
Memory usage: about 34MB. Memory usage didn't changed from 2.1
2.3 Let's try something better: Join with Currency selection
public function findLodgingById($id)
{
$lodging = null;
$q = $this->createQueryBuilder('lodging')
->select('lodging', 'currency')
->leftJoin('lodging.currency', 'currency')
->where('lodging.id = :id')
->setParameter('id', $id)
->getQuery();
$lodging_array = $q->getResult(); //will not fetch a single object but array
if ($lodging_array) {
$lodging = reset($lodging_array);
}
return $lodging;
}
Let's check performace
Number of queries done: Finally number of queries decreased! We reach two query instead of three. What query gone? Lazy loading of associated (current) currency is gone but, of course, you have to fetch all possible currency.
Page loading time: about 350ms.
Memory usage: about 34MB. Memory usage didn't changed
Definitive Solution (?)
public function findLodgingById($id)
{
$lodging = null;
$q = $this->createQueryBuilder('lodging')
->select('lodging')
->where('lodging.id = :id')
->setParameter('id', $id)
->getQuery();
$q->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true);
$lodging_array = $q->getResult(); //will not fetch a single object but array
if ($lodging_array) {
$lodging = reset($lodging_array);
}
return $lodging;
}
The $q->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true); line of code seems to save time and memory compared to other solutions.
Number of queries done: Two, of course
Page loading time: about 340ms.
Memory usage: about 32MB.
PLEASE PAY ATTENTION
This solution doesn't let you change associated (Currency) entity as it will be betched with Query::HINT_FORCE_PARTIAL_LOAD, true
Comments
Results seems to be good for page loading time (memory usage of course will not change) and though performances seems to be only "a little" better, you shouldn't not focus ONLY onto results: here we're taking as an example only a simple snippet of code with just one lazy loading operation: think about an entity (or, worst, a lot of entity like blog posts with related comments) that will do wrong(*) lazy loading for every entity fetched and managed: you could reach even 50-70 query for a single page (and of course, in this case, you could notice easily performace benefits due to "single" query)
(*) Why I say wrong? Because if you can migrate the fetching-logic of your objects elsewhere or if you already know what entity/attribute you need, so your contents aren't dynamic and you can know them before use, lazy-loading is not only useless but also harmful.
Contrariwise if you can't know at "coding writing time" what properties or entities you'll need, of course lazy loading could save you memory (and time) wasting for useless objects/associations.
Final thoughts
It's better to "lose" some minutes for write DQL query (that seems to be even silly) that use "built-in" ones. Moreover you should use array (and not objects) for read-only operation (list elements that couldn't be modificated) changing getResult() method call as follows: getResult(Doctrine\ORM\Query::HYDRATE_ARRAY);. That will change "default" value (HYDRATE::OBJECT)
It seems like premature optimisation to be worrying about it before you're seeing a problem.
Write whatever's quickest, and in this case, that's often going to be $em->findOneById($id) style. Then use valgrind to look for bottlenecks.
Looking at the amount of time you spend writing all that custom DQL, the overall peformance could be improved by fixing a bigger problem somewhere else in your application.

$em->remove() symfony2 erasing all rows

I have an issue when erasing something from a BD.
The problem is that it not only erase the object i looked for (using findOneBy), but all the objects related to the principal id.
//---Controller
$new = $this->getDoctrine()->getManager();
$OBJcar = $new->getRepository('SomeOtherBundle:CarEntityClass')
->findOneBy(array('idOwner' => $idowner, 'idCar' => $idcar));
if($OBJcar){
$new->remove($OBJcar);
$new->flush();
$msj="The car for an specific owner has been erased.";
}
//---Profiler (Query)
"START TRANSACTION"
Parameters: { }
Time: 0.22 ms
DELETE FROM schema.CarTable WHERE id_owner = ?
Parameters: ['123456']
Time: 0.63 ms
"COMMIT"
Parameters: { }
Time: 0.63 ms
How to erase the one row i am getting from the db?
I voted down the answer above, because I'm tired of people using string DQLs.
It's not standartized, non-object oriented(even though in background dql operates with objects), it doesn't use caching mechanisms query builder provides, it's non-flexible and simply looks unclean.
Here is the "right way"(IMHO):
You add the repository class for entity
You add the method you need with a query builder in it
You call the method while passing parameters needed for specific REPOSITORY OBJECT ORIENTED ACTION
You get an easy-to-handle result
Here's the code:
namespace ProjectName\BundleName\Repository;
use Doctrine\ORM\EntityRepository;
class CarRepository extends EntityRepository
{
public function deleteCarWithOwner($ownerId,$carId)
{
$isDeleted = $this->createQueryBuilder("car")
->delete()
->where('car.id = :carId')->setParameter("carId", $carId)
->andWhere('car.idOwner = :ownerId')->setParameter("ownerId", $ownerId)
->getQuery()->execute();
return $isDeleted;
}
}
Also, refer to http://doctrine-orm.readthedocs.org/en/latest/reference/query-builder.html for query builder details. There are a lot of "pros" and I see no "cons" for using builder.
LATE UPDATE
Also, a lot of Doctrine's entity events are not dispatched when using DQL.
Use DQL
$query = $em->createQuery('DELETE SomeOtherBundle:CarEntityClass c WHERE c.idOwner = 4 AND c.id = 10');
$query->execute();
This will remove only single car with ID 10 and owner with ID 4.

Resources