How to get the newest entity in a OneToMany relation with Doctrine - symfony

Intro
For this question the following two entities are the main characters:
ObjectElementTask
Activitites
One ObjectElementTask can have many activities.
Goal
I want to get a collection of ALL Activities and order them by ObjectElementTask and get only the last Activity (based on the 'executiondate' of the activities).
Question
How can i setup the querybuilder from Doctrine to achieve my goal? BTW: I am using Symfony 2.8.
Entities
ObjectElementTask:
class ObjectElementTask
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Activity", mappedBy="objectelementtask", cascade={"persist", "remove"}, orphanRemoval=true)
* #ORM\OrderBy({"executionDate" = "DESC"})
* #Expose
*/
private $activities;
Activity
class Activity {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="ObjectElementTask", inversedBy="activities", cascade={"persist"})
* #ORM\JoinColumn(name="objectelementtask_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $objectelementtask;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
protected $executionDate;
Thanks in advance!
Edit
Based on the answer in this question:
DQL Select every rows having one column's MAX value
i have come up with the following code:
$qb = $this->getDoctrine()
->getRepository('AppBundle:Activity')
->createQueryBuilder('a')
->select('MAX(a.executionDate) maxDate');
$qb2 = $this->getDoctrine()
->getRepository('AppBundle:ObjectElementTask')
->createQueryBuilder('o');
$qb2->innerJoin('AppBundle:ObjectElementTask', 'oe', 'WITH', $qb2->expr()->eq('oe.activities', '(' . $qb->getDQL() . ')'));
But as you can guess, this doesn't work. It only returns one row (the newest in the Activity). And i want the newest activity of each ObjectElementTask.
Edit 2
I tried this code:
$activities = $this->getDoctrine()
->getRepository('AppBundle:Activity')
->createQueryBuilder('a')
->select('a, MAX(a.executionDate) AS HIDDEN max_date')
->groupBy('a.objectelementtask')
->getQuery()
->getResult();
And the max date returns the value of the last activitie but not the full entity (only the max date...)
Edit 3:
This is the final solution:
This is how i finally got what i needed:
$em = $this->getEntityManager();
$rsm = new ResultSetMappingBuilder($em);
$query = $em->createNativeQuery('SELECT a1.* FROM activity a1 INNER JOIN ( SELECT id, max(execution_date) MaxPostDate, objectelementtask_id FROM activity GROUP BY objectelementtask_id ) a2 ON a1.execution_date = a2.MaxPostDate AND a1.objectelementtask_id = a2.objectelementtask_id order by a1.execution_date desc', $rsm);
$rsm->addRootEntityFromClassMetadata('AppBundle:Activity', 'a1');
$activities = $query->getResult();

This is how i finally got what i needed:
$em = $this->getEntityManager();
$rsm = new ResultSetMappingBuilder($em);
$query = $em->createNativeQuery('SELECT a1.* FROM activity a1 INNER JOIN ( SELECT id, max(execution_date) MaxPostDate, objectelementtask_id FROM activity GROUP BY objectelementtask_id ) a2 ON a1.execution_date = a2.MaxPostDate AND a1.objectelementtask_id = a2.objectelementtask_id order by a1.execution_date desc', $rsm);
$rsm->addRootEntityFromClassMetadata('AppBundle:Activity', 'a1');
$activities = $query->getResult();

Related

Symfony\Doctrine createQueryBuilder() select 'not in' from a OneToMany relation

I have three entities : Trophy | Competition | Season
One Competition is created for one trophy for one season (you can't have two competitions with same combination "season + trophy").
Competition as a ManyToOne relation with Trophy, and a ManyToOne relation with Season.
Trophy and Season have no direct relation.
I want to display two dropdowns on a page with the content of the second one being dependent from the value of the first :
First dropdown allow to select a trophy type (which is a property of Trophy entity), second dropdown must list seasons that are "still available" for trophy type selected (meaning by that "list all seasons for which there are no competition for this trophy type")
I've got almost all working (listener in the Formtype, ajax etc) I've created a specific function allWithoutThisCompetitionType() in SeasonRepository. Function is correctly called every-time user select a new value in dropdown BUT... I don't know anything about SQL nor dql, so I'm struggling to find the correct formulation for my query. I've tried with notin(), with "sub" or "nested" queries... I definitely don't know what I'm doing...
How can I do something like ? :
$qb = $em->getRepository(\App\Entity\Seasonmanager\Season::class)->createQueryBuilder('s')
->where('s.competitions.trophy != :trophy')
->setParameter('trophy', $trophy);
= Here are all the seasons for which no competition has been already created with this trophy
Thank you for your help.
Trophy entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\Seasonmanager\TrophyRepository")
*/
class Trophy
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $uniqueid;
// other properties here...
//////////////////////////////////////////////////////////////////////////////////
//// LIAISONS VERS D'AUTRES ENTITY ////
/**
* #ORM\OneToMany(targetEntity="App\Entity\Seasonmanager\Competition", mappedBy="trophy", orphanRemoval=true)
*/
private $competitions;
Competition entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\Seasonmanager\CompetitionRepository")
* #UniqueEntity(
* fields={"trophy","season"},
* errorPath="trophy",
* message="Une compétition existe déjà pour ce trophée et cette saison"
* )
*/
class Competition
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
// other properties here...
//////////////////////////////////////////////////////////////////////////////////
//// LIAISONS VERS D'AUTRES ENTITY ////
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Seasonmanager\Trophy", inversedBy="competitions")
* #ORM\JoinColumn(nullable=false)
*/
private $trophy;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Seasonmanager\Season", inversedBy="competitions")
* #ORM\JoinColumn(nullable=false)
*/
private $season;
Season entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\Seasonmanager\SeasonRepository")
* #UniqueEntity("yearin")
*/
class Season
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="integer", length=4)
*/
private $yearout;
/**
* #ORM\Column(type="string", length=8)
*/
private $uniqueid;
// other properties here...
//////////////////////////////////////////////////////////////////////////////////
//// LIAISONS VERS D'AUTRES ENTITY ////
/**
* #ORM\OneToMany(targetEntity="App\Entity\Seasonmanager\Competition", mappedBy="season", orphanRemoval=true)
*/
private $competitions;
The SeasonRepository where I try to add my query :
namespace App\Repository\Seasonmanager;
use App\Entity\Seasonmanager\Season;
use App\Entity\Seasonmanager\Trophy;
use App\Entity\Seasonmanager\Competition;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;
/**
* #method Season|null find($id, $lockMode = null, $lockVersion = null)
* #method Season|null findOneBy(array $criteria, array $orderBy = null)
* #method Season[] findAll()
* #method Season[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class SeasonRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Season::class);
}
public function allWithoutThisCompetitionType($type): array
{
$em = $this->getEntityManager();
$trophys = $em
->getRepository(Trophy::class)
->findBy(['uniqueid' => $type],['id'=>'DESC'])
;
$trophy = reset($trophys);
$qb = $em->getRepository(\App\Entity\Seasonmanager\Season::class)->createQueryBuilder('s')
->where('s.competitions.trophy', $trophy);
$query = $qb->getQuery();
$result = $query->getResult();
$donnees = $result;
return $donnees;
}
Here is the query, though, I'm not 100% sure it will match your need.
Let me know in comment if something is wrong, I will edit my answer.
public function allWithoutThisCompetitionType($trophy) {
// Split init from the rest of the query in case you need to use `$qb->expr()`
$qb=$this->createQueryBuilder("season");
$qb->leftJoin("season.competition", "competition") // Join competition
->join("competition.trophy", "trophy") // Join Trophy
->andWhere($qb->expr()->orX( // Or (either one of the following satements)
$qb->expr()->isNull("competition.id"),
$qb->expr()->notIn("trophy.uniqueid", ":trophy")))
->setParameter("trophy", $trophy);
return $qb->getQuery()->getResult();
}

Doctrine: select a table that is not managed by doctrine

Using the Doctrine QueryBuilder, I want to execute a query which in native SQL looks like this:
`SELECT image FROM image i INNER JOIN about_images a ON i.id = a.image_id`;
The result in DQL is as follows:
ImageRepository.php:
return $this->createQueryBuilder('i')
->select('i')
->innerJoin('about_images', 'a', 'WITH', 'i.id = a.imageId')
->getQuery()
->getResult();
Where image is an entity, and about_images is a join table (the result of a #ManyToMany relationship). However, I get the error that about_images is not defined, which makes sense as it is not managed by doctrine.
AboutPage.php (i.e the entity where the join table is created)
/**
* #var Image[]|ArrayCollection
*
* #ORM\ManyToMany(targetEntity="App\Entity\Image", cascade={"persist", "remove"})
* #ORM\JoinTable(name="about_images",
* joinColumns={#ORM\JoinColumn(name="about_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="image_id", referencedColumnName="id", unique=true)})
*/
private $images;
Fields from Image entity:
/**
* #var int
*
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var string
*
* #ORM\Column(type="string", length=255)
*/
private $image;
/**
* #var File
*
* #Vich\UploadableField(mapping="collection_images", fileNameProperty="image")
* #Assert\File(maxSize="150M", mimeTypes={"image/jpeg", "image/jpg", "image/png", "image/gif"},
* mimeTypesMessage="The type ({{ type }}) is invalid. Allowed image types are {{ types }}")
*/
private $imageFile;
/**
* #var string
*
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $imageAlt;
/**
* #var DateTime
*
* #ORM\Column(type="datetime")
*/
private $updatedAt;
/**
* #var string
*
* #ORM\Column(type="string", nullable=true)
*/
private $alt;
How can I solve this problem? The results should be Image entities.
You can write native SQL and then map the output to your entities using a ResultSetMapper.
For your example it could look something like this in your Repository class:
public function findImagesWithAboutImages()
{
$sql = 'SELECT i.* FROM image i INNER JOIN about_images a ON i.id = a.image_id';
$entityManager = $this->getEntityManager();
$mappingBuilder = new ResultSetMappingBuilder($entityManager);
$mappingBuilder->addRootEntityFromClassMetadata(Image::class, 'i');
$query = $entityManager->createNativeQuery($sql, $mappingBuilder);
// If you want to set parameters e.g. you have something like WHERE id = :id you can do it on this query object using setParameter()
return $query->getResult();
}
If you want related data you will have to add it to the select clause with an alias and then use $mappingBuilder->addJoinedEntityFromClassMetadata() to assign these fields to the joined entity much like above with the root entity.
Your annotations in your entity already define how each field maps to a property and what type it has, so basically you should get an array of Image-entities with everything (but the related entities) loaded usable.
It is not quite clear the example sql with the code you have provided, but if you have a relation defined in your entities, you can join them with a query builder just by telling the relation field of the entity, so I think this should work
return $this->createQueryBuilder('i')
->select('i')
->innerJoin('i.images', 'a')
->getQuery()
->getResult();
As you have defined already your relations in your entities, Doctrine knows how to join your tables, so you just have to specify the relation field name and the alias.
And always remember that you have to use the field name in your entity (normally cameCasedStyle), not the column name at your database tables (normally snake_cased_style).

Symfony2 Doctrine Query, only populate single field in OneToMany Relationship

Is it possible to only populate a single field in a QueryBuilder query that fetches the relationship, my case:
$query = $em->createQuery(
'SELECT s FROM IREnterpriseAppBundle:StockItem s
WHERE s.user = :currentUser AND s.deleted = 0
ORDER BY s.id DESC'
)->setParameters(array('currentUser' => $user));
Now the entity StockItem has a relationship to the user:
/**
* StockItem
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="IREnterprise\AppBundle\Entity\StockItemRepository")
* #ORM\HasLifecycleCallbacks
*
* #ExclusionPolicy("all")
*
*/
class StockItem
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #Expose
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="IREnterprise\UserBundle\Entity\User", inversedBy="stockItems")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* #Expose
**/
private $user;
}
On the aforementioned query, the entire User object is fetched alongside with the stockitem, is it possible to only fetch/set a single field on the user object?
Since i am having issues excluding the rest of the user object with #exclusion policy: JMSSerializerbundle #expose relationship, ignores other entities policies
As a last resort i have to unset the fields of the user object in code, but that just seems retarded.
It is possible to only fetch a single field from a related entity but doing so, you won't be able to persist any change to the result.
If that's ok with you, you should use a JOIN and specify which fields from the related entity you want to fetch.
As mentioned in another answer, you can use createQueryBuilder instead of createQuery.
createQueryBuilder code sample:
$result = $em->getRepository('IREnterpriseAppBundle:StockItem')
->createQueryBuilder('s')
->select('s.id, u.id as user_id')
->join('s.user', 'u')
->where('s.user = :currentUser')
->andWhere('s.deleted = 0')
->setParameter('currentUser', $user)
->getQuery()
->getResult();
createQuery code sample:
$query = $em->createQuery(
'SELECT s.id, u.id as user_id FROM IREnterpriseAppBundle:StockItem s
JOIN s.user u
WHERE s.user = :currentUser AND s.deleted = 0
ORDER BY s.id DESC'
)->setParameters(array('currentUser' => $user));

Symfony join entities in one-to-many relationship

I am working on a Symfony project recording sales as relating to stock.
My reasoning:
one sale item is associated to one stock item
one stock item can be associated to multiple sale items
As a result, I setup a one-to-many sale-to-stock relationship as show in the following code snippets:
class Sale
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var float
*
* #ORM\Column(name="cost", type="float")
*/
private $cost;
/**
* #var \DateTime
*
* #ORM\Column(name="date", type="datetime")
*/
private $date;
/**
* #ORM\ManyToOne(targetEntity="iCerge\Salesdeck\StockBundle\Entity\Stock", inversedBy="sales")
* #ORM\JoinColumn(name="sid", referencedColumnName="id")
*/
protected $stock;
...
... and ...
class Stock
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #var \DateTime
*
* #ORM\Column(name="created", type="datetime")
*/
private $created;
/**
* #var \DateTime
*
* #ORM\Column(name="updated", type="datetime")
*/
private $updated;
/**
* #ORM\OneToMany(targetEntity="iCerge\Salesdeck\SalesBundle\Entity\Sale", mappedBy="stock")
*/
protected $sales;
...
NOW, if my code for implementing a one-to-many relationship is correct, I am trying to load a sale object with it's associated stock data in one query using the following code:
public function fetchSale($sid){
$query = $this->createQueryBuilder('s')
->leftjoin('s.stock', 't')
->where('s.id = :sid')
->setParameter('sid', $sid)
->getQuery();
return $query->getSingleResult();
}
The fetchSale function is from my projects SaleRepository.php class.
The leftjoin part of the query is what I hoped would successfully fetch the related stock information but I just get no output ([stock:protected]) as is shown below:
myprog\SalesProj\SalesBundle\Entity\Sale Object
(
[id:myprog\SalesProj\SalesBundle\Entity\Sale:private] => 50
[cost:myprog\SalesProj\SalesBundle\Entity\Sale:private] => 4.99
[date:myprog\SalesProj\SalesBundle\Entity\Sale:private] => DateTime Object
(
[date] => 2015-04-18 17:12:00
[timezone_type] => 3
[timezone] => UTC
)
[stock:protected] =>
[count:myprog\SalesProj\SalesBundle\Entity\Sale:private] => 5
)
How I can successfully fetch a sales' related stock data in the same query?
Doctrine is lazy-loading by default, so it's possible that $stock hasn't been initialized and the dump is not showing it. Try dumping $sale->getStock(). That tells Doctrine to go fetch it.
You can also force the loading of the Stock by selecting it:
public function fetchSale($sid){
$query = $this->createQueryBuilder('s')
->leftjoin('s.stock', 't')
->where('s.id = :sid')
->setParameter('sid', $sid)
->select('s', 't')
->getQuery();
return $query->getSingleResult();
}
By the way, fetchSale($sid) as it is now is the same as calling:
$entityManager->getRepository('SalesBundle:Sale')->find($sid);
You can use Doctrine's eager loading feature.
1. Always load associated object:
If you always want to fetch the stock object when loading the sale object, you can update your entity definition (see docs for #ManyToOne) by adding a fetch="EAGER" to the #ManyToOne definition:
/**
* #ORM\ManyToOne(targetEntity="iCerge\Salesdeck\StockBundle\Entity\Stock", inversedBy="sales", fetch="EAGER")
* #ORM\JoinColumn(name="sid", referencedColumnName="id")
*/
protected $stock;
Doctrine will then take care of loading all required objects in as few queries as possible.
2. Sometimes load associated object:
If you want to load the associated object only in some queries and not by default, according to the manual you can also tell Doctrine to use eager loading on a specific query. In your case, this may look like:
public function fetchSale($sid){
$query = $this->createQueryBuilder('s')
->where('s.id = :sid')
->setParameter('sid', $sid)
->getQuery();
$query->setFetchMode("FQCN\Sale", "stock", \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER);
return $query->getSingleResult();
}

Doctrine and join result type

I've 2 simple tables with many-to-one mono directional relations:
class Event extends BaseEvent
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
*
* #ORM\ManyToOne(targetEntity="Rposition")
* #ORM\JoinColumn(name="owner", referencedColumnName="id",onDelete="CASCADE", nullable=false )
**/
private $owner;
....
and the origin of the owner field class:
class Rposition
{
/**
* #var integer
*
* #ORM\Column(name="id", type="string", length=36)
* #ORM\Id
* #ORM\GeneratedValue(strategy="UUID")
*/
private $id;
/**
*
*
* #ORM\Column(name="scenario", type="string", length=255, nullable=false)
*/
private $scenario;
both class having the right getter/setter and a __tostring()
I need to implements a query like this:
public function findAllByScenario($scenario) {
$q = $this->getEntityManager()->createQuery("SELECT e , r.scenario , r.id
FROM LapoMymeBundle:Event e
JOIN LapoMymeBundle:Rposition r
WITH e.owner = r.id
WHERE r.scenario = :scenario
ORDER BY e.start ASC, e.end ASC
")->setParameter('scenario', $scenario);
try {
return $q->getResult();
} catch (\Doctrine\ORM\NoResultException $e) {
return null;
}
}
-is it the right way to making a join and sending back result from it?
-what are the right way for fetching the fields from the array of objects?
something like that's:
$events=$this->getDoctrine()->getManager()
->getRepository('LapoMymeBundle:Event')->findAllByScenario($scenario);
$outputUsersArray = Array();
foreach($events as $event)
{
$eventArray = Array();
$eventArray[JSONResponseFields::KEY_ID] = $event->getId();
$eventArray[JSONResponseFields::KEY_NAME] = $event->getOwner()->getId();
$outputEventsArray[] = $eventArray;
} ....
rise an error likes:
FatalErrorException: Error: Call to a member function getId() on a non-object in.....
You can use Doctrine Query builder to get this working. Assuming you have Querybuilder instance. The query would look like
$qb = this->getDoctrine()->getRepository('BundleName:Event')->createQueryBuilder('e');
$query = $qb
->select('e','p')
->innerjoin('u.owner','r')
->where('r.scenario= :scenario')
->setParameter('scenario', $scenarioid)
->getQuery();
$chatuser = $query->getResult();

Resources