Implementing getters for joined doctrine 2 collection - php

I'm trying to create simple getter methods in original entity for specific item in doctrine collection of joined entity.
Main entity looks something like this:
class Product
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
...
/**
* #ORM\OneToMany(targetEntity="File", mappedBy="product")
*/
private $files;
}
And joined entity:
class PrankFile
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="files")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $product;
/**
* #var string
*
* #ORM\Column(name="type", type="string", length=16)
*/
private $type;
...
My DQL in repository class is very simple:
return $this->getEntityManager()
->createQuery('SELECT p, f FROM AppProductBundle:Product p INNER JOIN p.files f ORDER BY p.created DESC')
->setMaxResults($limit)
->getResult();
In files entity type field tells me what kind of a file is it (image, sound, video, demo etc...)
Problem comes when I wish to print out a list of all products and display image next to product details, I would hate to loop through product files for each product displayed.
Is it possible to create some simple getter on product entity to fetch file of certain type?
Or maybe it would be better to create more complex DQL query for this, again how to do this?
I can't just fetch image record from files in DQL because I need all files for certain products.
Any help with this would be most welcome.

You can filter directly on collections using the filter api. If the collection is not loaded already Doctrine will apply your filter on a SQL level, giving you max performance. In case the collection is already eager loaded Doctrine will filter the ArrayCollection in memory.
use Doctrine\Common\Collections\Criteria;
class Product
{
public function getFilesByType($type)
{
$criteria = Criteria::create()
->where(Criteria::expr()->eq("type", $type))
return $this->files->matching($criteria);
}
}

Related

Symfony2 ManyToMany in one query

I have 2 entities related ManyToMany
class Product
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=100, unique=true)
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="ProductTransfer", inversedBy="product")
* #ORM\JoinTable(name="products_transfers")
*/
private $transfers;
and second one
class ProductTransfer
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var int
*
* #ORM\Column(name="stock_from_id", type="integer")
*/
private $stockFromId;
/**
* #var int
*
* #ORM\Column(name="stock_to_id", type="integer")
*/
private $stockToId;
/**
* #var int
*
* #ORM\Column(name="product_id", type="integer")
*/
private $productId;
/**
* #ORM\ManyToMany(targetEntity="Product",mappedBy="transfers")
*/
private $product;
All works great, additional table were created by Doctrine.
But when I'm trying to get all rows from ProductTransfers I see in profiler that each row need one single query with 2 joins.
So for 5000 products it will be really many queries.
Is there any way to get them all in one query like in "clean SQL"?
If there is no way to do it with Doctrine, what is best way to achieve that?
Like:
SELECT * FROM product_transfer pt
LEFT JOIN products_transfers pts ON pt.product_id=pts.product_id
LEFT JOIN product p ON pts.product_id=p.id;
edit:
created method in repository
public function loadTransfersByStock($stockId)
{
$q = $this->getEntityManager()
->createQuery(
'SELECT pt,p FROM AppBundle:ProductTransfer pt
LEFT JOIN AppBundle:Product p WITH pt.productId=p.id
WHERE pt.stockToId = :stockId'
);
return $q->setParameter('stockId', $stockId)->getResult();
}
and then result is
ProductsController.php on line 495:
array:3 [▼
0 => ProductTransfer {#1071 ▶}
1 => Product {#1084 ▶}
2 => ProductTransfer {#1099 ▶}
]
By default Doctrine uses lazy loading in object relations - the rows for the relationships are only loaded when you try to access them. If you know you're going to be needing all the related rows, try changing to eager fetching.
/**
* #ORM\ManyToMany(targetEntity="ProductTransfer", inversedBy="product", fetch="EAGER")
* #ORM\JoinTable(name="products_transfers")
*/
When you are writing the query like that in the repository, you are skipping the way doctrine "wants" you to write it. So instead of that, I would do something like this:
//ProductTransferRepository.php
$qb = $this->createQueryBuilder('pt');
$qb
->select('pt, p') // here you can choose the columns you want if you don't need everything from both tables
->leftJoin('pt.product', 'p')
->where('pt.stockToId = :stockId')
->setParameter('stockId', $stockId)
;
return $qb
->getQuery()
->getArrayResult()
;
Use getArrayResult to fetch results as array rather than objects, especially because you have many queries.

How to establish one-to-one relation through IDs between doctrine entities

I was hoping this be a straight forward process but it seems Doctrine doesn't really like the idea of linking entities through their IDs.
All I intended to do was normalising a table by shipping some fields from it to a new table and instead of adding a new reference field to the original table to hold the ID of the new corresponding record in the, make sure the new record in the child table will have identical ID to its parent row.
Here is an example of what I have:
A User entity, with annotated field $user to reference column ID in the UserDetail entity to itself's ID
/**
* #ORM\Table(name="user", options={"collate"="utf8_general_ci", "charset"="utf8", "engine"="InnoDB"})
* #ORM\Entity
*/
class User extends Entity
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id
/**
* #ORM\OneToOne(targetEntity="UserDetail", cascade={"persist"})
* #ORM\JoinColumn(name="id", referencedColumnName="id", nullable=true)
*/
private $userDetail;
...
}
and here is the UserDetail with its ID's #GeneratedValue removed
/**
* #ORM\Table(name="user_detail", options={"collate"="utf8_general_ci", "charset"="utf8", "engine"="InnoDB"})
* #ORM\Entity
*/
class UserDetail extends Entity
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
*/
private $id;
...
}
At this point what my expectation was to be able to do something like:
$user = new User();
$userDetail = new UserDetail();
$user->setUserDetail($userDetail)
$entityManager->persist($user);
$entityManager->flush();
And get two records persisted to the user and user_detail tables with identical IDs, but the reality is, not having any strategy defined for the UserDetail's identifier, doctrine will complaint about the missing ID, Entity of type UserDetail is missing an assigned ID for field 'id'.
Of course it is possible to do the job manually and in more than one call
$user = new User();
$entityManager->persist($user);
$entityManager->flush();
$userDetail = new UserDetail();
$userDetail->setId($user->getId)
$user->setUserDetail($userDetail)
$entityManager->persist($user);
$entityManager->flush();
But I'm still hoping there is a correct configuration (annotation) that can help me to avoid such extra steps and leave handling of a one-to-one relationship through the entity's IDs to Doctrine.
This is untested but I think the following might work, according to the docs (http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/composite-primary-keys.html):
/**
* #ORM\Table(name="user_detail", options={"collate"="utf8_general_ci", "charset"="utf8", "engine"="InnoDB"})
* #ORM\Entity
*/
class UserDetail extends Entity
{
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="User")
* #ORM\JoinColumn(name="id", referencedColumnName="id")
* #ORM\Id
*/
private $user;
...
}

Symfony2 duplicate the id in 2 fields (id and idbis) with strategy="AUTO"

I would like symfony/doctrine to duplicate the id in two different field every time a record is created.
Is it possible in one shot (I use Stragegy="AUTO") ?
If yes how can I do this?
For instence I would like my entity CATEGORY to have two attribut id (auto).
on named id, the other idbis.
(In my exemple I work on a entity which contains parent and children records, so I also have idparent to link the children categories with their parent category)
if the record is a parent Category, then id and idbis get the same integer:
=> id=2, idbis=2, idParent = NULL
if the record is a child Category (let say its parent is Category with id=2) then:
=> id=3, idParent=2, idCategory1=2
That would be great because then I could then easily retrieve all categories (parent and children) who are linked to category which id is 2.
You should take a look to Nested Set structures. It's made to easily retrieve sub elements in a simple query by using bounds.
Otherwise you can do that by creating a trigger on insert/update.
You can use a relation with the entity itself:
<?php
/** #Entity **/
class Category
{
// ...
/**
* #OneToMany(targetEntity="Category", mappedBy="parent")
**/
private $children;
/**
* #ManyToOne(targetEntity="Category", inversedBy="children")
**/
private $parent;
// ...
}
This is just a complete version of the good idea presented by #Markus. Basically parent_id is null for a main category, otherwise has as value the id of parent.
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\OneToMany(targetEntity="path\to\Entity\Categories", mappedBy="parent", cascade={"persist"})
*/
private $children;
/**
* #var path\to\Entity\Categories
*
* #ORM\ManyToOne(targetEntity="path\to\Entity\Categories", inversedBy="children")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
* })
*/
private $parent;

adding created timestamp to join table in doctrine2

I have the following property in my User entity to track followers and following. Basically a user can follow other user as well. I have a join column called app_user_follow_user, however I also wanted to add a timestamp of whenever someone follows another user, when did it happen. How can I specify a created timestamp via this ORM?
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="following")
*/
protected $followers;
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="followers")
* #ORM\JoinTable(name="app_user_follow_user",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="follow_user_id", referencedColumnName="id")}
* )
*/
protected $following;
Doctrine ManyToMany relationships are used when your join table has two columns. If you need to add another column you have to convert the relationship to OneToMany on both sides and ManyToOne on the joined entity.
This is entirely untested but it will hopefully give you the gist.
User Entity
/**
* #ORM\OneToMany(targetEntity="AppUserFollowUser", mappedBy="appUser")
*/
protected $followers;
/**
* #ORM\OneToMany(targetEntity="AppUserFollowUser", mappedBy="followUser")
*/
protected $following;
AppUserFollowUser Entity
/**
* #ORM\Table(name = "app_user_follow_user")
*/
class AppUserFollowUser
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="User", inversedBy="followers")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* })
*/
private $appUser;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="User", inversedBy="following")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="follow_user_id", referencedColumnName="id")
* })
*/
private $followUser;
/**
* #ORM\Column(name="created_date", type="datetime", nullable=false)
*/
private $createdDate;
}
I think that you will have to create a link entity manually (entiy1 onetomany linkEntity manytoone entity2.
Because, the usual link entity are automated and should be as simple and (data less) as possible, so doctrine can take all the controle over it,
imagine you need to get the timestamp, how can you do it on an (none hard coded) entity, you will need a getter, and the annotations are not supposed to contains code.

doctrine query builder join adding table twice

UPDATED at bottom:
I am trying to do what should be a simple join between two tables. I have a Gig table and a Venue table for a simple band site that I am building using Symfony2 (2.2). It's my first time with Symfony2 and doctrine so it is possible I am going completely in the wrong direction. I have created and populated the tables with DataFixtures and have verified that the ID relationships are correct. The problem I am getting is that the resulting DQL query has the Gig table referenced twice in the FROM section and that is causing me to get back several instances of the same record instead of the x number of records I am expecting. I don't know what I am doing wrong for that to be happening. Also, there may be an easier way of doing this but I am exploring all of my options since I am teaching myself Symfony2 in the process of building the site.
The Gig table contains a venue_id pointing to a Venue table that is defined in the Gig entity as a ManyToOne relationship (shown below). Using a doctrine findAll everything seems to work fine with the Venue class in the Gig Entity being populated correctly. I am trying to create a flat view of a few of the most recent Gigs to be displayed on the front page so I figured I would try to use a Join and include only the fields I need.
Here is the Repository Query:
public function getGigsWithLimit($maxGigs)
{
$qb = $this->createQueryBuilder('b')
->select('
g.gigDate,
g.startTime,
g.endTime,
g.message1 as gig_message1,
g.message2 as gig_message2,
g.url,
v.name,
v.address1,
v.address2,
v.city,
v.state,
v.zip,
v.phone,
v.url as venue_url,
v.message1 as venue_message1,
v.message2 as venue_message2,
v.message3 as venue_message3'
)
->from('WieldingBassBundle:Gig', 'g')
->leftJoin('g.venue', 'v')
->orderBy('g.gigDate', 'DESC')
->setMaxResults($maxGigs);
return $qb->getQuery()->getResult();
}
Here is the DQL it creates:
SELECT
g0_.id AS id0,
g0_.gig_date AS gig_date1,
g0_.start_time AS start_time2,
g0_.end_time AS end_time3,
g0_.message1 AS message14,
g0_.message2 AS message25,
g0_.url AS url6,
v1_.name AS name7,
v1_.address1 AS address18,
v1_.address2 AS address29,
v1_.city AS city10,
v1_.state AS state11,
v1_.zip AS zip12,
v1_.phone AS phone13,
v1_.url AS url14,
v1_.message1 AS message115,
v1_.message2 AS message216,
v1_.message3 AS message317
FROM
Gig g2_,
Gig g0_
LEFT JOIN
Venue v1_ ON g0_.venue_id = v1_.id
LIMIT
6
The Gig g2_ is my problem. If I delete it and execute the query everything is as expected. I don't know what is generating that.
The first table Gigs Entity looks like this (I am leaving out the getters and setters):
/**
* Gig
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="Wielding\BassBundle\Entity\GigRepository")
* #ORM\HasLifecycleCallbacks
*/
class Gig
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \DateTime
*
* #ORM\Column(name="gig_date", type="date")
*/
private $gigDate;
/**
* #var \DateTime
*
* #ORM\Column(name="start_time", type="datetime")
*/
private $startTime;
/**
* #var \DateTime
*
* #ORM\Column(name="end_time", type="datetime")
*/
private $endTime;
/**
* #var string
*
* #ORM\Column(name="message1", type="string", length=50, nullable=true)
*/
private $message1;
/**
* #var string
*
* #ORM\Column(name="message2", type="string", length=50, nullable=true)
*/
private $message2;
/**
* #var string
*
* #ORM\Column(name="url", type="string", length=128, nullable=true)
*/
private $url;
/**
* #var integer
*
* #ORM\Column(name="venue_id", type="integer")
*/
private $venueId;
/**
* #ORM\Column(type="datetime")
*/
protected $created;
/**
* #ORM\Column(type="datetime")
*/
protected $updated;
/**
* #ORM\ManyToOne(targetEntity="Venue", cascade="persist")
* #ORM\JoinColumn(name="venue_id", referencedColumnName="id")
*/
protected $venue;
The Venue table is simple and does not have any relationships defined so I will leave it out unless it is asked for.
Any ideas? Thanks for any help.
Andrew
I removed everything except what would recreate the problem and here is what I was left with:
I simplified the repository method to:
public function getGigsWithLimit2($maxGigs)
{
$qb = $this->createQueryBuilder('a')
->select('g.id')
->from('WieldingBassBundle:Gig', 'g')
->setMaxResults($maxGigs);
return $qb->getQuery()->getResult();
}
This now generates:
SELECT
g0_.id AS id0
FROM
Gig g1_,
Gig g0_
LIMIT
6
There is that darn Gig g1_ problem again. I got the "Explain Query" from the Symfony profiler and it shows:
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE g1_ index IDX_ED7D664240A73EBA 4 9 Using index
1 SIMPLE g0_ index IDX_ED7D664240A73EBA 4 9 Using index; Using join buffer
I don't pretend to know what that means but it shows both table entries with different information about how it was used.
whats the use of "venueId" if you already got "venue" which contains the foreign key?
I found the problem. I am not used to Doctrine and was using the ->from in a repository that did not need it since the entity was automatically related through the annotations. My earlier query that worked was in the controller and not a repository so the ->from was necessary.
You are trying to do SQL. Doctrine is different. Your query fetches every field. Doctrine prefers to fetch entities. I think you probably want this:
public function getGigsWithLimit($maxGigs)
{
$qb = $this->createQueryBuilder('g')
->leftJoin('g.venue', 'v')
->orderBy('g.gigDate', 'DESC')
->setMaxResults($maxGigs);
return $qb->getQuery()->getResult();
}
The return result is a list of entities which you can call all the specific methods directly. You are still welcome to specify fields with doctrine, if you want partial objects, but I've found the normal method of fetching the entire entity covers most of my needs.
This is a fundamentally different paradigm than SQL and will take some getting used to.

Categories