From f99a096d01bd0324ba641ae7c4831bfc0142f84b Mon Sep 17 00:00:00 2001 From: "Jonathan H. Wage" Date: Fri, 14 May 2010 15:23:51 -0400 Subject: [PATCH] Adding a little more information to the dql chapter. --- manual/en/dql-doctrine-query-language.txt | 339 +++++++++++++++------- 1 file changed, 240 insertions(+), 99 deletions(-) diff --git a/manual/en/dql-doctrine-query-language.txt b/manual/en/dql-doctrine-query-language.txt index fe96c7d47..233b46577 100755 --- a/manual/en/dql-doctrine-query-language.txt +++ b/manual/en/dql-doctrine-query-language.txt @@ -2,17 +2,16 @@ DQL stands for **D**octrine **Q**uery **L**anguage and is an Object Query Language derivate that is very similar to the **H**ibernate **Q**uery **L**anguage (HQL) or the **J**ava **P**ersistence **Q**uery **L**anguage (JPQL). -In essence, DQL provides powerful querying capabilities over your object model. Imagine all your objects lying around in some storage (like an object database). When writing DQL queries, think about querying that storage to pick a certain subset of your objects. +In essence, DQL provides powerful querying capabilities over your object model. Imagine all your objects lying around in some storage (like an object database). When writing DQL queries, think about querying that storage to pick a certain subset of your objects. > **CAUTION** > A common mistake for beginners is to mistake DQL for being just some form of SQL > and therefore trying to use table names and column names or join arbitrary tables > together in a query. You need to think about DQL as a query language for your object -> model, not for your relational schema. +> model, not for your relational schema. DQL is case in-sensitive, except for namespace, class and field names, which are case sensitive. - ++ Types of DQL queries DQL as a query language has SELECT, UPDATE and DELETE constructs that map to their corresponding @@ -21,7 +20,7 @@ have to be introduced into the persistence context through `EntityManager#persis consistency of your object model. DQL SELECT statements are a very powerful way of retrieving parts of your domain model that are -not accessible via assocations. Additionally they allow to retrieve entities and their associations +not accessible via associations. Additionally they allow to retrieve entities and their associations in one single sql select statement which can make a huge difference in performance in contrast to using several queries. @@ -37,9 +36,10 @@ The select clause of a DQL query specifies what appears in the query result. The Here is an example that selects all users with an age > 20: - [sql] - SELECT u FROM MyProject\Model\User u WHERE u.age > 20 - + [php] + $query = $em->createQuery('SELECT u FROM MyProject\Model\User u WHERE u.age > 20'); + $users = $query->getResult(); + Lets examine the query: * `u` is a so called identification variable or alias that refers to the `MyProject\Model\User` class. By placing this alias in the SELECT clause we specify that we want all instances of the User class that are matched by this query appear in the query result. @@ -52,7 +52,7 @@ The SELECT clause allows to specify both class identification variables that sig of a complete entity class or just fields of the entity using the syntax `u.name`. Combinations of both are also allowed and it is possible to wrap both fields and identification values into aggregation and DQL functions. Numerical fields can -be part of computations using mathematical operatos. See the sub-section on [DQL Functions, Aggregates and Operations](#dqlfn) +be part of computations using mathematical operations. See the sub-section on [DQL Functions, Aggregates and Operations](#dqlfn) on more information. +++ Joins @@ -69,21 +69,23 @@ Example: Regular join of the address: - [sql] - SELECT u FROM User u JOIN u.address a WHERE a.city = 'Berlin' + [php] + $query = $em->createQuery("SELECT u FROM User u JOIN u.address a WHERE a.city = 'Berlin'"); + $users = $query->getResult(); Fetch join of the address: - [sql] - SELECT u, a FROM User u JOIN u.address a WHERE a.city = 'Berlin' + [php] + $query = $em->createQuery("SELECT u, a FROM User u JOIN u.address a WHERE a.city = 'Berlin'"); + $users = $query->getResult(); When Doctrine hydrates a query with fetch-join it returns the class in the FROM clause on the root level of the result array. In the previous example an array of User instances is returned and the address of each user is fetched and hydrated into the `User#address` variable. If you access -the address Doctrine does not need to +the address Doctrine does not need to lazy load the association with another query. > **NOTE** -> Doctrine allows you to walk all the assocations between all the objects in your domain model. +> Doctrine allows you to walk all the associations between all the objects in your domain model. > Objects that were not already loaded from the database are replaced with lazy load proxy instances. > Non-loaded Collections are also replaced by lazy-load instances that fetch all the contained objects upon > first access. However relying on the lazy-load mechanism leads to many small queries executed @@ -99,95 +101,194 @@ Named parameters are specified with ":name1", ":name2" and so on. +++ DQL SELECT Examples -This section contains a large set of DQL queries and some explainations of what is happening. +This section contains a large set of DQL queries and some explanations of what is happening. The actual result also depends on the hydration mode. - [sql] - -- Hydrate all Users - SELECT u FROM MyProject\Model\User u +Hydrate all User entities: - -- Retrieve the IDs of all CmsUsers - SELECT u.id FROM CmsUser u + [php] + $query = $em->createQuery('SELECT u FROM MyProject\Model\User u'); + $users = $query->getResult(); // array of User objects - -- Retrieve the IDs of all users that have written an article - SELECT DISTINCT a.user.id FROM CmsArticle a +Retrieve the IDs of all CmsUsers: - -- Retrieve all articles and sort them by the name of the articles users instance - SELECT a FROM CmsArticle a ORDER BY a.user.name ASC + [php] + $query = $em->createQuery('SELECT u.id FROM CmsUser u'); + $ids = $query->getResult(); // array of CmsUser ids - -- Retrieve the Username and Name of a CmsUser - SELECT u.username, u.name FROM CmsUser u +Retrieve the IDs of all users that have written an article: - -- Retrieve a ForumUser and his single associated entity - SELECT u, a FROM ForumUser u JOIN u.avatar a + [php] + $query = $em->createQuery('SELECT DISTINCT a.user.id FROM CmsArticle a'); + $ids = $query->getResult(); // array of CmsUser ids - -- Retrieve a CmsUser and fetch join all the phonenumbers he has - SELECT u, p FROM CmsUser u JOIN u.phonenumbers p +Retrieve all articles and sort them by the name of the articles users instance: - -- Hydrate a result in Ascending or Descending Order - SELECT u FROM ForumUser u ORDER BY u.id ASC - SELECT u FROM ForumUser u ORDER BY u.id DESC + [php] + $query = $em->createQuery('SELECT a FROM CmsArticle a ORDER BY a.user.name ASC'); + $articles = $query->getResult(); // array of CmsArticle objects - -- Using Aggregate Functions - SELECT COUNT(u.id) FROM CmsUser u GROUP BY u.id +Retrieve the Username and Name of a CmsUser: - -- With WHERE Clause and Positional Parameter - SELECT u FROM ForumUser u WHERE u.id = ?1 + [php] + $query = $em->createQuery('SELECT u.username, u.name FROM CmsUser u'); + $users = $query->getResults(); // array of CmsUser username and id values + echo $users[0]['username']; - -- With WHERE Clause and Named Parameter - SELECT u FROM ForumUser u WHERE u.username = :name +Retrieve a ForumUser and his single associated entity: - -- With Nested Conditions in WHERE Clause - SELECT u from ForumUser u WHERE (u.username = :name OR u.username = :name2) AND u.id = :id + [php] + $query = $em->createQuery('SELECT u, a FROM ForumUser u JOIN u.avatar a'); + $users = $query->getResult(); // array of ForumUser objects with the avatar association loaded + echo get_class($users[0]->getAvatar()); - -- With COUNT DISTINCT - SELECT COUNT(DISTINCT u.name) FROM CmsUser +Retrieve a CmsUser and fetch join all the phonenumbers he has: - -- With Arithmetic Expression in WHERE clause - SELECT u FROM CmsUser u WHERE ((u.id + 5000) * u.id + 3) < 10000000 + [php] + $query = $em->createQuery('SELECT u, p FROM CmsUser u JOIN u.phonenumbers p'); + $users = $query->getResult(); // array of CmsUser objects with the phonenumbers association loaded + $phonenumbers = $users[0]->getPhonenumbers(); - -- Using multiple classes fetched using a FROM clause (all returned on the root level of the result) - SELECT u, a FROM CmsUser u, CmsArticle a WHERE u.id = a.user.id +Hydrate a result in Ascending: - -- Using a LEFT JOIN to hydrate all user-ids and optionally associated article-ids - SELECT u.id, a.id FROM CmsUser u LEFT JOIN u.articles a + [php] + $query = $em->createQuery('SELECT u FROM ForumUser u ORDER BY u.id ASC'); + $users = $query->getResult(); // array of ForumUser objects - -- Restricting a JOIN clause by additional conditions - SELECT u FROM CmsUser u LEFT JOIN u.articles a WITH a.topic LIKE '%foo%' +Or in Descending Order: - -- Using several Fetch JOINs - SELECT u, a, p, c FROM CmsUser u - JOIN u.articles a - JOIN u.phonenumbers p - JOIN a.comments c + [php] + $query = $em->createQuery('SELECT u FROM ForumUser u ORDER BY u.id DESC'); + $users = $query->getResult(); // array of ForumUser objects - -- BETWEEN in WHERE clause - SELECT u.name FROM CmsUser u WHERE u.id BETWEEN ?1 AND ?2 +Using Aggregate Functions: - -- DQL Functions in WHERE clause - SELECT u.name FROM CmsUser u WHERE TRIM(u.name) = 'someone' + [php] + $query = $em->createQuery('SELECT COUNT(u.id) FROM Entities\User u'); + $count = $query->getSingleScalarResult(); - -- IN() Expression - SELECT u.name FROM CmsUser u WHERE u.id IN(46) - SELECT u FROM CmsUser u WHERE u.id IN (1, 2) - SELECT u FROM CmsUser u WHERE u.id NOT IN (1) +With WHERE Clause and Positional Parameter: - -- CONCAT() DQL Function - SELECT u.id FROM CmsUser u WHERE CONCAT(u.name, 's') = ?1 - SELECT CONCAT(u.id, u.name) FROM CmsUser u WHERE u.id = ?1 + [php] + $query = $em->createQuery('SELECT u FROM ForumUser u WHERE u.id = ?1'); + $users = $query->getResult(); // array of ForumUser objects - -- EXISTS in WHERE clause with correlated Subquery - SELECT u.id FROM CmsUser u - WHERE EXISTS (SELECT p.phonenumber FROM CmsPhonenumber p WHERE p.phonenumber = u.id) +With WHERE Clause and Named Parameter: - -- Get all users who are members of $group. - SELECT u.id FROM CmsUser u WHERE :param MEMBER OF u.groups + [php] + $query = $em->createQuery('SELECT u FROM ForumUser u WHERE u.username = :name'); + $users = $query->getResult(); // array of ForumUser objects - -- Get all users that have more than 1 phonenumber - SELECT u FROM CmsUser u WHERE SIZE(u.phonenumbers) > 1 +With Nested Conditions in WHERE Clause: - -- Get all users that have no phonenumber - SELECT u FROM CmsUser u WHERE u.phonenumbers IS EMPTY + [php] + $query = $em->createQuery('SELECT u from ForumUser u WHERE (u.username = :name OR u.username = :name2) AND u.id = :id'); + $users = $query->getResult(); // array of ForumUser objects + +With COUNT DISTINCT: + + [php] + $query = $em->createQuery('SELECT COUNT(DISTINCT u.name) FROM CmsUser + $users = $query->getResult(); // array of ForumUser objects + +With Arithmetic Expression in WHERE clause: + + $query = $em->createQuery('SELECT u FROM CmsUser u WHERE ((u.id + 5000) * u.id + 3) < 10000000'); + $users = $query->getResult(); // array of ForumUser objects + +Using a LEFT JOIN to hydrate all user-ids and optionally associated article-ids: + + [php] + $query = $em->createQuery('SELECT u.id, a.id as article_id FROM CmsUser u LEFT JOIN u.articles a'); + $results = $query->getResult(); // array of user ids and every article_id for each user + +Restricting a JOIN clause by additional conditions: + + [php] + $query = $em->createQuery("SELECT u FROM CmsUser u LEFT JOIN u.articles a WITH a.topic LIKE '%foo%'"); + $users = $query->getResult(); + +Using several Fetch JOINs: + + [php] + $query = $em->createQuery('SELECT u, a, p, c FROM CmsUser u JOIN u.articles a JOIN u.phonenumbers p JOIN a.comments c'); + $users = $query->getResult(); + +BETWEEN in WHERE clause: + + [php] + $query = $em->createQuery('SELECT u.name FROM CmsUser u WHERE u.id BETWEEN ?1 AND ?2'); + $usernames = $query->getResult(); + +DQL Functions in WHERE clause: + + [php] + $query = $em->createQuery("SELECT u.name FROM CmsUser u WHERE TRIM(u.name) = 'someone'"); + $usernames = $query->getResult(); + +IN() Expression: + + [php] + $query = $em->createQuery('SELECT u.name FROM CmsUser u WHERE u.id IN(46)'); + $usernames = $query->getResult(); + + $query = $em->createQuery('SELECT u FROM CmsUser u WHERE u.id IN (1, 2)'); + $users = $query->getResult(); + + $query = $em->createQuery('SELECT u FROM CmsUser u WHERE u.id NOT IN (1)'); + $users = $query->getResult(); + +CONCAT() DQL Function: + + [php] + $query = $em->createQuery("SELECT u.id FROM CmsUser u WHERE CONCAT(u.name, 's') = ?1"); + $ids = $query->getResult(); + + $query = $em->createQuery('SELECT CONCAT(u.id, u.name) FROM CmsUser u WHERE u.id = ?1'); + $idUsernames = $query->getResult(); + +EXISTS in WHERE clause with correlated Subquery + + [php] + $query = $em->createQuery('SELECT u.id FROM CmsUser u WHERE EXISTS (SELECT p.phonenumber FROM CmsPhonenumber p WHERE p.phonenumber = u.id)'); + $ids = $query->getResult(); + +Get all users who are members of $group. + + [php] + $query = $em->createQuery('SELECT u.id FROM CmsUser u WHERE :param MEMBER OF u.groups'); + $ids = $query->getResult(); + +Get all users that have more than 1 phonenumber + + [php] + $query = $em->createQuery('SELECT u FROM CmsUser u WHERE SIZE(u.phonenumbers) > 1'); + $users = $query->getResult(); + +Get all users that have no phonenumber + + [php] + $query = $em->createQuery('SELECT u FROM CmsUser u WHERE u.phonenumbers IS EMPTY'); + $users = $query->getResult(); + +++++ Partial Object Syntax + +By default when you run a DQL query in Doctrine and select only a subset of the +fields for a given entity, you do not receive objects back. Instead, you receive +only arrays as a flat rectangular result set, similar to how you would if you +were just using SQL directly and joining some data. + +If you want to select partial objects you can use the `partial` DQL keyword: + + [php] + $query = $em->createQuery('SELECT partial u.{id, username} FROM CmsUser u'); + $users = $query->getResult(); // array of partially loaded CmsUser objects + +You use the partial syntax when joining as well: + + [php] + $query = $em->createQuery('SELECT partial u.{id, username}, partial a.{id, name} FROM CmsUser u JOIN u.articles a'); + $users = $query->getResult(); // array of partially loaded CmsUser objects +++ Using INDEX BY @@ -357,14 +458,13 @@ An instance of the `Doctrine\ORM\Query` class represents a DQL query. You create [php] // $em instanceof EntityManager - + // example1: passing a DQL string $q = $em->createQuery('select u from MyProject\Model\User u'); - + // example2: usin setDql $q = $em->createQuery(); $q->setDql('select u from MyProject\Model\User u'); - +++ Query Result Formats @@ -397,7 +497,7 @@ A pure result usually looks like this: [1] => Object [2] => Object ... - + A mixed result on the other hand has the following general structure: array @@ -431,30 +531,78 @@ Here is how the result could look like: ... And here is how you would access it in PHP code: - + [php] foreach ($results as $row) { echo "Name: " . $row[0]->getName(); echo "Name UPPER: " . $row['nameUpper']; } - + You may have observed that in a mixed result, the object always ends up on index 0 of a result row. -+++ Hydration Mode Asumptions ++++ Hydration Modes -Each of the Hydration Modes makes assumptions about how the result is returned to userland. You should +Each of the Hydration Modes makes assumptions about how the result is returned to user land. You should know about all the details to make best use of the different result formats: +The constants for the different hydration modes are: + +* Query::HYDRATE_OBJECT +* Query::HYDRATE_ARRAY +* Query::HYDRATE_SCALAR +* Query::HYDRATE_SINGLE_SCALAR + ++++ Object Hydration +Object hydration hydrates the result set into the object graph: + + [php] + $query = $em->createQuery('SELECT u FROM CmsUser u'); + $users = $query->getResult(Query::HYDRATE_OBJECT); + ++++ Array Hydration -++++ Scalar Hydration Details +You can run the same query with array hydration and the result set is hydrated into +an array that represents the object graph: + + [php] + $query = $em->createQuery('SELECT u FROM CmsUser u'); + $users = $query->getResult(Query::HYDRATE_ARRAY); + +You can use the `getArrayResult()` shortcut as well: + + [php] + $users = $query->getArrayResult(); + +++++ Scalar Hydration + +If you want to return a flat rectangular result set instead of an object graph +you can use scalar hydration: + + [php] + $query = $em->createQuery('SELECT u FROM CmsUser u'); + $users = $query->getResult(Query::HYDRATE_SCALAR); + echo $users[0]['u_id']; The following assumptions are made about selected fields using Scalar Hydration: 1. Fields from classes are prefixed by the DQL alias in the result. A query of the kind 'SELECT u.name ..' returns a key 'u_name' in the result rows. +++++ Single Scalar Hydration + +If you a query which returns just a single scalar value you can use single scalar +hydration: + + [php] + $query = $em->createQuery('SELECT COUNT(a.id) FROM CmsUser u LEFT JOIN u.articles a WHERE u.username = ?1 GROUP BY u.id'); + $query->setParameter(1, 'jwage'); + $numArticles = $query->getResult(Query::HYDRATE_SCALAR); + +You can use the `getSingleScalarResult()` shortcut as well: + + [php] + $numArticles = $query->getSingleScalarResult(); + +++ Iterating Large Resultsets There are situations when a query you want to execute returns a very large result-set that needs @@ -583,7 +731,7 @@ The following context-free grammar, written in an EBNF variant, describes the Do QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement -+++ Statements ++++ Statements SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] UpdateStatement ::= UpdateClause [WhereClause] @@ -736,12 +884,12 @@ The following context-free grammar, written in an EBNF variant, describes the Do ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary | StateFieldPathExpression BooleanPrimary | CaseExpression | EntityTypeExpression - CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | + CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression - GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression + GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" - WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression - SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* + WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression + SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" CaseOperand ::= StateFieldPathExpression | TypeDiscriminator SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression @@ -761,7 +909,7 @@ The following context-free grammar, written in an EBNF variant, describes the Do AggregateExpression ::= ("AVG" | "MAX" | "MIN" | "SUM") "(" ["DISTINCT"] StateFieldPathExpression ")" | "COUNT" "(" ["DISTINCT"] (IdentificationVariable | SingleValuedPathExpression) ")" - + +++ Other Expressions QUANTIFIED/BETWEEN/COMPARISON/LIKE/NULL/EXISTS @@ -793,11 +941,4 @@ QUANTIFIED/BETWEEN/COMPARISON/LIKE/NULL/EXISTS "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" | "LOWER" "(" StringPrimary ")" | - "UPPER" "(" StringPrimary ")" - - - - - - - + "UPPER" "(" StringPrimary ")" \ No newline at end of file