Doctrine - Access metadata saved on linking table - orm

I have 3 tables:
# schema.yml
Author:
connection: store-rw-library
tableName: lib_author
actAs: { Timestampable: ~ }
columns:
id:
type: integer(4)
unsigned: 1
primary: true
autoincrement: true
name:
type: string(50)
notnull: true
Book:
connection: store-rw-library
tableName: lib_book
actAs: { Timestampable: ~ }
columns:
id:
type: integer(4)
unsigned: 1
primary: true
autoincrement: true
name:
type: string(50)
notnull: true
relations:
Author:
class: Author
foreignAlias: Books
refClass: LinkingAuthorBook
LinkingAuthorBook:
connection: store-rw-library
tableName: lib_linking_author_book
columns:
author_id:
type: integer(4)
unsigned: 1
primary: true
book_id:
type: integer(4)
unsigned: 1
primary: true
created_at:
type: timestamp(25)
notnull: true
relations:
Author:
foreignAlias: AuthorBooks
Book:
foreignAlias: AuthorBooks
Per the notes via Doctrine docs, I established the relationships as M:M between Author and Book using the LinkingAuthorBook table.
Now, I am trying to get all the books authored by a specific author, in one query, something like:
class AuthorTable
{
public function getAuthor($id)
{
$q = Doctrine_Query::create()
->select('a.id AS author_id')
->addSelect('a.name AS author_name')
->addSelect('b.AuthorBooks')
->from('Author a')
->innerJoin('a.Books b')
->where('a.id = ?', $id);
$result = $q->fetchArray();
}
}
The resulting query from the above DQL construct:
SELECT
m.id AS m__id,
m.name AS m__1,
m2.id AS m2__id,
m2.name AS m2__1,
m.id AS m__0,
m.name AS m__1
FROM
lib_author m
INNER JOIN
lib_linking_author_book m3 ON (m.id = m3.author_id)
INNER JOIN
lib_book m2 ON m2.id = m3.book_id
WHERE
(m.id = '163')
From the above query, I see that it is correctly doing the joins, but how do I access the LinkingAuthorBook.created_at metadata column, established in my schema.yml file?
The only way I was able to get access to the metadata column was by adding an explicit innerJoin to LinkingAuthorBook (with associated book_link alias), but this resulted in another join in the resulting SQL. Which doesn't make sense because it has access to the data it needs from the original query.
-- Update (3.7.2012) --
The problem is still occurring, if I setup a loop to iterate over all the books belonging to an author, I cannot combine the meta data from the LinkingAuthorBook table or the Book table without forcing another query.
Updated query:
$q = Doctrine_Query::create()
->from('Author a')
->innerJoin('a.Books b')
->innerJoin('b.LinkingAuthorBook ab ON a.id = ab.author_id AND b.id = ab.book_id')
->where('a.id = ?', $id);
Example loop from LinkingAuthorBook -> Book:
foreach ($author->getLinkingAuthorBook() as $link) {
// Works fine, no extra query
var_dump($link->getCreatedAt());
// Forces an extra query, even though 'name' is the column name
// So even though doctrine uses lazy-load, it should be able to
// get this data from the original query
var_dump($link->getBook()->getName());
}
And the same for the following loop of Book -> LinkingAuthorBook:
foreach ($author->getBook() as $book) {
// Forces extra query
var_dump($book->getLinkingAuthorBook()->getCreatedAt());
// No extra query
var_dump($book->getName());
}
My work around:
class Book
{
public $meta;
}
class AuthorTable
{
public function getAuthor($id)
{
$q = Doctrine_Query::create()
->from('Author a')
->innerJoin('a.Books b')
->innerJoin('b.LinkingAuthorBook ab ON a.id = ab.author_id AND b.id = ab.book_id')
->where('a.id = ?', $id);
$author = $q->fetchOne();
// Manually hydrate Book Meta
foreach ($author->getLinkingAuthorBook() as $authorBook) {
$authorBooks[$authorBook->getId()] = $authorBook;
}
foreach ($author->getBook() as $book) {
$book->meta = $authorBooks[$book->getId()];
}
}
}
So now, I can iterate over books, with meta, without forcing an extra query:
foreach ($author->getBook() as $book) {
// No extra query
var_dump($book->meta->getCreatedAt());
// No extra query
var_dump($book->getName());
}
-- Update (3.8.2012) --
So I was able to prove that in the following code:
foreach ($author->getBooks() as $book)
{
echo $book->getName().'- '.$book->LinkingAuthorBook[0]->getCreatedAt().'<br/>';
}
Each iteration caused a new query to be issued in order to get the createdAt value. I did this by issuing the following commands in MySQL:
mysql> set global log_output = 'FILE';
mysql> set global general_log = 'ON';
mysql> set global general_log_file = '/var/log/mysql/queries.log';
Then I tailed /var/log/mysql/queries.log and was able to see the additional queries being generated. So, as far as I can tell, manually hydrating the Book::Meta object after initial query, is the only way to access the metadata without having to issue another query.

My suggestion:
//one query for fetching an Author, his books and created_at for the each book
$q = Doctrine_Query::create()
->from('Author a')
->innerJoin('a.Books b')
->innerJoin('b.LinkingAuthorBook ab ON a.id = ab.author_id AND b.id = ab.book_id')
->where('a.id = ?', 1);
$author = $q->fetchOne();
echo 'Author: '.$author->getName().'<br/>';
echo 'Books: <br/>';
foreach ($author->getBooks() as $book)
{
echo $book->getName().'- '.$book->LinkingAuthorBook[0]->getCreatedAt().'<br/>';
}
Only one query you realy need.

Related

Dynamic column search in multiple tables with gorm golang

My scenario is i have a grid with search option where user can select the column and can do the search, the grid data is coming from various tables. I have attached a sample screen of grid.
User Screen
So i'm trying to create a dynamic query for search but the problem is i can able to search only in main table (schema.Robot) not in Preload tables. whenever i trying to search data data from Preload tables let say from RobotModel table that time getting below error
pq: missing FROM-clause entry for table "robot_models"
Here is my go code
func (r *RobotsRepository) GetRobotsSummary(listParams viewmodel.ListParams, companyID uint) ([]*schema.Robot, int, error) {
mrobots := []*schema.Robot{}
var count int
var order string
if listParams.SortColumn == "" {
listParams.SortColumn = "id"
listParams.SortOrder = 1
} else {
listParams.SortColumn = util.Underscore(listParams.SortColumn)
}
if listParams.SortOrder == 0 {
order = "ASC"
} else {
order = "DESC"
}
var searchQuery string
if listParams.SearchText != "" {
switch listParams.SearchColumn {
case "Robot":
listParams.SearchColumn = "name"
case "Model":
listParams.SearchColumn = "robot_models.name"
}
searchQuery = listParams.SearchColumn +" LIKE '%"+ listParams.SearchText +"%' and Company_ID = " + fmt.Sprint(companyID)
}else{
searchQuery = "Company_ID = " + fmt.Sprint(companyID)
}
orderBy := fmt.Sprintf("%s %s", listParams.SortColumn, order)
err := r.Conn.
Preload("RobotModel", func(db *gorm.DB) *gorm.DB {
return db.Select("ID,Name")
}).
Preload("Task", func(db *gorm.DB) *gorm.DB {
return db.Where("Task_Status in ('In-Progress','Pending')").Select("ID, Task_Status")
}).
Preload("CreatedUser", func(db *gorm.DB) *gorm.DB {
return db.Select("ID,Display_Name")
}).
Preload("UpdatedUser", func(db *gorm.DB) *gorm.DB {
return db.Select("ID,Display_Name")
}).
Where(searchQuery).
Order(orderBy).
Offset(listParams.PageSize * (listParams.PageNo - 1)).
Limit(listParams.PageSize).
Find(&mrobots).Error
r.Conn.Model(&schema.Robot{}).Where(searchQuery).Count(&count)
return mrobots, count, err
}
In searchQuery variable i'm storing my dynamic query.
My question is how can i search data for preload table columns
Here is the sql query which i'm trying to achieve using gorm
SELECT robots.id,robots.name,robot_models.name as
model_name,count(tasks.task_status) as task_on_hand,
robots.updated_at,users.user_name as updated_by
FROM rfm.robots as robots
left join rfm.tasks as tasks on tasks.robot_id = robots.id and
tasks.task_status in ('In-Progress','Pending')
left join rfm.robot_models as robot_models on robot_models.id =
robots.robot_model_id
left join rfm.users as users on users.id = robots.updated_by
WHERE robot_models.name::varchar like '%RNR%' and robots.deleted_at is null
GROUP BY robots.id,robot_models.name,users.user_name
ORDER BY task_on_hand DESC LIMIT 2 OFFSET 0
and sorry for bad English!
Even though you are preloading, you are still required to explicitly use joins when filtering and ordering on columns on other tables. Preloading is used to eagerly load the data to map into your models, not to join tables.
Chain on something like this:
.Joins("LEFT JOIN rfm.robot_models AS robot_models ON robot_models.id = robots.robot_model_id")
I'm not positive if you can use the AS keyword using this technique, but if not, it should be easy enough to adjust your query accordingly.

Grails query to filter on association and only return matching entities

I have the following 1 - M (one way) relationship:
Customer (1) -> (M) Address
I am trying to filter the addresses for a specific customer that contain certain text e.g.
def results = Customer.withCriteria {
eq "id", 995L
addresses {
ilike 'description', '%text%'
}
}
The problem is that this returns the Customer and when I in turn access the "addresses" it gives me the full list of addresses rather than the filtered list of addresses.
It's not possible for me to use Address.withCriteria as I can't access the association table from the criteria query.
I'm hoping to avoid reverting to a raw SQL query as this would mean not being able to use a lot functionality that's in place to build up criteria queries in a flexible and reusable manner.
Would love to hear any thoughts ...
I believe the reason for the different behavior in 2.1 is documented here
Specifically this point:
The previous default of LEFT JOIN for criteria queries across associations is now INNER JOIN.
IIRC, Hibernate doesn't eagerly load associations when you use an inner join.
Looks like you can use createAlias to specify an outer join example here:
My experience with this particular issue is from experience with NHibernate, so I can't really shed more light on getting it working correctly than that. I'll happily delete this answer if it turns out to be incorrect.
Try this:
def results = Customer.createCriteria().listDistinct() {
eq('id', 995L)
addresses {
ilike('description', '%Z%')
}
}
This gives you the Customer object that has the correct id and any matching addresses, and only those addresses than match.
You could also use this query (slightly modified) to get all customers that have a matching address:
def results = Customer.createCriteria().listDistinct() {
addresses {
ilike('description', '%Z%')
}
}
results.each {c->
println "Customer " + c.name
c.addresses.each {address->
println "Address " + address.description
}
}
EDIT
Here are the domain classes and the way I added the addresses:
class Customer {
String name
static hasMany = [addresses: PostalAddress]
static constraints = {
}
}
class PostalAddress {
String description
static belongsTo = [customer: Customer]
static constraints = {
}
}
//added via Bootstrap for testing
def init = { servletContext ->
def custA = new Customer(name: 'A').save(failOnError: true)
def custB = new Customer(name: 'B').save(failOnError: true)
def custC = new Customer(name: 'C').save(failOnError: true)
def add1 = new PostalAddress(description: 'Z1', customer: custA).save(failOnError: true)
def add2 = new PostalAddress(description: 'Z2', customer: custA).save(failOnError: true)
def add3 = new PostalAddress(description: 'Z3', customer: custA).save(failOnError: true)
def add4 = new PostalAddress(description: 'W4', customer: custA).save(failOnError: true)
def add5 = new PostalAddress(description: 'W5', customer: custA).save(failOnError: true)
def add6 = new PostalAddress(description: 'W6', customer: custA).save(failOnError: true)
}
When I run this I get the following output:
Customer A
Address Z3
Address Z1
Address Z2

Grails criteria groupby object

Example grouping by name of the zones:
def result = User.createCriteria().list{
projections {
roles {
zones{
groupProperty("name")
}
}
}
}
but suppose I want to get the "id" or other attributes. the fact is that i want the object on the representing the group and not the string "name".
result.each{ println it.customFuncion() }
"zones" is a hasMany attribute and then i cant group by itself. What should be done, but doesnt works:
def result = User.createCriteria().list{
projections {
roles {
groupProperty("zones")
}
}
}
Is that possible? Thank you guys!
use hql for complex queries:
def result = User.executeQuery("select u.id, zone.name from User as u inner join u.roles as role inner join role.zones as zone group by u.id, zone.name")
Then you can access the result columns as follows:
result.each { row -> row[0] // u.id } // or access via defined column name
Imagine that i do not know your real hasMany collection names.

Django ORM equivalent

I have the following code in a view to get some of the information on the account to display. I tried for hours to get this to work via ORM but couldn't make it work. I ended up doing it in raw SQL but what I want isn't very complex. I'm certain it's possible to do with ORM.
In the end, I just want to populate the dictionary accountDetails from a couple of tables.
cursor.execute("SELECT a.hostname, a.distro, b.location FROM xenpanel_subscription a, xenpanel_hardwarenode b WHERE a.node_id = b.id AND customer_id = %s", [request.user.id])
accountDetails = {
'username': request.user.username,
'hostname': [],
'distro': [],
'location': [],
}
for row in cursor.fetchall():
accountDetails['hostname'].append(row[0])
accountDetails['distro'].append(row[1])
accountDetails['location'].append(row[2])
return render_to_response('account.html', accountDetails, context_instance=RequestContext(request))
It would be easier if you post models. But from SQL I'm assuming the models are like this:
class XenPanelSubscription(models.Model):
hostname = models.CharField()
distro = models.CharField()
node = models.ForeignKey(XenPanelHardwareNode)
customer_id = models.IntegerField()
class Meta:
db_table = u'xenpanel_subscription'
class XenPanelHardwareNode(models.Model):
location = models.CharField()
class Meta:
db_table = u'xenpanel_hardwarenode'
Based on these models:
accountDetails = XenPanelSubscription.objects.filter(customer_id = request.user.id)
for accountDetail in accountDetails:
print accountDetail.hostname, accountDetail.distro, accountDetail.node.location

Doctrine ORM - Self join without any real relation

I'm trying to find related objects to one object by matching the objects tags. I've constructed a mysql query which will return the objects that match the most by counting the matching tags.
I'm new to doctrine (1.2) so I'm wondering if someone could help me to get on the right track modifying my schema and creating a DQL query? The big problem is that the two tagset doesn't relate to each others in my schema.yml I would guess.
Schema.yml:
Object:
columns:
name:
relations:
Tags: { foreignAlias: Objects, class: Tag, refClass: Tagset}
Tagset:
columns:
object_id: {type: integer, primary: true, notnull: true}
tag_id: { type: integer, primary: true, notnull: true }
relations:
Object: { foreignAlias: Tagsets }
Tag: { foreignAlias: Tagsets }
Tag:
columns:
name: { type: string(255), notnull: true }
Object: { foreignAlias: Tags, class: Object, refClass: Tagset}
Here is the mysql query which works using the schema above:
SELECT object.name, COUNT(*) AS tag_count
FROM tagset T1
INNER JOIN tagset T2
ON T1.tag_id = T2.tag_id AND T1.object_id != T2.object_id
INNER JOIN object
ON T2.object_id = object.id
WHERE T1.object_id = 2
GROUP BY T2.object_id
ORDER BY COUNT(*) DESC
You can use subqueries as well. Something like this:
$object_id = 2;
Doctrine::getTable('Tagset')->createQuery('t')
->select('t.tag_id, o.id, o.name, COUNT(t.tag_id) AS tag_count')
->innerJoin('t.Object o WITH o.id != ?', $object_id)
->where('t.tag_id IN (SELECT t.tag_id FROM Tagset t WHERE t.object_id = ?)', $object_id)
->groupBy('t.object_id')
Solution:
$q = new Doctrine_RawSql();
$this->related_objects = $q->
select('{o.name}')->
from('tagset t1 JOIN tagset t2 ON t1.tag_id = t2.tag_id AND t1.object_id != t2.object_id JOIN object o ON t2.object_id = o.id')->
addComponent('o','Object o')->
where('t1.object_id = ?', $this->object->id)->
groupBy('t2.object_id')->
orderBy('COUNT(*) DESC')->
execute();