This question already has an answer here:
partial select doctrine query builder
(1 answer)
Closed 1 year ago.
I have one problem in Symfony that I cannot solve.
I have one Entity class e.g. Category where I have e.g. $id, $name, $description, OneToMany User[] $members, OneToMany Book[] $books
Now... I need to get all categories (e.g. WHERE description IS NOT NULL) but in results, I don't want to have $books.
I need Category with id, name, description and [member1, member2...] but NO books.
I use something like:
$em->getRepository('AppBundle:Category')->findAll();
You can use ->select('fieldA, fieldB') to query specific fields.
Create a method in your repository to make your custom query:
public function findAllWithoutBooks()
{
return $this->createQueryBuilder('c')
->select('c.id, c.name, c.description')
->leftJoin('c.members', 'm')
->addSelect('m.id AS member_id, m.name AS member_name')
->andWhere('c.description IS NOT NULL')
->getQuery()
->getResult()
}
And use it like any other method:
$em->getRepository('AppBundle:Category')->findAllWithoutBooks();
Doctrine by default uses Lazy fetching, meaning the books are not queried until after you call $category->getBooks(). Using partial references is not needed as detailed below, unless the mapping fetch declaration was changed to EAGER for the association, provided you are not calling $category->getBooks();
Alternatively, a custom query using the partial keyword can be used to explicitly select only the specified fields and hydrate partial Category and associated User objects.
Use of partial objects is tricky. Fields that are not retrieved from the database will not be updated by the UnitOfWork even if they get changed in your objects. You can only promote a partial object to a fully-loaded object by calling EntityManager#refresh() or a DQL query with the refresh flag.
Example Query
$qb = $em->getRepository('AppBundle:Category')->createQueryBuilder('c');
$categories = $qb->select('partial c.{id, name, description}')
->leftJoin('c.members', 'm')
->addSelect('partial m.{id, name}')
->where($qb->expr()->isNotNull('c.description'))
->getQuery()
->getResult();
Result
array(
Category {
id,
name,
description,
members => array(
User {
id,
name
},
User {
id,
name
}
...
),
},
Category {
id,
name,
description,
members => array(
User {
id,
name
},
User {
id,
name
}
...
)
}
...
)
Related
I have a list with messages. Its possible to reply to these messages (parent - child). I do not show child-messages in the list.
How can I always display the newest parent-message on top. Newest means that either the parent OR one of the childern has the newest timestamp.
Here is my eloquent query:
Message::withCount(['childMessages as latest_child_message' => function($query) {
$query->select(DB::raw('max(created_at)'));
}])
->orderByDesc('latest_child_message')
->orderByDesc('created_at')
->get();
Both orderBy should somehow be combined. Otherwise either the parent or the child sort will be prioritised.
In the context it's not possible to sort the collection after the DB-query.
edit 1:
Since "ee" is the latest response (child), the "bb" message should be at the bottom of the list.
edit 2:
The query will be used in a function returning a query
public static function getEloquentQuery(): Builder {
$query = parent::getEloquentQuery();
return $query->doTheMagicHere();
}
edit 3
This would be a working query.. but it's very slow
SELECT
id,
comment,
(SELECT MAX(cc.id) FROM comments cc WHERE cc.commentable_id = c.id) AS child_id
FROM
comments c
WHERE
commentable_type NOT LIKE '%Comment%'
ORDER BY CASE WHEN child_id IS NULL
THEN id
ELSE child_id
END DESC
;
In the withCount closure you must set conditions.
Use this:
Message::with('childMessages')->get()->sortByDesc(function ($parent, $key) {
$child = $parent->childMessages()->orderBy('created_at', 'desc')->first();
return $child ? $child->created_at : $parent->created_at;
});
the orderBy way you need is a bit complicated. it's better to use sortByDesc method and sort data on collection.
I hope this works.
What I need :
I'm building an API that returns users and some relations : I have an entity called "User" which has a lot of relationships. Let's take the "comments" as example :
/**
* #ORM\OneToMany(targetEntity="Comments", mappedBy="idClient", cascade={"persist"})
*/
protected $comments;
In some cases, the client wants to get the user data and the comments data in the same query (by adding "comments" to the "include query param), and wants to sort the comments in a specific order. This order is provided by the client in the query params. In this example, the comments must be sorted by id ASC.
/api/users?include=comments&sort=comments.id
Note that order ASC is implicit in that case.
I have a search() function that build the query :
$qb = $this->createQueryBuilder($this->elementName);
/* SELECTs */
$selects = $this->getSelects($params);
foreach($selects as $select) {
$qb->addSelect($select);
}
/* WHEREs */
$wheres = $this->getWheres($params);
foreach($wheres as $where) {
$qb->andWhere($where);
}
/* ORDER BY */
foreach($sortBy as $column => $order) {
$qb->addOrderBy($column, $order);
}
/* LIMIT and OFFSET */
$qb->setFirstResult($offset)
->setMaxResults($limit);
$query = $qb->getQuery();
$results = $query->getResult();
This function is called to get the primary data of the request : the users data. Then, the users are transformed by a UserTransformer, in order to answer the client in a specific format (JSONAPI).
The relationships (as comments) are called later by querying the entity is the object transformer :
$comments = $user->getComments(); // Returning $this->comments in the User class.
return $this->collection($comments, new CommentsTransformer()); // sends the $comments data to the CommentsTransformer.
What I tried
I tried addOrderBy() to the query builder but I get an error because the DQL does not contains any association named comments.id :
Doctrine\ORM\Query\QueryException: [Semantical Error] line 0, col 110 near 'id ASC': Error: Class Foo\Users has no field or association named comments.id
Here is the DQL :
SELECT e FROM Foo\Users u WHERE [...] ORDER BY u.comments.id ASC
Is there any way I can "see" the comments properties and sort the comments on them in my Query ?
Or is there any way I can inject the sort order in my Users class so it can retrieve the comments data in that dynamical order ? like using $user->getComments($sortBy) and then catch the $sortBy in my Users class (or preferably on my entity mother class) and alter the build-in Doctrine request to add my sorting order ?
PS : sorry for (probably) bad english, it's not my mother tongue.
apply criteria in your getComments function like
use Doctrine\Common\Collections\Criteria;
public function getComments()
{
$criteria = Criteria::create()
->orderBy(['id' => Criteria::ASC]);
return $this->comments->matching($criteria);
}
I'm facing some trouble getting my results with doctrine query builder, in a Symfony 2.8 app :
I've got here 3 entities :
Song
Artist
Category
All songs have at least 1 artist and 1 category
Song has manytomany relation with Artist, and manytomany with Category aswell
I would like to get the Songs entities having the same artists OR categories as one song given to this function :
public function findRelatedSongs($song)
{
$em = $this->getEntityManager();
$artistsIds = $this->getArtistsIds($song);
//returns a string like '1,2,3'
$categoriesIds = $this->getCategoriesIds($song);
//returns a string like '1,2,3'
$q = $em->getRepository("BeatAdvisorBundle\Entity\Song")
->createQueryBuilder('s')
->join('s.artists', 'a')
->join('s.categories', 'c')
->where('a.id in (:artistsIds)')
->orWhere('c.id in (:categoriesIds)')
->andWhere('s.id <> :songId')
->setParameter('artistsIds', $artistsIds)
->setParameter('categoriesIds', $categoriesIds)
->setParameter('songId', $song->getId())
->getQuery();
$sql = $q->getSql();
// here I can read the sql query generated
$result = $q->setMaxResults(16)
->getResult();
return $result;
}
It gives me back the related songs on same artists, but not on categories.
Is there a problem with the way I wrote this ?
If I copy and paste the sql query, setting the ids as parameters like something_id in (1,2) it works good...
EDIT
Now I know that song-A having only artist-x will match some songs having only artist-x ; same for categories. might be a problem of type (string VS int) causing problems with in(x,y) instead of in (x) ?...
As far as I know Doctrine uses DQL (Doctrine Query Language), not SQL. Expressions are a bit different sometimes. You can use the QueryBuilders Expression object to programmatically build your expressions.
$qb->where(
$qb->expr()->in('a.id', ':artistsIds'),
$qb->expr()->eq('s.id', ':songId')
);
Reference:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/query-builder.html
OK, my error was to set my parameters as string (imploded arrays of ids).
I had to give the array of integers itself...
I have a table of posts related many to one to a table of authors. Both tables are related to a third table (likes) that indicates which users have liked which posts. I'd like to select the authors and likes with the posts, but don't know how to access the joined objects after fetching results. My query builder looks as follows:
$result = $em->createQueryBuilder()
->select('p')
->from('Post', 'p')
->leftJoin('p.author', 'a')
->leftJoin('p.likes', 'l', 'WITH', 'l.post_id = p.id AND l.user_id = 10')
->where("p.foo = bar")
->addSelect('a AS post_author')
->addSelect('l AS post_liked')
->getQuery()->getResult();
In the above, the author will always be valid, where the like value may be null if the requesting user (user 10 in the example) has not interacted with the post. The query works fine, but I can't access the data for aliases post_author or post_liked. The resulting data looks like this:
[
[0] => Doctrine PostEntity,
[1] => Doctrine PostEntity,
...
]
I'd like something that looks more like this:
[
[0] => ['post' => Doctrine PostEntity,
'post_author' => Doctrine UserEntity,
'post_liked' => Doctrine LikeEntity],
[1] => ['post' => Doctrine PostEntity,
'post_author' => Doctrine UserEntity,
'post_liked' => Doctrine LikeEntity],
...
]
Were I only trying to load the author, it'd be fine because I could load the author value from the loaded post (Doctrine automatically hydrates the object with selected join data from the author table). For example:
$post = $result[0];
$author = $post->getAuthor(); // Doctrine UserEntity
The issue comes up if I try to load a like for the current user. For example:
$post = $result[0];
$like = $post->getLike(); // INVALID - there's no property "like" on Post
$likes = $post->getLikes(); // valid, but loads the whole collection
$like = $post->post_liked; // INVALID - the SQL alias is not a valid object property
How do I access the data specified in the query?
I ended up using $query->getArrayResults() which populates an array with collections based on the association naming in doctrine configuration. The AS keyword in the query only serves as a hook for the query itself, not the output array/entity hydration.
For a complete solution with examples, see my answer here.
I have a Pivot table thats used to join two other tables that have many relations per hotel_id. Is there a way I can eagerload the relationship that pulls the results for both tables in one relationship? The raw SQL query, works correctly but when using belongsToMany the order is off.
Amenities Pivot Table
id
hotel_id
distance_id
type_id
Distance Table
id
name
Type Table
id
name
RAW Query (This works fine)
SELECT * FROM amenities a
LEFT JOIN distance d ON a.distance_id = d.id
LEFT JOIN type t ON a.type_id = t.id WHERE a.hotel_id = ?
My "Hotels" Model is using belongsToMany like so
public function distance() {
return $this->belongsToMany('Distance', 'amenities', 'hotel_id', 'distance_id');
}
public function type() {
return $this->belongsToMany('Type', 'amenities', 'hotel_id', 'type_id');
}
This outputs the collection, but they are not grouped correctly. I need to loop these into select fields side by side as entered in the pivot table, so a user can select a "type" and the "distance", but the order is off when using the collection. The raw query above outputs correctly.
Hotels::where('id','=','200')->with('distance', 'type')->take(5)->get();
Ok Solved it. So apparently you can use orderBy on your pivot table. Incase anyone else has this issue this is what I did on both relationships.
public function distance() {
return $this->belongsToMany('Distance', 'amenities', 'hotel_id', 'distance_id')->withPivot('id')->orderBy('pivot_id','desc');
}
public function type() {
return $this->belongsToMany('Type', 'amenities', 'hotel_id', 'type_id')->withPivot('id')->orderBy('pivot_id','desc');
}
It's not really a great practice to include other query building steps in the relationship methods on your models. The relationship method should just define the relationship, nothing else. A cleaner method is to apply eager load constraints. (scroll down a bit) Consider the following.
Hotels::where('id', 200)->with(array(
'distance' => function ($query)
{
$query->withPivot('id')->orderBy('pivot_id','desc');
},
'type' => function ($query)
{
$query->withPivot('id')->orderBy('pivot_id','desc');
},
))->take(5)->get();
If you find that you are eagerly loading this relationship in this way often, consider using scopes to keep things DRY. The end result will allow you to do something like this.
Hotels::where('id', 200)->withOrderedDistance()->withOrderedType()->take(5)->get();
P.S. Your models should be singular. Hotel, not Hotels. The model represents a single record.
Solved by using ->withPivot('id')->orderBy('pivot_id','desc');
Posted answer in the question.