I have to simple Entity: Log and User.
Log has a ManyToOne relationship with Entity.
Log:
type: entity
repositoryClass: LogRepository
id:
id:
type: integer
generator:
strategy: AUTO
fields:
message:
type: string
manyToOne:
user:
targetEntity: User
joinColumns:
user_id:
referencedColumnName: id
My use case is to show the list of the logs and one or two information about the user (like his name and his mail for example)
If I use the findall method, Symfony debug toolbar shows me that Doctrine performs a lot of queries. One query gives me the logs and one query is performed for each user! It is not good of course because I can have thousand logs in my view. I don't want to overload my database server. This problem seems very simple to solve. But I'm searching for a while and the results seems to be "bad practices".
So I began by writing a new method in the LogRepository class using the querybuilder:
public function getLog(){
$qb = $this->createQueryBuilder('l')
->select('l')
->innerJoin(
'ApplicationSonataUserBundle:User', 'u',
Expr\Join::WITH,'l.user = u.id')
;
return $qb->getQuery()->getResult();
}
I still had the same problem. I have changed the select parameters on my method to :
public function getLog(){
$qb = $this->createQueryBuilder('l')
->select('l','u')
->innerJoin('ApplicationSonataUserBundle:User','u',
Expr\Join::WITH,'l.user = u.id')
;
return $qb->getQuery()->getResult();
}
Eureka? OK, I only have one query but my method didn't return only Log, BUT User too... So my Twig template crashes because my loop contains User, not only Log. When this is a User, my view crash because I want to write message fields. (Log.message exists. But User.message is not a valid field, of course)
It works pretty good, if I change one more time my method with a loop to filter my results :
public function getLog(){
$qb = $this->createQueryBuilder('l')
->select('l','u')
->innerJoin('ApplicationSonataUserBundle:User','u',
Expr\Join::WITH,'l.user = u.id')
;
//THE STRANGE LOOP
$results = array();
foreach ($qb->getQuery()->getResult() as $result){
if ($result instanceof Log){
$results[] = $result;
}
};
return $results;
}
I have only one query, it is what I'm searching for. My twig template doesn't crash, because my array contains only Log.
So what's the matter? It works, but I think this is not the good/best practices.
Someone can explain me a better way, a better practice to use an inner join query, to minimize the performed query and have an ArrayCollection result which contains only instance of Log?
It should not be necessary to use the loop. Try like this:
public function getLog(){
$qb = $this->createQueryBuilder('l')
->select('l','u')
->innerJoin('l.user', 'u');
$logs = $qb->getQuery()->getResult();
return $logs;
}
It should only return $logs with a populated (fetch joined) association user.
Related
In my Symfony project I have a User entity with the following properties to define followers relation
#[ORM\ManyToMany(targetEntity: self::class, inversedBy: 'followers')]
#[ORM\JoinTable(name: "follows")]
#[ORM\JoinColumn(name: "follower_id", referencedColumnName: "id")]
#[ORM\InverseJoinColumn(name: "followed_id", referencedColumnName: "id")]
private $followedUsers;
#[ORM\ManyToMany(targetEntity: self::class, mappedBy: 'followedUsers')]
private $followers;
I am attempting to get the paginated list of a User's followers with the following query in my UserRepository
public function getPaginatedFollowersByUser(User $user, int $offset, int $limit): Paginator
{
$qb = $this->createQueryBuilder('u')
->select('u')
->innerJoin('u.followers', 'f')
->andWhere('f.id = :userId')
->setParameter('userId', $user->getId())
->setFirstResult($offset)
->setMaxResults($limit);
$paginator = new Paginator($qb, true);
return $paginator;
}
where Paginator is an instance of Doctrine\ORM\Tools\Pagination\Paginator.
This works fine and now I want to know how many items are there in the result. In the DB there is only 1 follower defined for the user I am querying, yet $paginator->count() and count($paginator) both return the value 2. When I iterate the paginator I find only 1 result, as expected.
I am not sure what I am missing or doing wrong. Is the counting of the result done in a different way?
Thank you!
NOTE: The workaround I've found so far is to use
$count = count($paginatedUsers->getIterator()->getArrayCopy());
instead of
$count = count($paginatedUsers);
It is not very elegant but it does output the expected 1.
I am embarrassed to say that the paginator was working perfectly fine and it was my mistake that this failed.
The DB in fact had 2 entries corresponding to this query, meaning that the result given by the paginator was in fact correct. The $offset parameter in my query was 1 instead of the 0 that I expected, which made the first element be ignored and therefore my iterator only found 1 result but there were 2 results for the query.
I have the following query that should check the start and end date in another entity and compare it against the start and end dates enter to create an instance of an entity, however its not returning anything.
public function createAction(Request $request)
{
$entity = new Payrollperiod();
$form = $this->createCreateForm($entity);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$qb = $em->getRepository('comtwclagripayrollBundle:PayrollWeek')->createQueryBuilder('p');
$qb->select('p')
->where('p.startDate = :entityStart')
->andWhere('p.endDate = :entityEnd')
->setParameter('entityStart',$entity->getstartDate())
->setParameter('entityEnd',$entity->getendDate())
->getQuery()
->getResult();
How long I've been using Doctrine, I've never seen:
In MySQL (and PostgreSQL, SQLite) equal symbol is =, not ==.
In DQL you can't use entities' methods like getStartDate(), only attributes (in detail you can use only fields and associations defined in entity's mapping).
So:
$qb->select('p')
->where('p.getstartDate()==entity.startDate')
->andWhere('p.getendDate()==entity.endDate');
Should be:
$qb->select('p')
->where('p.startDate = entity.startDate')
->andWhere('p.getEndDate = entity.endDate')
What is entity.*? You have not declared entity Entity in your DQL.
[EDIT] If entity.* is another entity and you not define its in Query, then you must parameterize it:
$qb->select('p')
->where('p.startDate = :entityStart')
->andWhere('p.getEndDate = :entityEnd')
->setParameter('entityStart', $entity->getStartDate())
->setParameter('entityEnd', $entity->getEndDate())
Without exception message I cannot tell more about your code. It could be NonUniqueResultException because query finds more than 1 result.
Doctrine (and Symfony) have so many exceptions where everything is nice explain - what's crashed, why, where. Sometimes even the name of exception tell us everything - like UniqueConstraintViolationException.
I'm doing a join between two tables using the doctrine that comes bundled in the current symfony release. This is my controller code:
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Acme\GearDBBundle\Entity\TbGear;
use Acme\GearDBBundle\Entity\TbDships;
class DefaultController extends Controller
{
public function indexAction()
{
$repository = $this->getDoctrine()
->getRepository('AcmeGearDBBundle:TbGear');
$query = $repository->createQueryBuilder('p')
->select('p', 'q')
->innerJoin('p.fkShip', 'q', 'WITH', 'p.fkShip = q.id')
->getQuery();
$result = $query->getResult();
foreach ( $result as $p ) {
$gear[] = array('shortname' => $p->getGearShortName(), 'name' => $p->getGearName(), 'shipname' => $p->getShipName /* Does not work, since the getter is in a different entity */);
}
return $this->render('AcmeGearDBBundle::index.html.twig', array('gear' => $gear));
}
}
The query generated by this is correct and delivers the expected fields if I execute it in phpmyadmin.
SELECT t0_.GEAR_NAME AS GEAR_NAME0, t0_.GEAR_SHORT_NAME AS GEAR_SHORT_NAME1, t0_.STATUS AS STATUS2, t0_.ID AS ID3, t1_.SHIP_NAME AS SHIP_NAME4, t1_.CONTACT_NAME AS CONTACT_NAME5, t1_.CONTACT_EMAIL AS CONTACT_EMAIL6, t1_.ID AS ID7, t0_.FK_SHIP_ID AS FK_SHIP_ID8, t0_.FK_TYPE AS FK_TYPE9
FROM tb_gear t0_
INNER JOIN tb_dships t1_ ON t0_.FK_SHIP_ID = t1_.ID
AND (t0_.FK_SHIP_ID = t1_.ID)
However, I have no clue how do access those fields in the returned result set. The way I expected it to work ( by accessing the getter of the joined table entity ) does not work. The error message reads: FatalErrorException: Error: Call to undefined method Acme\GearDBBundle\Entity\TbGear::getShipName() in /var/www/symfony/src/Acme/GearDBBundle/Controller/DefaultController.php line 24
which makes sense since the TbGear entity doesn't have a getter method called getShipName() , since that's a method from the joined entity. But how do I access those values? This probably is a stupid question, but I just can't figure it out. Any help is appreciated.
$p->getFkShip()->getShipName() maybe?
This should work since it will retrieve only TbGear that satisfies you relationship. So you could be able to access to all FkShip (I suppose that is a many-to-one relation) that should be only one, and then .... you got it!
EDIT
Of course I suppose that you have correctly designed your class so that you have a getter from TbGear to access the relation with FkShip
Can you add that custom getter: getShipName()?
public function getShipName(){
if ( $this->ship != null ){
return $this->ship->getName();
}
return null; // or an empty string
}
I'm working with Entity objects from Doctrine queries and i end up with a very big array, with all information from all entities related. This ends up being a huge data tree... how can i limit this? Avoid listing all data from all relationships?
You can always remove not needed associations (this is a best practice for speeding up Doctrine). Or you can select only fields that you need in your presentation layer (as read-only data):
public function getAll()
{
$qb = $this->createQueryBuilder('u'); // Where are in User custom repository
return $qb
->select(array('u.id', 'u.first', 'u.last'))
->getQuery()
->getResult();
}
If you still need to work with objects (or for complex queries that needs plain SQL) a possibility is filling only needed properties (and eventually, associations/nested collections) of your domain object.
An example, more on native SQL:
public function getAll()
{
$mapping = new \Doctrine\ORM\Query\ResultSetMapping();
$mapping->addEntityResult('Acme\HelloBundle\User', 'e');
$mapping->addFieldResult('e', 'id', 'id');
$mapping->addFieldResult('e', 'first', 'first');
$mapping->addFieldResult('e', 'last', 'last');
$sql = "SELECT id, first, last FROM user ";
$result = $this->_em->createNativeQuery($sql, $mapping)->getResult();
// Or hust return $result itself (array)
return new \Doctrine\Common\Collections\ArrayCollection($result);
}
Of course the disadvance (?) is use of native SQL. I don't believe that ResultSetMapping can be used with DQL.
EDIT: take a look at http://docs.doctrine-project.org/projects/doctrine-orm/en/2.0.x/reference/best-practices.html
Designer:
tableName: designers
actAs:
I18n:
fields: [name]
columns:
id:
type: integer(2)
unsigned: true
primary: true
autoincrement: true
name:
type: string(30)
notnull: true
While default I18n behavior must be used like this
$d = Doctrine_Query::create()
->select('id, t.name')
->from('Designer d')
->leftJoin('d.Translation t')
->where("t.lang = 'en'")
->execute();
I would be faaar more convenient to set some constant to the current language, say en, and have every i18nable field correspond to it, thus having such query
$d = Doctrine_Query::create()
->select('id, name')
->from('Designer d')
->execute();
equivalent to the above one.
I'm trying to make a new behavior, extending the default one, which could provide such things, but I need your help.
Getting the needed language is easy, so let's just say there is define('LANGUAGE', 'en'). The basic behavior class is
class TransparentI18N extends Doctrine_Template
{
private $_translation = NULL;
public function setUp()
{
$this->addListener(new TransparentI18NListener());
$this->actAs(new Doctrine_Template_I18n($this->_options));
}
}
So the idea is to add a listener, that would modify the query to set joins and select needed fields whenever such fields are present in the select clause. The TransparentI18NListener contains the preDqlSelect, which receives the Doctrine_Event object. And I can get the relevant Doctrine_Query and even getDqlPart('select') works, but the latter returns the raw select string, like id, t.name, how can I get, to which table each clause corresponds?
Then I'd have to set fields of the Doctrine_Record instance. Is it possible without making models extend some custom class which would give such functionality? I'm really reluctant to add such a class, but in case everything other fails, I should be able to make it save these fields and override the __get method to show translated fields if they are requested.
I was too scared to think about the insert/update/delete part, but hopefully if I deal with the above problems, I'd be able to add hooks to dql and get the job done.
Do you think such a think is possible in general without messing with core doctrine libraries? Having to use it in the default way would be a huge pain in the *...
I don't have a good, non-hacky solution for your actual problem but have an alternative to try.
You could put all your find/get queries in the models corresponding table class.
So you still have to do the Translation-Joints but all you queries are at one place and are easy to maintain.
class DesignerTable extends Doctrine_Table
{
protected $_lang = LANGUAGE;
public function findAll()
{
return Doctrine_Query::create()
->select('id, t.name')
->from('Designer d')
->leftJoin('d.Translation t')
->where("t.lang = ?", $this->_lang)
->execute();
}
public function findOneById($id)
{
return Doctrine_Query::create()
->select('id, t.name')
->from('Designer d')
->leftJoin('d.Translation t')
->where('.lang = ?', $this->_lang)
->andWhere('id = ?', $id)
->execute();
}
...
}
// In your controllers:
// Find all designers
$designers = Doctrine_Core::getTable('Designer')->findAll();
// Find one designer by id
$designers = Doctrine_Core::getTable('Designer')->findOneById(13);