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.
Related
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
}
...
)
}
...
)
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 few tables that are joined through a distant relationship - for example:
A.id = B.a_id, B.id = C.b_id, C.id = D.c_id
And given A.id, I want to delete all the rows in D that are associated with A.id.
Since Model::deleteAll() does not accept any joins, only conditions, how do I go about it?
All the models (A, B, C, D) already have belongTo relationships defined.
My last resort would be raw SQL, but I would like to know if there's a way in CakePHP to do it.
I could not find similar questions as they all were about deleting ALL the associated data, rather than just one table's data via an associated key.
Use Containable behavior to find D records
public function deleteD($idA){
$this->ModelA->Behaviors->load('Containable');
$options = array(
'contain' => array(
'ModelB' => array(
'ModelC' = array(
'ModelD'
)
)
),
'conditions' => array('ModelA' => $idA)
);
$findDIds = $this->ModelA->find('all',$options);
debug($findDIds); // find right path to ModelD
$ids = Hash::extract($findDIds,'{n}.ModelD.id');
$this->loadModel('ModelD');
foreach($ids as $id){
$this->ModelD->delete($id);
}
}
Note, I not tested this function.
For example I have 3 tables:
songs(id, song_name)
song_category(id, song_id, category_id)
categories(id, name)
I want to get songs which have categories with id higher than 5. I want to do it using ORM, not with simple SQL query. Is it possible to do it with one query like this:
$songs = ORM::factory("songs")->where("category.id > 5")
No, you cannot do this with a single Kohana ORM call.
The best way I have found to do it is something like this, which makes a modification to the SQL query that the ORM will generate:
// Get the basic "song" model
$songs = ORM::factory("songs");
// Get the information about how it is connected to
// the "category" model using the `through` model
$song_relations = $results->has_many();
$category_relation = $song_relations['categories'];
$through = $category_relation['through'];
// Join on `through` model's target foreign key (far_key) and `target` model's primary key
$join_col1 = $through.'.'.$category_relation['foreign_key'];
$join_col2 = $songs->object_name().'.'.$songs->primary_key();
$songs->join($through)->on($join_col1, '=', $join_col2);
// Now, filter on the
$songs->where($through.'.'.$category_relation['far_key'], '>', 5);
$arr = $results->find_all()->as_array();
You could save some code by hardcoding the values in the join method call, but this way leverages the ORM relation definitions that you already have.
This assumes that your Song model has the following code in it:
protected $_has_many = [
'categories' => [
'model' => 'category',
'through' => 'song_category',
'foreign_key' => 'song_id',
'far_key' => 'category_id',
]
];
I have two models, Plant and Emp, that have a Has And Belongs To Many relationship. I've configured them to be associated and the query to get the data for each is correct, but the problem is Plant and Emp are on different databases. Emp is on Database 1, Plant is on Database 2. Because of this they don't query the join table properly; the join table is only on Database 1.
When the Plant model tries to access the join table it's querying Database 2, which does not have this data.
This is the association Emp has for Plant.
var $hasAndBelongsToMany = array(
'Plant' =>
array(
'className' => 'Plant',
'joinTable' => 'emp_plant',
'foreignKey' => 'employee_id',
'associationForeignKey' => 'LocationID',
'unique' => true,
'conditions' => '',
)
);
Update:I tried to set a "finderQuery" attribute to let me query the join table, but I don't know how to give a raw SQL query like that and allow it to dynamically use the id for the instance of the Model instead of a predefined value.
I can set something like
SELECT * FROM [Plant] AS [Plant] JOIN [DB].[DBO].[empplant] AS
[EmpPlant] ON ([EmpPlant].[employee_id] = **4**
AND [EmpPlant].[ID] = [Plant].[LocationID])
Which will give me the correct data for one employee, but I don't know how to make this finderQuery a dynamic query. There has to be a way for this to work.
Try
var $useDbConfig = 'alternate';
in your Model Class.
I needed to use a custom finderQuery and use the special {$__cakeID__$} identifier in place of the model ID being matched. This is a fixed version of the sample above, set as the finder query in the relationship entry for the $hasAndBelongsToMany array.
'finderQuery'=>'SELECT * FROM [Plant] AS [Plant] JOIN [DB].[DBO].[empplant] AS
[EmpPlant] ON ([EmpPlant].[employee_id] = {$__cakeID__$}
AND [EmpPlant].[ID] = [Plant].[LocationID])'
This works but if anyone knows how to fix this situation without a custom finder query (what I was trying to avoid by using associations) please post an answer and I will mark that correct instead.