I have two entities, Class and Student. One Class can have multiple Students (oneToMany):
# YAML notation for Entity 'Class'
...
oneToMany:
students:
targetEntity: MyBundle\Entity\Student
mappedBy: class
For fetching all Classes, I'm writing my own query like this:
SELECT c
FROM MyBundle:Class c
WHERE c.whatever = :parameter
ORDER BY c.id DESC
Now I'm trying to fetch a list of Classes, ordered (DESC) by the count of related Students. So that the result would look like:
Class.id Class.count(Student)
-------- --------------------
3 109
1 81
4 58
2 21
How would I go there? I tried something somewhat like this:
SELECT
c,
COUNT(c.students) AS students
FROM MyBundle:Class c
WHERE c.whatever = :param
GROUP BY c.id
ORDER BY students DESC
(Note: I implement DoctrineExtensions' Date function)
But I'm getting an error:
[Semantical Error] line 0, col 26 near 'students)': Error: Invalid PathExpression. StateFieldPathExpression or SingleValuedAssociationField expected.
Try to use this in your Class repository:
public function getAllClassesOrderedByNumberOfStudents()
{
$qb = $this->createQueryBuilder('class');
$buffer = $qb
->select('class, COUNT(students) AS students_per_class')
->innerJoin('class.students', 'students')
->groupBy('class.id')
->orderBy('students_per_class', 'DESC');
$q = $buffer->getQuery();
return $q->getResult();
}
Related
Problem description
I'm using sqlalchemy (v1.2) declarative, and I have a simple class Node with an id and a label. I would like to build a self-referencing many-to-many relationship where the association table is not a database table, but a dynamic select statement. This statement selects from two joined aliases of Node and returns rows of the form (left_id, right_id), defining the relationship. The code I have so far works if I access the relationship through an instance object, but when I try to filter by the relationship the joins are messed up.
The "classical" self-referential many-to-many relation
For reference, let's start with the example from the documentation on Self-Referential Many-to-Many Relationship, which uses an association table:
node_to_node = Table(
"node_to_node", Base.metadata,
Column("left_node_id", Integer, ForeignKey("node.id"), primary_key=True),
Column("right_node_id", Integer, ForeignKey("node.id"), primary_key=True)
)
class Node(Base):
__tablename__ = 'node'
id = Column(Integer, primary_key=True)
label = Column(String, unique=True)
right_nodes = relationship(
"Node",
secondary=node_to_node,
primaryjoin=id == node_to_node.c.left_node_id,
secondaryjoin=id == node_to_node.c.right_node_id,
backref="left_nodes"
)
def __repr__(self):
return "Node(id={}, Label={})".format(self.id, self.label)
Joining Node to itself through this relationship:
>>> NodeAlias = aliased(Node)
>>> print(session.query(Node).join(NodeAlias, Node.right_nodes))
SELECT node.id AS node_id, node.label AS node_label
FROM node JOIN node_to_node AS node_to_node_1
ON node.id = node_to_node_1.left_node_id
JOIN node AS node_1
ON node_1.id = node_to_node_1.right_node_id
Everything looks well.
The many-to-many relation through an association select statement
As an example we implement a relationship next_two_nodes which connects a node to the two nodes with id+1 and id+2 (if existent). The complete code for testing.
Here is a function which generates the select statement for the "dynamic" association table:
_next_two_nodes = None
def next_two_nodes_select():
global _next_two_nodes
if _next_two_nodes is None:
_leftside = aliased(Node, name="leftside")
_rightside = aliased(Node, name="rightside")
_next_two_nodes = select(
[_leftside.id.label("left_node_id"),
_rightside.id.label("right_node_id")]
).select_from(
join(
_leftside, _rightside,
or_(
_leftside.id + 1 == _rightside.id,
_leftside.id + 2 == _rightside.id
)
)
).alias()
return _next_two_nodes
Note that the function caches the result in a global variable, so that successive calls always return the same object instead of using new aliases. Here is my attempt to use this select in a relationship:
class Node(Base):
__tablename__ = 'node'
id = Column(Integer, primary_key=True)
label = Column(String, unique=True)
next_two_nodes = relationship(
"Node", secondary=next_two_nodes_select,
primaryjoin=(lambda: foreign(Node.id)
== remote(next_two_nodes_select().c.left_node_id)),
secondaryjoin=(lambda: foreign(next_two_nodes_select().c.right_node_id)
== remote(Node.id)),
backref="previous_two_nodes",
viewonly=True
)
def __repr__(self):
return "Node(id={}, Label={})".format(self.id, self.label)
Some test data:
nodes = [
Node(id=1, label="Node1"),
Node(id=2, label="Node2"),
Node(id=3, label="Node3"),
Node(id=4, label="Node4")
]
session.add_all(nodes)
session.commit()
Accessing the relationship through an instance works as expected:
>>> node = session.query(Node).filter_by(id=2).one()
>>> node.next_two_nodes
[Node(id=3, Label=Node3), Node(id=4, Label=Node4)]
>>> node.previous_two_nodes
[Node(id=1, Label=Node1)]
However, filtering on the relationship does not give the expected result:
>>> session.query(Node).join(NodeAlias, Node.next_two_nodes).filter(NodeAlias.id == 3).all()
[Node(id=1, Label=Node1),
Node(id=2, Label=Node2),
Node(id=3, Label=Node3),
Node(id=4, Label=Node4)]
I would expect only Node1 and Node2 to be returned. And indeed, the SQL statement of the join is wrong:
>>> print(session.query(Node).join(NodeAlias, Node.next_two_nodes))
SELECT node.id AS node_id, node.label AS node_label
FROM node JOIN (SELECT leftside.id AS left_node_id, rightside.id AS right_node_id
FROM node AS leftside JOIN node AS rightside
ON leftside.id + 1 = rightside.id OR leftside.id + 2 = rightside.id) AS anon_1
ON anon_1.left_node_id = anon_1.left_node_id
JOIN node AS node_1 ON anon_1.right_node_id = node_1.id
Comparing with the working example above, instead of ON anon_1.left_node_id = anon_1.left_node_id it should clearly read ON node.id = anon_1.left_node_id. My primaryjoin seems to be wrong, but I cannot figure out how to connect the last dots.
After more debugging I found that "Clause Adaption" is replacing my ON clause. I'm not sure about the details, but for some reasen sqlalchemy thinks that I am referring to the node.id from the select rather than from the original Node table. The only way I found to suppress clause adaption was to select in text form:
select(
[literal_column("leftside.id").label("left_node_id"),
literal_column("rightside.id").label("right_node_id")]
)...
This way the relationship to Node is broken and filtering works as expected. It feels like a hack with unforeseeable side effects, maybe someone knows a cleaner way...
I'm trying to improve my query.
I have this table:
id lang_code language
1 fr-1 french
2 fr-2 french
3 en-1 english
4 en-2 english
5 NULL espagnol
6 NULL chineese
I did this query
$langues = $this->getDoctrine()->getManager()
->createQuery('SELECT DISTINCT c FROM AVCMediasBundle:Langue c GROUP BY c.language')
->getResult()
;
I have this result and it's OK for the moment
id lang_code language
2 fr-2 french
4 en-2 english
5 NULL espagnol
6 NULL chineese
But now I'm trying to improve this query. I need to do the same but without NULL result in lang_code column
The result must be like that:
id lang_code language
2 fr-2 french
4 en-2 english
I tired that but some problems...
$langues = $this->getDoctrine()->getManager()
->createQuery('SELECT DISTINCT c FROM AVCMediasBundle:Langue c GROUP BY c.language , WHERE c.langCode IS NOT NULL')
->getResult()
;
How can I do please ? thx :)
You have wrong order of clauses in your DQL (btw this would be also wrong in plain sql) - GROUP BY must go to the end of query, after WHERE
Try this one:
$langues_dispo_google_speech = $this->getDoctrine()->getManager()
->createQuery('SELECT DISTINCT c FROM AVCMediasBundle:Langue c WHERE c.langCode IS NOT NULL GROUP BY c.language')
->getResult()
;
Check this to see correct sql syntax
I have two entities, Class and Student. One Class can have multiple Students, so it's a oneToMany relation. (Don't bother, that it should be a manyToMany relationship, I'm just drawing an example.)
# YAML notation for Entity 'Class'
...
oneToMany:
students:
targetEntity: MyBundle\Entity\Student
mappedBy: class
# YAML notation for Entity 'Student'
...
date: datetime # this is the date where Student joined a Class
For fetching all Classes, I'm writing my own query like this:
SELECT c
FROM MyBundle:Class c
WHERE c.whatever = :parameter
ORDER BY c.id DESC
Now I'm trying to fetch a list of "rising" Classes. That means, that all Classes, that have been joined by Students within the last five days - ordered (DESC) by the number of joined Students.
So that the result would look like:
Class.id Class.count(Student) Latest Join
-------- -------------------- -----------
3 19 2013-12-07
1 12 2013-12-08
4 8 2013-12-07
2 5 2013-12-09
How would I go there? I tried something somewhat like this:
SELECT
c,
COUNT(c.students) AS students,
MAX(s.date)
FROM MyBundle:Class c
WHERE c.whatever = :param
AND DATE(s.date) > :fivedayspan
ORDER BY students DESC
(Note: I implement DoctrineExtensions' Date function)
I am setting the parameter:
$q->setParameter('fivedayspan', date('Y-m-d', strtotime('-5 days')));
But I'm getting an error:
[Semantical Error] line 0, col 36 near 'students)': Error: Invalid PathExpression. StateFieldPathExpression or SingleValuedAssociationField expected.
Will this work?
SELECT
c,
COUNT(c.students) AS c.N,
MAX(c.date)
FROM MyBundle:Class c
WHERE c.whatever = :param
AND DATE(c.date) > :fivedayspan
ORDER BY c.N DESC
Ok i have this code:
SELECT
IFNULL(s2.id,s1.id) AS effectiveID,
IFNULL(s2.status, s1.status) AS effectiveStatus,
IFNULL(s2.user_id, s1.user_id) as effectiveUser,
IFNULL(s2.likes_count, s1.likes_count) as effectiveLikesCount
FROM statuses AS s1
LEFT JOIN statuses AS s2 ON s2.id = s1.shared_from_id
WHERE s1.user_id = 4310
ORDER BY effectiveID DESC
LIMIT 15
And i need to rewrite it to querybuilder. Something like that?
$fields = array('IFNULL(s2.id,s1.id) AS effectiveID','IFNULL(s2.status, s1.status) AS effectiveStatus', 'IFNULL(s2.user_id, s1.user_id) as effectiveUser','IFNULL(s2.likes_count, s1.likes_count) as effectiveLikesCount');
$qb=$this->_em->createQueryBuilder()
->select($fields)
->from('WallBundle:Status','s1')
->addSelect('u')
->where('s1.user = :user')
->andWhere('s1.admin_status = false')
->andWhere('s1.typ_statusu != :group')
->setParameter('user', $user)
->setParameter('group', 'group')
->leftJoin('WallBundle:Status','s2', 'WITH', 's2.id=s1.shared_from_id')
->innerJoin('s1.user', 'u')
->orderBy('s1.time', 'DESC')
->setMaxResults(15);
var_dump($query=$qb->getQuery()->getResult());die();
This error is
[Syntax Error] line 0, col 7: Error: Expected known function, got 'IFNULL'
Use COALESCE instead of IFNULL like this
$fields = array('COALESCE(s2.id,s1.id) AS effectiveID','COALESCE(s2.status, s1.status) AS effectiveStatus', 'COALESCE(s2.user_id, s1.user_id) as effectiveUser','COALESCE(s2.likes_count, s1.likes_count) as effectiveLikesCount');
COALESCE return the first value not null in the list, so if A is null and B not null, then COALESCE(A,B) will return B.
There is a Doctrine extension that adds this among others.
This is the DQL file from IFNULL.
https://github.com/beberlei/DoctrineExtensions/blob/master/src/Query/Mysql/IfNull.php
This chapter explains how you use them.
http://symfony.com/doc/2.0/cookbook/doctrine/custom_dql_functions.html
My action:
$matches_request = $em->getRepository('Bundle:ChanceMatch')->findByRequestUser(1);
$matches_reply = $em->getRepository('Bundle:ChanceMatch')->findByReplyUser(1);
Is it possible to join the querys with an or condition with getRepository, eg.
$matches_reply = $em->getRepository('FrontendChancesBundle:ChanceMatch')->findBy(array('requestUser' => 1, 'replyUser' => 1);
//this of course gives me the a result when `requestUser` and `replyUser` is `1`.
My table
id | requestUser | replyUser
....
12 | 1 | 2
13 | 5 | 1
My query should return the id 12 & 13.
Thanks for help!
You can use QueryBuilder or create a custom repository for that entity and create a function that internally use QueryBuilder.
$qb = $em->getRepository('FrontendChancesBundle:ChanceMatch')->createQueryBuilder('cm');
$qb
->select('cm')
->where($qb->expr()->orX(
$qb->expr()->eq('cm.requestUser', ':requestUser'),
$qb->expr()->eq('cm.replyUser', ':replyUser')
))
->setParameter('requestUser', $requestUserId)
->setParameter('replyUser', $replyUserId)
;
$matches_reply = $qb->getQuery()->getSingleResult();
// $matches_reply = $qb->getQuery()->getResult(); // use this if there can be more than one result
For more information on custom Repository see official documentation:
http://symfony.com/doc/2.0/book/doctrine.html#custom-repository-classes
It's possible to use Criteria for the complex queries with getRepository():
$criteria = new \Doctrine\Common\Collections\Criteria();
$criteria
->orWhere($criteria->expr()->contains('domains', 'a'))
->orWhere($criteria->expr()->contains('domains', 'b'));
$domain->ages = $em
->getRepository('Group')
->matching($criteria);