How to get a collection of related entities by using Doctrine ResultSetMapping? - symfony

I use Doctrine 2.3.4. and Symfony 2.3.0
I have two entities: Person and Application.
Application gets created when some Person applies for a job.
Relation from Person to Application is OneToMany, bidirectional.
Using the regular Doctrine documentation here I managed to get a correct result set only when working with a single entity.
However, when I add joined entity, I get a collection of root entities but joined to a wrong related entity.
In other words, the problem is that I get a collection of Applications but all having the same Person.
Native sql query, when executed directly returns a correct result.
This is the code:
$sql = "SELECT a.id, a.job, p.first_name, p.last_name
FROM application a
INNER JOIN person p ON a.person_id = p.id";
$rsm = new ResultSetMapping;
$rsm->addEntityResult('\Company\Department\Domain\Model\Application', 'a');
$rsm->addFieldResult('a','id','id');
$rsm->addFieldResult('a','job','job');
$rsm->addJoinedEntityResult('\Company\Department\Domain\Model\Person' , 'p', 'a', 'person');
$rsm->addFieldResult('p','first_name','firstName');
$rsm->addFieldResult('p','last_name','lastName');
$query = $this->em->createNativeQuery($sql, $rsm);
$result = $query->getResult();
return $result;
Here are the Entity classes:
namespace Company\Department\Domain\Model;
use Doctrine\ORM\Mapping as ORM;
/**
* Person
*
* #ORM\Entity
* #ORM\Table(name="person")
*/
class Person
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string First name
*
* #ORM\Column(name="first_name",type="string",length=255)
*/
private $firstName;
/**
* #var string Last name
*
* #ORM\Column(name="last_name",type="string",length=255)
*/
private $lastName;
/**
*
* #var Applications[]
* #ORM\OneToMany(targetEntity="Application", mappedBy="person")
*/
private $applications;
Application class:
namespace Company\Department\Domain\Model;
use Doctrine\ORM\Mapping as ORM;
/**
* Application (Person applied for a job)
*
* #ORM\Entity
* #ORM\Table(name="application")
*/
class Application
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var Person
*
* #ORM\ManyToOne(targetEntity="Person", inversedBy="applications")
* #ORM\JoinColumn(name="person_id", referencedColumnName="id")
*/
private $person;
/**
* #var string
* #ORM\Column(name="job",type="string", length=100)
*/
private $job;
I must be missing something here?

Found out where the error was:
The Person->id property has to be mapped too.
Also, order of columns in SELECT clause has to match the order of addFieldResult() statements.
Therefore, $sql should look like this:
SELECT a.id, a.job, p.id AS personId, p.first_name, p.last_name
FROM application a
INNER JOIN person p ON a.person_id=p.id
And mapping for related property like this:
$rsm->addJoinedEntityResult('\Company\Department\Domain\Model\Person' , 'p', 'a', 'person');
$rsm->addFieldResult('p','personId','id');
$rsm->addFieldResult('p','first_name','firstName');
$rsm->addFieldResult('p','last_name','lastName');
So, the mapped field result column name corresponds to sql result column name, and third parameter, id in this case, should be the property actual name.

Related

How to modify Symfony ORM insert\update query

I have project that is migrate to Symfony, that project have multiple tables,and also some tables are migrated to ORM, but now i need to incert/update from Symfony to table that have Entity but not managed by ORM. Problem consist in not null columns that require some value and in Entity I cannot define that value because of table relations.
It posible to edit MySql query before they submited to Database.
For example i have Entity:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* User
*
* #ORM\Table(name="p_user")
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User
{
/**
* #var int
*
* #ORM\Column(name="user_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string|null
*
* #ORM\Column(name="name", type="string", length=55, nullable=true)
*/
private $name;
/**
* #var Permission
*
* #ORM\ManyToOne(targetEntity="Permission", inversedBy="user", cascade={"persist", "remove"})
* #ORM\JoinColumn(name="permission_id", referencedColumnName="permission_id", onDelete="CASCADE")
*/
private $permission;
}
permission_id can be null but in database is not null with default value 0, same for name but with default value ''.
That mean when I make flush, ORM execute INSERT INTO p_user (name, permission_id) VALUES ('name', null), but I want also to execute INSERT INTO p_user (name) VALUES ('name').
It's possible to do that I wanted.
To achieve this you can provide default values.
private $permission = 0;
private $name = '';

How to override the id's annotation in a Doctrine inheritnce hiearchy

I'm working with Symfony and MySQL and I'm trying to follow some convention across all my table, one of them is to keep each id's colmun name in the format id_tablename (see diagram). So i kept the id name generated by Symfony in the classes, but I want to replace each field in the database by id_product, id_tire, etc, ...
For that i'm using the Column annotation, e.g:
abstract class Product
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer", name="id_product")
*/
private $id;
// ...
}
And for each child class, I use AttributeOverride annotation as explained in the doc, like bellow
/**
* #ORM\Entity(repositoryClass=TireRepository::class)
* #ORM\AttributeOverrides(
* #ORM\AttributeOverride(
* name = "id",
* column=#ORM\Column(name="id_tire")
* )
* )
*/
class Tire extends Product
{
// ...
}
But when attempting a php bin/console make:migration I got the error The column type of attribute 'id' on class 'App\Entity\Tire' could not be changed.
Did I miss something ?
Edit: I tried to override another attribute ($name) with the following code that work:
/**
* #ORM\Entity(repositoryClass=RimRepository::class)
* #ORM\AttributeOverrides(
* #ORM\AttributeOverride(
* name = "name",
* column=#ORM\Column(name="name_rim")
* )
* )
*/
class Rim extends Product
{
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
// ...
}
But even by doing the same thing with $id attribute, I still have the same error message.
Seem like Doctrine have difficult to work with renamed fields too, when you have relations betweens classes. So for now I keep the default id name for each table in database, to continue working.
Please check correct example below.
Looks like you just missing type="integer" in AttributeOverride
use Doctrine\ORM\Mapping as ORM;
/**
* #MappedSuperclass
*/
abstract class Product
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer", name="id_product")
*/
protected $id;
/**
* #ORM\Column(type="string", length=255)
*/
protected $name;
// ...
}
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\AttributeOverrides(
* #ORM\AttributeOverride(
* name = "id",
* column=#ORM\Column(name="id_tire", type="integer")
* )
* )
*/
class Tire extends Product
{
// ...
}
As result migration SQL will be similar to following
$this->addSql('CREATE TABLE tire (id_tire INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id_tire))');
It seem like it's a problem with how Doctrine works. As my system require many relations between entities, I didn't noticed it, but without relations, evrything works fine if they are correctly mapped. For exemple with:
Parent class
/**
* #ORM\Entity(repositoryClass=ProductRepository::class)
*/
class Product
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer", name="id_product")
*/
private $id;
// ...
}
Child classes
/**
* #ORM\Entity(repositoryClass=RimRepository::class)
* #ORM\AttributeOverrides(
* #ORM\AttributeOverride(
* name = "id",
* column=#ORM\Column(type="integer", name="id_rim")
* )
* )
*/
class Rim extends Product
{
/**
* #ORM\Column(type="integer")
*/
private $id;
// ...
}
/**
* #ORM\Entity(repositoryClass=TIreRepository::class)
* #ORM\AttributeOverrides(
* #ORM\AttributeOverride(
* name = "id",
* column=#ORM\Column(type="integer", name="id_tire")
* )
* )
*/
class Tire extends Product
{
/**
* #ORM\Column(type="integer")
*/
private $id;
// ...
}
Will generate those tables in database. But with relations at any level of the hiearchy like in this case, Doctrine will fail to retrive renamed column with annotations. Si in my case I had to keep the default id name across all tables in order to let Doctrine find what he excpect when making relations between tables.
I tried to remove all relation from child class and keep those from parent class, also the opposite, but Doctrine alwas still searching column id while looking for relation/contraints:
$this->addSql('ALTER TABLE picture ADD CONSTRAINT FK_16DB4F894584665A FOREIGN KEY (product_id) REFERENCES product (id)');
It's seem like impossible to do right now, with complex database structure.

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).

Entity containing other entities without having a table

I have an Entity ( Invoice ) which is purely for calculation purposes and has no table, that associates with two other entities having relations by tables. (Although there are so many other entities involved ).
class Row{
/**
* #var integer
*
* #ORM\Column(name="row_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="File")
* #ORM\JoinColumn(name="file_id", referencedColumnName="file_id")
*/
protected $file;
/**
* #var \DateTime
*
* #ORM\Column(name="date", type="date")
*/
private $date;
}
class File
{
/**
* #var integer
*
* #ORM\Column(name="file_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
}
class Invoice
{
/**
* #ORM\Id
* #ORM\Column(name="invoice_id", type="integer")
* #ORM\GeneratedValue
*/
protected $id = null;
/**
* #ORM\OneToMany(targetEntity="Row", mappedBy="row_id")
*/
protected $row;
/**
* #ORM\OneToMany(targetEntity="File", mappedBy="file_id")
*/
protected $file;
}
I want to be able to query for Invoices :
$sDate = //Some date
$this->getEntityManager()
->createQuery("SELECT Invoice, Row, File
FROM
ReportsEntitiesBundle:Invoice Invoice
LEFT JOIN
Row.row Row
LEFT JOIN
Row.file File
WHERE date=:date"
)
->setParaMeter(':date', $sDate)
->setFirstResult($iPage*$iLimit)
->setMaxResults($iLimit)
->getResult();
The questions :
# Doctrine tries to query the database, how can I prevent that and have it find the relevant entities?
# How can I relate the date ( which is in Row entity and cannot be in Invoice ) to the query?
Later this Invoice will become a part of another big entity for calculating/search purposes.
Thank you
Short Answer: You can't
Long Answer : You can't because an entity with #ORM annotations means its persisted to a database - querying that entity relates to querying a database table. Why not just create the table ?!?!?
You need somewhere to persist the association between file and row - a database table is a perfect place !!!!
Update
Just to clarify ... an Entity is just a standard class - it has properties and methods ... just like any other class - When you issue doctrine based commands it uses the annotations within the entities to configure the tables / columns / relationships etc if remove those you can use it however you like ... but you will need to populate the values to use it and you wont be able to use it in a Doctrine query and it obviously wont be persisted !
You can use a read-only entity. It's contents are backed by a view which you create manually in SQL.
PHP:
/** #ORM\Entity(readOnly =true) */
class InvoiceView
{ ...
SQL:
CREATE VIEW invoice_view AS (
SELECT ...

Use foreign keys as where argument in Symfony/Doctrine

I'm pretty new to symfony/Doctrine and having some problems with querybuilder:
Given this ER:
And following declaration:
namespace xxx\SeoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* xxx\SeoBundle\Entity\Session
*
* #ORM\Table(name="session")
* #ORM\Entity
*/
class Session
{
const repositoryName = "InternetSmSeoBundle:Session";
/**
* #var string $id
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
*/
private $id;
......
/**
* #var Gsite
*
* #ORM\ManyToOne(targetEntity="Gsite")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="gsite_id", referencedColumnName="id")
* })
*/
private $site;
......
}
I need to find sessions which filtering them by site.
I've tried following approach:
$rep = $this->em->getRepository(Session::repositoryName);
$qb = $rep->createQueryBuilder("s");
$qb->setMaxResults(200);
$qb->where("1=1");
$qb->orderBy("time", "desc");
//site
if ($params->site != null){
/** #var Gsite **/
$site = $params->site;
$qb->where($qb->expr()->eq("gsite_id",$site->getId()));
}
Or even
$qb->where($qb->expr()->eq("site",$site));
But it doesn't work. What is correct way to filter data in presence of Many To One foreign keys? Do I need to create declaration of gsite_id column in my Model?
Thanks.
Set the parameter, Doctrine will be able to infer the type (no need to use the foreign key id):
$qb
->where($qb->expr()->eq('site', ':site'))
->setParameter('site', $site);
;

Resources