Doctrine2 :one-to-many relation; - php

i have 2 entity with one-to-many relation : Article & ArticleCategory
class Article {
/**
* #var integer
*
* #ORM\Column(name="rate", type="integer",options={"default" : 0})
*/
private $rate = 0;
/**
* #var \ArticleCategory
*
* #ORM\ManyToOne(targetEntity="ArticleCategory",inversedBy="articles")
* #ORM\JoinColumn(name="article_category_id", referencedColumnName="id")
*/
private $category;
}
class ArticleCategory {
/**
*
* #var \Article
* #ORM\OneToMany(targetEntity="Article", mappedBy="category")
*/
protected $articles;
}
now i want to fetch categories which has much articles with top rates..
" i mean top N categories ordered by highest rated article in them (categories which has more articles with rate above the average rate)"
how can i do this?

finally i found this for my question, maybe it can be useful for others! :)
$query = $em->createQuery('SELECT c, avg(a.rate) x
FROM YoutabsGeneralModelBundle:ArticleCategory c
JOIN c.articles a
GROUP BY c.id
ORDER BY x DESC');
i add ORDER BY because i wanted to set limit on this :
$query->setMaxResults($limit);

Related

Where IN query on Doctrine ManyToMany tags relation

A little bit of background:
I've got Office entities.
Offices may have articles.
Articles have tags.
Offices can share a tag with other offices (and therefore articles).
Now I'm trying to build a query that says: "Fetch all articles that either belong to my office or have tags that have been shared with my office".
I've tried this in MySQL and the following query works as expected:
SELECT
*
FROM
`articles` `a`
WHERE
(
`office_id` = 2
OR
`a`.`id` IN (
SELECT
`at`.`article_id`
FROM
`article_tags` `at`
WHERE
`at`.`article_id` = `a`.`id`
AND
`at`.`tag_id` IN
(
SELECT
`st`.`tag_id`
FROM
`shared_tags` `st`
WHERE
`st`.`shared_with_id` = 2
AND
`st`.`article` = 1
)
)
)
AND
`status` = 1
My entities are as follows:
Article
/**
* #ORM\ManyToMany(targetEntity="Tag", inversedBy="articles")
* #ORM\JoinTable(name="article_tags")
* #var ArrayCollection|Tag[]
*/
protected $tags;
Tag
/**
* #ORM\ManyToMany(targetEntity="Article", mappedBy="tags")
* #var ArrayCollection|Article[]
*/
protected $articles;
SharedTag
class SharedTag extends Entity
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #var int
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Office", inversedBy="sharedTags")
* #ORM\JoinColumn(name="shared_with_id", referencedColumnName="id")
* #var Office
*/
protected $sharedWith;
/**
* #ORM\ManyToOne(targetEntity="Office", inversedBy="sharedTags")
* #ORM\JoinColumn(name="shared_by_id", referencedColumnName="id")
* #var Office
*/
protected $sharedBy;
/**
* #ORM\ManyToOne(targetEntity="Tag", inversedBy="sharedTags")
* #var Tag
*/
protected $tag;
/**
* #ORM\Column(type="boolean", options={"default" = 0})
* #var bool
*/
protected $template = 0;
/**
* #ORM\Column(type="boolean", options={"default" = 0})
* #var bool
*/
protected $article = 0;
/**
* #ORM\Embedded(class = "\ValueObjects\CreatedAt", columnPrefix=false)
* #var CreatedAt
*/
protected $createdAt;
}
So, how can I query this in DQL or with the QueryBuilder? I've tried several methods but I can't seem to use the tag_id from the relationship articles.tags in a WHERE IN() query.
Thank you in advance!
Edit:
After some trial and error and thanks to Jean's answer I was able to query it like this:
SELECT
a
FROM
Article a
LEFT JOIN a.tags t
LEFT JOIN a.office o
LEFT JOIN o.sharedTags st
WHERE
(
a.office = :office
OR
(
t = st.tag
AND
st.sharedWith = :office
AND
st.article = 1
)
)
AND
a.status.status = :status
AND
a.template IS NULL
GROUP BY a.id
You should use a join and not subqueries. This way you have better performance and less complicated DQL code:
SELECT DISTINCT `a`
FROM `articles` `a`
LEFT JOIN `a.tags` `t`
LEFT JOIN `t.shared_with_id` `o`
WHERE (`a`.`office_id` = 2 OR `o`.`id` = 2)
AND `status` = 1

Where-ing in discriminated tables

How can I select all items from one specific author ? Its possible this way ? Or how can I edit entities if I want many item types and item packages (item has many items) too ?
Item
/**
* #ORM\Table()
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({
* "cd" = "ItemCD",
* "dvd" = "ItemDVD",
* "pack" = "ItemPack",
* })
*/
class Item
{
/**
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=250, nullable=false)
*/
private $name;
}
ItemCD
/**
* #ORM\Table()
* #ORM\Entity
*/
class ItemCD extends Item
{
/**
* #ORM\ManyToOne(targetEntity="Author", inversedBy="item")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
private $author;
}
ItemDVD
/**
* #ORM\Table()
* #ORM\Entity
*/
class ItemDVD extends Item
{
/**
* #ORM\ManyToOne(targetEntity="Author", inversedBy="item")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
private $author;
}
ItemPack
/**
* #ORM\Table()
* #ORM\Entity
*/
class ItemPack extends Item
{
/**
* #ORM\ManyToMany(targetEntity="Item", inversedBy="item")
* #ORM\JoinTable()
*/
private $items;
}
Author
/**
* #ORM\Table()
* #ORM\Entity
*/
class Author
{
/**
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=250, nullable=false)
*/
private $name;
}
You will have to query for specific elements. This is a known (and wanted) limitation, since DQL is a static typed language: see http://www.doctrine-project.org/jira/browse/DDC-16
Related: how to access fields in inherited table in doctrine2 / dql query
A way of handling this with a workaround is using 2 subqueries in your DQL:
SELECT
i
FROM
Item i
WHERE
i.id IN(
SELECT
i2.id
FROM
ItemDvd i2
WHERE
i2.author = :author
)
OR
i.id IN(
SELECT
i3.id
FROM
ItemCd i3
WHERE
i3.author = :author
)
As you can see you have to extract the identifiers for each possible subtype manually.
Edit: to get all the packs from a given author (along with single DVDs or CDs), the query becomes even worse:
SELECT
i
FROM
Item i
WHERE
i.id IN(
SELECT
i2.id
FROM
ItemDvd i2
WHERE
i2.author = :author
)
OR
i.id IN(
SELECT
i3.id
FROM
ItemCd i3
WHERE
i3.author = :author
)
OR
i.id IN(
SELECT
i4.id
FROM
ItemPack i4
JOIN
i4.items i5
WHERE
i5.id IN (
SELECT
i6.id
FROM
Item i6
WHERE
i6.id IN(
SELECT
i7.id
FROM
ItemDvd i7
WHERE
i7.author = :author
)
OR
i6.id IN(
SELECT
i8.id
FROM
ItemCd i8
WHERE
i8.author = :author
)
)
)
Make $author in Item and have ItemPacks $author value always be null. Then you can do:
$em->findBy("Item", array("author" => $author));
And you always get instances of ItemDVD or ItemCD.
It's tricky and lengthy the answer.
I think the Entities aproach is ok, and by querying the item entity you would get what you want.
Now for forms you'll probably need one FormType per sub-item and then user the aproach for Form Collections (http://symfony.com/doc/2.1/cookbook/form/form_collections.html) and I'm certain that you will need to hook into the pre-bind event to prepare the data.
This is a quick thought, may be it can help you.

How to prevent redundant lookups when joining entity relationships?

I'm getting up to speed with Symfony 2 and Doctrine and having difficulties with the number of unnecessary database lookups being performed to hydrate with joined entities.
After performing a joined query with a child object, the child is automatically pulling its other mappings from the database. It's doing this despite that I'm not attempting to access any of its properties. It's as if they're being accessed inside the find query.
My example looks like the below - There are entities called Person and Building that both join an entity called Place:
class Person {
/**
* Where this person lives
* #var Place $Home
*
* #ORM\OneToOne(targetEntity="Place", cascade={"persist"}, inversedBy="Resident" )
* #ORM\JoinColumn(name="place_id", referencedColumnName="id")
*/
private $Home;
}
class Building {
/**
* Where this building stands
* #var Place $Site
*
* #ORM\OneToOne(targetEntity="Place", cascade={"persist"}, inversedBy="Landmark" )
* #ORM\JoinColumn(name="place_id", referencedColumnName="id")
*/
private $Site;
}
class Place {
/**
* Reverse mapping
* #var Person $Resident
*
* #ORM\OneToOne(targetEntity="Person", mappedBy="Home")
*/
private $Resident;
/**
* Reverse mapping
* #var Building $Landmark
*
* #ORM\OneToOne(targetEntity="Building", mappedBy="Site")
*/
private $Landmark;
}
My Person repository join looks like this:
/**
* #override
* #return Person
*/
public function find( $id ){
$query = $this->getEntityManager()
->createQuery('
SELECT p, h
FROM MyBundle:Person p
JOIN p.Home h
WHERE p.id = :id'
)->setParameter('id', $id);
return $query->getSingleResult();
}
How can I prevent the Place fetching its Building relationship separately during the find operation? Is there something I can pass into the Query instance to stop this?
add fetch option to your mapping.
Like so:
class Place {
/**
* Reverse mapping
* #var Person
*
* #ORM\OneToOne(targetEntity="Person", mappedBy="Home", fetch="EXTRA_LAZY")
*/
private $Resident;
}

DQL many to many and count

I'm using Symfony 2 with Doctrine, and I've got two entities joined in a many to many association.
Let's say I have two entities: User and Group, and the related tables on db are users, groups and users_groups.
I'd like to get the top 10 most populated groups in DQL, but I don't know the syntax to perform queries on the join table (users_groups). I already looked on the Doctrine manual but I didn't found the solution, I guess I still have a lot to learn about DQL.
In plain sql that would be:
select distinct group_id, count(*) as cnt from users_groups group by group_id order by cnt desc limit 10
Can you please help me to translate this to DQL?
Update (classes):
/**
* Entity\E_User
*
* #ORM\Table(name="users")
* #ORM\Entity
*/
class E_User
{
/**
* #ORM\ManyToMany(targetEntity="E_Group", cascade={"persist"})
* #ORM\JoinTable(name="users_groups",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="cascade")},
* inverseJoinColumns={#ORM\JoinColumn(name="group_id", referencedColumnName="id", onDelete="cascade")}
* )
*/
protected $groups;
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $name
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/* ... other attributes & getters and setters ...*/
}
/**
* Entity\E_Group
*
* #ORM\Table(name="groups")
* #ORM\Entity
*/
class E_Group
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $name
*
* #ORM\Column(name="text", type="string", length=255)
*/
private $name;
/* ... other attributes & getters and setters ...*/
}
It's not easy without seeing the actual classes, but by guessing you have a many-to-many bidirectional relationship:
$dql = "SELECT g.id, count(u.id) as cnt FROM Entity\Group g " .
"JOIN g.users u GROUP BY g.id ORDER BY cnt DESC LIMIT 10;";
$query = $em->createQuery($dql);
$popularGroups = $query->getArrayResult();
UPDATE:
You don't have to use a bidirectional relationship, you can query the other way around:
$dql = "SELECT g.id, count(u.id) as cnt FROM Entity\User u " .
"JOIN u.groups g GROUP BY g.id ORDER BY cnt DESC LIMIT 10;";
For those who want to build the query with Doctrine's QueryBuilder instead of using DQL directly take this solution.
Please note that my problem wasn't to get the top user groups, but technically the problem is pretty similar to mine. I work with posts (like articles/blog posts) and tags that are added to posts. I needed to determine a list of related posts (identified by same tags). That list had to be sorted by relevance (the more same tags another post has the more relevant it is).
This is the method of my PostRepository class:
/**
* Finds all related posts sorted by relavance
* (from most important to least important) using
* the tags of the given post entity.
*
* #param Post $post
*
* #return POST[]
*/
public function findRelatedPosts(Post $post) {
// build query
$q = $this->createQueryBuilder('p')
->addSelect('count(t.id) as relevance')
->innerJoin('p.tags', 't')
->where('t.id IN (:tags)')
->setParameter('tags', $post->getTags())
->andWhere('p.id != :post')
->setParameter('post', $post->getId())
->addGroupBy('p.id')
->addOrderBy('relevance', 'DESC')
->getQuery();
// execute query and retrieve database result
$r = $q->execute();
// result contains arrays, each array contains
// the actual post and the relevance value
// --> let's extract the post entities and
// forget about the relevance, because the
// posts are already sorted by relevance
$r = array_map(function ($entry) {
// first index is the post, second one
// is the relevance, just return the post
return reset($entry);
}, $r);
// array of posts
return $r;
}
Thank you #Tom Imrei for you solution. Also the answer #26549597 was very helpful.
To improve Tom's answer, you could use DQL's HIDDEN keyword. This way, the result doesn't contain the useless cnt column and Arvid's array_map solution isn't needed (which could speed up the result significantly for larger queries).
And the OP's question was to get the top 10 groups, not just their IDs.
It would look something like this:
$query = 'SELECT g, HIDDEN COUNT(u.id) AS cnt FROM Entity\Group g LEFT JOIN g.users u ORDER BY cnt DESC';
$groups = $em->createQuery($query)->setMaxResults(10)->getResult();
Also, note the use of LEFT JOIN that ensures that empty groups are not dismissed.

Double association between 2 tables with Doctrine2 PHP

I'm trying to do an association of 5 objects with Doctrine2 (PHP).
I'm using PostgreSQL.
Here is the Database schema:
Database schema
A company may have many Hubs, each Hub have one Harbor.
A company may have many Line, each Line have a Linelist.
A Linelist have 2 Harbors.
For example, a Linelist is "Los Angeles-Seattle", and multiple companies may own it thanks to the Line table.
I'm trying to query all the Hub, Harbor, Linelist, and Line for one company.
I have the SQL query:
SELECT *
FROM hub h
JOIN harbor a
ON a.id = h.harbor_id
JOIN linelist l
ON (l.harborstart_id = a.id OR l.harborend_id = a.id)
JOIN line m
ON m.linelist_id = l.id
WHERE h.company_id = 41
AND m.company_id = 41"
I'm trying to do the same using DQL.
I tried this, but it doesn't worked:
$query = $this->getEntityManager()
->createQuery('SELECT h, a, l, m
FROM AmGameBundle:Hub h
JOIN h.harbor a
JOIN a.linelist l
JOIN l.line m
WHERE h.company = :company_id
AND m.company = :company_id')
->setParameter('company_id', $company_id);
As a result, I only have the LineList and Line objects matching harborstart_id, but I want the one matching either harborstart_id or harborend_id.
Do you think this is possible in DQL?
It might be better to change the relation between Harbor and Linelist for a many to many?
I think that's a matter of defining 2 relations from harbor to linelist in your Entities. I imagine you have something like
<?php
/**
* #Entity
* #Table(name="LineList")
*/
class LineList {
/**
* #var object $startHarbor
* #ManyToOne(targetEntity="Harbor", inversedBy="startHarbors")
* #JoinColumn(name="harborstart_id", referencedColumnName="id", nullable=FALSE)
*/
protected $startHarbor;
}
/**
* #Entity
* #Table(name="Harbor")
*/
class Harbor {
/**
* #var object $startHarbors
* #OneToMany(targetEntity="LineList", mappedBy="startHarbor")
*/
protected $startHarbors;
}
That will let you join Harbors to LineLists via harborstart_id (you named the variable linelist, but I think now it's better to change the identifiers as there will be 2 referring to the same foreign table), then if you want to harborend_id
<?php
/**
* #Entity
* #Table(name="LineList")
*/
class LineList {
/**
* #var object $startHarbor
* #ManyToOne(targetEntity="Harbor", inversedBy="startHarbors")
* #JoinColumn(name="harborstart_id", referencedColumnName="id", nullable=FALSE)
*/
protected $startHarbor;
/**
* #var object $endHarbor
* #ManyToOne(targetEntity="Harbor", inversedBy="endHarbors")
* #JoinColumn(name="harborend_id", referencedColumnName="id", nullable=FALSE)
*/
protected $endHarbor;
}
/**
* #Entity
* #Table(name="Harbor")
*/
class Harbor {
/**
* #var object $startHarbors
* #OneToMany(targetEntity="LineList", mappedBy="startHarbor")
*/
protected $startHarbors;
/**
* #var object $endHarbors
* #OneToMany(targetEntity="LineList", mappedBy="endHarbor")
*/
protected $endHarbors;
}
Now you can change the DQL to:
$query = $this->getEntityManager()
->createQuery('SELECT h, a, sh, eh, m
FROM AmGameBundle:Hub h
JOIN h.harbor a
JOIN a.startHarbors sh
JOIN a.endHarbors eh
JOIN l.line m
WHERE h.company = :company_id
AND m.company = :company_id')
->setParameter('company_id', $company_id);
That should get you in the right direction. If it becomes troublesome though, a many-to-many approach as you speculated should be a well documented solution.

Categories