I have a tree structure with a parent field. Currently I am trying to get all parent nodes to display the path to the current node.
Basically I am doing a while-loop to process all nodes.
$current = $node->getParent();
while($current) {
// do something
$current = $current->getParent();
}
Using the default findById method works. Because the entity has some aggregated fields, I am using a custom repository method, to load all basic fields with one query.
public function findNodeByIdWithMeta($id) {
return $this->getEntityManager()
->createQuery('
SELECT p, a, c, cc, ca, pp FROM
TestingNestedObjectBundle:NestedObject p
JOIN p.actions a
LEFT JOIN p.children c
LEFT JOIN c.children cc
LEFT JOIN c.actions ca
LEFT JOIN p.parent pp
WHERE p.id = :id
')
->setParameter('id', $id)
->setHint(
\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER,
'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
)
->getOneOrNullResult();
}
With that code, loading the parents fails. I only get the immediate parent (addressed by LEFT JOIN p.parent pp) but not the parents above. E.g. $node->getParent()->getParent() returns null.
Whats wrong with my code? Did I misunderstood the lazy loading thing?
Thanks a lot,
Hacksteak
It looks like your are using the adjacency model for storing trees in a relational database. Which in turn means, that you will need a join for every level to get all ancestors with a single query.
As you are already using the Doctrine Extension Library I recommend to have a look at the Tree component.
My Answer involves not using DQL and instead creating a NestedSetManager which has access to your DBAL connection so you can use SQL. I never felt like the ORM's did a good job with NestedSets query logic.
With a NestedSetManager, you can then write a bunch of clean methods and it's really simple because all these queries are well documented. See this link. Some of the method in my NestedSetManager are:
setNode();
setRoot();
loadNestedSet();
moveNodeUp();
modeNodeDown();
getRootNode();
addNodeSibling();
getNodesByDepth();
getParents();
getNodePath();
childExists();
addChildToNode();
renameNode();
deleteNode();
// And many more
You can have a ball and create a lot of create NestedSet functionality if you're not tied down by an ORM's somewhat complex functionality.
Also -- Symfony2 makes all this really really easy. You create your NestedSetManager class file and reference it in your Services.yml and pass in your Dbal connection. Mine looks like this:
services:
manager.nestedset:
class: Acme\CoreBundle\Manager\NestedSetManager
arguments: [ #database_connection ]
you can then access your nestedsets with:
$path = $this->get('manager.nestedset')->setNode(4)->getNodePath(); // in your controller
Moral of the story, ORM/NestedSets drove me bonkers and this solution work really well. If you're being forced to use DQL and have no other options, this answer probably wont be acceptable.
Related
I'm writing a Symfony application, and I am facing a problem with Doctrine entities eager loading.
I'm not sure if I can load multiple one to many collections on the same entity instance while keeping good performance.
There are many examples on the internet with people load one entity and one of its relationships.
Ex :
$user = $em->createQuery('
select a, au
from OctiviTestBundle:Article a
left join a.authorList au
where a.id = ?1')
->setParameter(1, $id)
->getOneOrNullResult();
However if I also want to load the article comments, the following request retrieves too many results (nb authors * nb comments) => combinatory explosion
$user = $em->createQuery('
select a, au
from OctiviTestBundle:Article a
left join a.authorList au
left join a.commentList c
where a.id = ?1')
->setParameter(1, $id)
->getOneOrNullResult();
In fact, I found no way to reuse an object once it has been loaded from the database. I don't know how to make a second query to load more parts of it later.
Eg :
$user = $em->createQuery('
select a, au
from OctiviTestBundle:Article a
left join a.authorList au
where a.id = ?1')
->setParameter(1, $id)
->getOneOrNullResult();
$em->LoadRelation($user, 'commentList');
$em->LoadRelation($user, 'commentList.author')
$em->LoadRelation($user, 'commentList.author.school');
... load any relation I want, while keeping only one root object.
I would like to be able to have only the main entity instance variable, eager load its 2 relationships and then go through the hierarchy.
I know I can load the two lists in different php variables, but I'd like to only pass the "$user" variable to the view template.
Do you have ideas about how to resolve this issue ?
Thanks
The only (tricky) solution I found is on this website : https://tideways.io/profiler/blog/5-doctrine-orm-performance-traps-you-should-avoid
1) Load the related entities
$companies = array_map(function($employee) {
return $employee->getCompany();
}, $employees);
$repository = $entityManager->getRepository('Acme\Company');
$repository->findBy(['id' => $companies]);
2) Don't use the result (drop the $companies variable), but now Doctrine has got the results in cache, so when I do $employee->getCompany()->getName(), it should not generate new queries.
=> Doesn't work : Doctrine doesn't put the results in the cache to reuse them later.
If the result is always going to be massive no matter what where's and ands you put in, and if you have no intent on doing any write operations on the result you should fetch them as an array instead of as the default objects.
The resulting array is still used exactly the same within twig however as you no longer have to hydrate the objects your memory saving will be huge.
I've been using the Fat-Free Framework recently, and things are going well (arguably better the longer I use it and leverage its components); however, I'm having difficulty with the ORM injecting the MySQL table name into a virtual field (used for lookup).
I know the SQL is good, and I know I could perform a second database call to retrieve the lookup field data, but since I've got things nearly working in virtual field format (and it's probably easiest to digest and debug)...
Is there any way to prevent F3 from inserting the table name during SQL generation?
Setup is easy...
class Bookmark extends \DB\SQL\Mapper
In the constructor, after the call to the parent constructor, I add my virtual fields...
$this->type_name = '
SELECT CASE bookmark_type_id
WHEN 1 THEN \'Project\'
WHEN 2 THEN \'Member\'
ELSE \'Unknown\' END
';
NOTE: This works, though NOT if I use an IF, then I get the table name injected into the IF clause -- after the first comma.
$this->description = '
SELECT CASE bookmark_type_id
WHEN 1 THEN (SELECT p.title FROM projects p WHERE p.id = reference_id)
WHEN 2 THEN (SELECT CONCAT_WS(\' \', m.first_name, m.last_name) FROM members m WHERE m.id = reference_id)
ELSE \'Unknown\' END
';
NOTE: This fails with the table name inserted after the first comma (i.e. before m.first_name).
For clarity, this is the result (notice `cm_bookmark`.):
SELECT CASE bookmark_type_id
WHEN 1 THEN (SELECT p.title FROM projects p WHERE p.id = reference_id)
WHEN 2 THEN (SELECT CONCAT_WS(' ',`cm_bookmark`. m.first_name, m.last_name) AS FullName FROM members m WHERE m.id = reference_id)
ELSE 'Unknown' END
) AS `description`
I get the feeling this is just another one of those "don't do that" situations, but any thoughts on how to achieve this in F3 would be appreciated.
(Oddly, it's only after the first comma in the subquery. If the table name insertion was consistently clever, I'd expect to see it peppered in front of m.last_name too, but it isn't.)
EDIT: It seems as though it's related to the second occurrence of something in parentheses. I've used CONCAT() in another virtual field call, and it works fine -- but it's the first (and only) use of parentheses in the field set up. If I remove the call to CONCAT_WS() and return a single field, the setup above works fine.
EDIT2: To clarify how the load is occurring, see below...
// database setup
// (DB_* set up in config.ini)
$db = new \DB\SQL($f3->get('DB_CONN'), $f3->get('DB_USER'), $f3->get('DB_PASS'));
$f3->set('DB', $db);
...
// Actual call
$db = \Base::instance()->get('DB');
$bookmark = new \CM\Models\Bookmark($db);
$bookmark->load_by_id($item['id']);
...
// in Bookmark Class (i.e. load_by_id)
$b->load(array('id=?', $id));
The only answer (to stay on this path) I have come up with so far is to create another virtual field and piece the 2 parts together later.
Not ideal, but it works.
The mapper does not allow such advanced capabilities, but I would suggest you use Cortex which luckily extends mapper so not much code change.
Below is an example:
Class Bookmark extends \DB\Cortex{
protected
$db = 'DB', // From $f3->set('DB', $db);
$table = 'bookmarks'; // your table name
/* You can also define these custom field preprocessors as a method within the class, named set_* or get_*, where * is the name of your field.*/
public function get_desciption($value) {
switch($this->bookmark_type_id){
case "1":
/*.....................Hope you get the drill*/
}
}
}
I am trying to retrieve entities from one class joined with another class.
Not all entities actually have joined entities.
It's kinda like the following statement:
SELECT a, b FROM A a LEFT JOIN B b ON a.id = b.aid GROUP BY a.id;
or in code:
$query_builder = $em->getRepository('repository_of_A')->createQueryBuilder('a');
$query_builder = $query_builder->leftJoin('a.b', b);
$query_builder = $query_builder->groupBy('a.id');
$query = $query_builder->select('a, b')->getQuery();
$entities = $query->getResult();
Now the problem is that whenever there is no entity B for A, Doctrine returns a proxy object for A.
Because I work with reflections I need the real object instead of the proxy.
In the attached screenshot the object with index 26 has no corresponding entity B for A (Shop).
Does anyone know why and how can I solve this problem?
Note: I know that I could just use the classname instead of the entity when using reflections, but I would also like to understand the problem here as it may affect the runtime...
Edit: Attached a screenshot
If the issue is that fields are not loaded, then before using reflection check if Doctrine loaded the entity and load it otherwise:
if (
$object instanceof \Doctrine\Common\Persistence\Proxy
&& !$object->__isInitialized()
) {
$object->__load();
}
// ... your code
But as I see from your screenshot, you have misidentified the issue. If you select a first (as in your example), then there will be no proxies in the result list.
As I would guess, in your example all Shop entities are in some association (not selected via query, but for example from $country->getShops();) and Shop [70] is not a proxy only because somewhere before that point Doctrine has already loaded it. If entity is in map (by ID) - it's used instead of proxy as it's already loaded.
I have upgraded to Doctrine 2.2.2 now. I have managed to successfully connect my database to my application and was able to generate proxies and repositories. I have no problem with those generation. I am just confused with regards to using the DQL of doctrine 2.2.2.
The case is this: I currently have a repository class responsible for user registration, authentication, etc. I have managed to execute the DQL on it but I just felt weird about this stuff (in my repository class).
$query = $em->createQuery("SELECT u FROM MyProject\\Entity\\AdminUsers u");
I tried also:
$query = $em->createQuery("SELECT u FROM AdminUsers u");
The last did not work but the first one works fine but it seems weird. Is it really the right way of executing DQL in doctrine 2? or am I missing something important.
NOTE: on the above declaration of this repository class is:
namespace MyProject\Repository;
use Doctrine\ORM\EntityRepository,
MyProject\Entity\AdminUsers;
It almost is the right way to do it. If you would use single quotes ', you could just use a single backslash \ instead of a double backslash \\.
Doctrine cant find out (or it would be extremely expensive to do so) which classes you imported via use statements.
But you can use a typed repository which you retrieve from the entity manager via:
$repo = $em->getRepository('MyDomain\Model\User');
$res = $repo->findSomeone();
And in the findSomeone() function you can do this:
$qb = $this->createQueryBuilder('u');
$dql = $qb->where('u.id = 1')->getDQL();
return $this->_em->createQuery($dql)->getSingleResult();
Meaning, the repository is already typed on your entity and knows which class to select from.
Some documentation:
Querying with doctrine
Querybuilder
10 step get started guide (which covers the basics including repositories)
Two models are returned as Zend_Db_Select objects.
Then, I need to join them and fetch data together at once.
class Model_User extends Abstract_Model {
public function sqlUser() {
return $this->select(array(
'user_id', 'user.name', 'user.login', 'address.street', 'city.city_id', 'city.city_name','region.region_id', 'region.region_name'
))
->joinUsing('address','address_id','') ->join('city', 'city.city_id = address.city_id', '')
->join('region', 'region.region_id = city.region_id', '');
}
}
class Model_Technics extends Abstract_Model{
public function sqlList() {
return $this->select()
// here some more sql
->joinUsing('catalog_model','model_id','');
}
}
Then I need some where else fetch sqlList with all info for every user. I dont whant to duplicate all code, I just want to join sql from User model through join
You could iterate through the "sections" of zend_db_select (columns,from,joins,where) and figure a way to append them to the other query, but I wouldn't suggest it.
An alternative might be to have a "join" method on the model you're joining to, which will take your select object and run the join method(s) on it. It would, of course, depend on the table(s) already available in the select object already. Or that method might just pass back a structure defining how to join to the join-model's table, which your primary model can decide what to do with it.
There are a ton of solutions, but there's no really easy way to just jam two zend_db_select objects together without some extra logic between them. The main missing piece would be relationship information between the two models.
I know this isn't a full blown answer (as I can't comment yet), but hopefully it will point you to a path that sounds usable.