QQuery and custom joins

thread: 15 messages  |  last: a year ago  |  started: wednesday, october 25, 2006, 11:37 am pdt


#1  |  VexedPanda (Calgary, AB) Canada
Wednesday, October 25, 2006, 11:37 AM PDT

Ok, I've also got custom SQL where I join the same table twice (to find ancestors via Nested Sets), like the following :

Select * from user
join group as g on g.id = user.group_id
join group as parent_group on g.lft between parent_group.lft and parent_group.rgt
where parent_group.id = 1

This shows all users in a given group, and it's children groups.

How would I accomplish this using QQueries, or are custom joins unavaliable?

#2  |  Mike Ho (Sunnyvale, CA) United States of America Qcodo Administrator
Wednesday, October 25, 2006, 12:26 PM PDT

Unfortunately, custom joins currently aren't available in QQ... so you're better off running this as a manually-coded query.

#3  |  VexedPanda (Calgary, AB) Canada
Thursday, October 26, 2006, 9:13 AM PDT

Ok, planning on adding this functionality myself, so here are some questions:

If I wanted to create a QQAncestorNode class, what QQNodeBase functions would I need to override?

In order to use it once created, is the only other thing I need to do is update QQNodeGroup's __get function and add a 'Ancestors' case to return a QQAncestorNode for?

It seems like it would be nice then to have the codegen'd class be QQNodeGroupBase with QQNodeGroup defined in Group.class.php instead, so that it's functions can be overridden.

#4  |  Mike Ho (Sunnyvale, CA) United States of America Qcodo Administrator
Thursday, October 26, 2006, 9:30 AM PDT

actually, what exactly does your data model look like for user and group?
There is support for directed graphs in QQ, and while you might not be able to do a custom join condition, you could do a regular join and simply add a simple QQ::Equal condition...

#5  |  VexedPanda (Calgary, AB) Canada
Thursday, October 26, 2006, 9:49 AM PDT

Nested sets allow you to retrieve any of the following using a single query:
Ancestors, Parent, Children, Descendants
I don't think such functionality could be retrofitted into any regular joins, but I'll give you more details anyhow, in case you can come up with something.

Here's an example database similar to my real one:
group
--------
id (int) [PK]
name (varchar)
lft (int) [U]
rgt (int) [U]
level (int)

user
--------
id (int) [PK]
name (varchar)

user_group_assn
--------
user_id (int) [PK]
group_id (int) [PK]


The self-referential queries on groups rely entirely on lft and rgt (level can also be used to avoid nested selects when trying to retrieve specific levels ie: “Only Great Grand Parents”). A row with lft and rgt surrounding another row's lft and rgt it is that row's ancestor. If it's the row with the least difference between lft and rgt that surrounds another row's lft and rgt, then it's that row's parent.

More info on Nested Sets can be found at:
<http://dev.mysql.com/tech-resources/articles/hierarchical-data.html>

#6  |  VexedPanda (Calgary, AB) Canada
Thursday, October 26, 2006, 4:30 PM PDT

Ok, here's what I did, and it seems to work:

Created QQAncestorNode.class.php in includes/qcodo/qform:

<?php
    
class QQAncestorNode extends QQBaseNode {
        public function 
__construct($objParentNode) {
            
$this->objParentNode $objParentNode;
            if (
$objParentNode) {
                if (
version_compare(phpversion(), '5.1.0') == -1)
                    
$this->strRootTableName $objParentNode->__get('_RootTableName');
                else
                    
$this->strRootTableName $objParentNode->_RootTableName;
            } else
                
$this->strRootTableName $strName;
        }

        public function 
GetColumnAliasHelper(QQueryBuilder $objBuilder$strBegin$strEnd$blnExpandSelection) {
            
// Are we at the Parent Node?
            
if (is_null($this->objParentNode))
                
// Yep -- Simply return the Parent Node Name
                
return $this->strName;
            else {
                
// No -- First get the Parent Alias
                
$strParentAlias $this->objParentNode->GetColumnAliasHelper($objBuilder$strBegin$strEnd$blnExpandSelection);

                
// Next, Join the Appropriate Table
                
$objBuilder->AddJoinItem(sprintf('LEFT JOIN %s%s%s AS %s%s___ancestors_%s%s ON %s%s%s.%slft%s BETWEEN %s%s___ancestors_%s%s.%slft%s AND %s%s___ancestors_%s%s.%srgt%s'
                    
$strBegin$this->strTableName$strEnd,
                    
$strBegin$strParentAlias$this->strName$strEnd,
                    
$strBegin$strParentAlias$strEnd,
                    
$strBegin$strEnd,
                    
$strBegin$strParentAlias$this->strName$strEnd,
                    
$strBegin$strEnd,
                    
$strBegin$strParentAlias$this->strName$strEnd,
                    
$strBegin$strEnd));
                
                
// Next, Expand the Selection Fields for this Table (if applicable)
                // TODO: If/when we add assn-based attributes, possibly add selectionfields addition here?
//                if ($blnExpandSelection) {
//                    call_user_func(array($this->strClassName, 'GetSelectFields'), $objBuilder, $strParentAlias . '__' . $this->strName);
//                }

                // Return our Alias
                
return $strParentAlias '___ancestors_' $this->strName;
            }
        }
    }
?>

Made sure that file was loaded, by adding this to prepend.inc.php:

<?php
        QApplicationBase
::$PreloadedClassFile['QQAncestorNode'] = __QCODO__ '/qform/QQAncestorNode.class.php';
?>

Added a QQNodeGroupAncestors class in Group.class.php

<?php
    
class QQNodeGroupAncestors extends QQAncestorNode {
        protected 
$strType 'association';
        protected 
$strName 'groupancestors';

        protected 
$strTableName 'groups';
        protected 
$strPrimaryKey 'id';
        protected 
$strClassName 'Group';

        public function 
__get($strName) {
            switch (
$strName) {
                case 
'Id':
                    return new 
QQNode('id''integer'$this);
                case 
'Name':
                    return new 
QQNode('name''string'$this);
                case 
'Lft':
                    return new 
QQNode('lft''integer'$this);
                case 
'Rgt':
                    return new 
QQNode('rgt''integer'$this);
                case 
'Level':
                    return new 
QQNode('level''integer'$this);
                case 
'UsersForGroupMember':
                    return new 
QQNodeGroupUsersForGroupMember($this);

                case 
'Ancestors':
                    return new 
QQNodeGroupAncestors($this);

                case 
'_PrimaryKeyNode':
                    return new 
QQNode('id''integer'$this);
                default:
                    try {
                        return 
parent::__get($strName);
                    } catch (
QCallerException $objExc) {
                        
$objExc->IncrementOffset();
                        throw 
$objExc;
                    }
            }
        }
    }

?>

And finally, the annoying part, hacked GroupGen.class.php to include the Ancestor case in QQNodeGroup::__get

<?php
                
case 'Ancestors':
                    return new 
QQNodeGroupAncestors($this);
?>

What's annoying about that? It's overwritten on every codegen.

So now instead of the SQL I used in the first thread, I use:
QQ::Equal(QQN::User()->Group->Ancestors->Id, 1)

So, did I miss something, is there a way to clean that up, or did I stumble upon an acceptable way to add “ancestor” Nested Set support?

If so, I'll be going ahead and adding QQDescendantsNode, QQParentNode, and QQChildrenNode, as well as updating my templates to make “the codegen'd class be QQNodeGroupBase with QQNodeGroup defined in Group.class.php instead”.

#7  |  VexedPanda (Calgary, AB) Canada
Friday, October 27, 2006, 10:56 AM PDT

So, any feedback on this?

Specifically, is there any way to clean up the QQNodeGroupAncestors's __get function?
I'm thinking something like this, but the near-guaranteed exception bugs me:

<?php
try {
  return 
parent::__get($strName);
} catch (
QCallerException $objExc) {
  try {
    return new 
QQNodeGroup('id''integer'$this)->__get($strName);
  } catch (
QCallerException $objExc) {
    
$objExc->IncrementOffset();
    throw 
$objExc;
  }
}
?>

(Not to be impatient, I just needed something to say to cover up mistakenly posting something else. :P)

#8  |  Mike Ho (Sunnyvale, CA) United States of America Qcodo Administrator
Friday, October 27, 2006, 12:22 PM PDT

VP, i think you're on teh right track (i'm not too familiar with nested sets, so i can't say for sure).

In terms of wanting to override the nodes, themselves, in a custom subclass, I could be wrong but I believe that what you are wanting to perform is very much a corner case (not within the 80% use case scenario)...

While I do agree with you that allowing for a QQNodeGroupGen and a QQNodeGroup in the subclass makes sense from a “now you can easily customize this” perspective, there is a performance hit whenever you do this sort of subclassing.

And while I'm not 100% certain that we should NOT have it... at this point I still am hesitent to put this in by default.

The easy workaround, of course, would be to simply override the two templates (there are only two templates that need to be updated, as far as I can see) db_orm_class and db_orm_class_shell, so that it will generate QQNode”CLASSNAME”Gen and a new QQNode”CLASSNAME”.

And once that's done, you can go ahead and do your overriding/customization without altering your gen class.

#9  |  VexedPanda (Calgary, AB) Canada
Friday, October 27, 2006, 1:10 PM PDT

Yep, I already went ahead and did that, thanks. :)

Glad to hear this seems to be the right way to go about this. I think I've gotten it all working, just need to do some more testing.

#10  |  fimbulvetr (Denver, CO)
Wednesday, April 11, 2007, 9:39 AM PDT

Hi VP,

I am about to tackle a nested set application myself. Are you willing to share a diff w/ version 0.3.x or some direction? Any gotchas you eventually ran across?

Thanks for your help



Copyright © 2005 - 2012, Quasidea Development, LLC
This open-source framework for PHP is released under the terms of The MIT License.