Doctrine ORM - Self join without any real relation - sql

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();

Related

Postgres query - return flattened JSON

I have the following working query:
const getPromos = async (limit = 10, site: string, branch: string) => {
const query = `SELECT
json_build_object(
'id', p.id,
'description', p.description,
'discounted_price', p.discounted_price,
'items', jsonb_agg((i.id, i.price, i.title))
)
FROM promotions p
INNER JOIN promotion_items pi ON p.id = pi.promotion_id
INNER JOIN items i ON pi.item_code = i.item_code WHERE site_id = ${site} and store_id = ${branch}
GROUP BY p.id LIMIT ${limit}`;
return await db.query(query);
};
The issue is simple - each item (in this example - promotion) is returned with an object that wraps it - named json_build_object. I don't want any object to wrap my promotions - just like this:
[{id:1, .... items: [...items here...]}, {id:2, .... items: [...items here...]}]
Any idea?
You can get the desired result directly from the query when you aggregate the resultset using jsonb_agg (like items further down the query).
SELECT
jsonb_agg(jsonb_build_object(
'id', p.id,
'description', p.description,
'discounted_price', p.discounted_price,
'items', jsonb_agg((i.id, i.price, i.title))
))
--- the rest of your query

Sequelize is automatically adding a sub query within the where clause. Is there a way to make it skip adding the where clause

I have a Sequelize query that uses INNER JOINS. The issue is that sequelize is internally adding another where clause with a sub-query on the child table. That is eating up the query performance. Below are an examples of my code and the raw query output.
Is there a way to make sequelize skip adding this where clause?
Sequelize version: 6.x
PostModel.findAll({
where: {
id: 1,
},
include: [
{
model: CommentsModel,
required: true,
}
]
})
The query builds an SQL query as below.
SELECT "post".*
FROM (SELECT "post"."*"
FROM "posts" AS "post"
WHERE "post"."id" = 2
AND (SELECT "post_id"
FROM "comments" AS "c"
WHERE "comments"."post_id" = "post"."id" AND ("c"."text_search" ## 'who:*')) IS NOT NULL
ORDER BY "post"."id" DESC
LIMIT 50 OFFSET 0) AS "post"
LEFT OUTER JOIN "post_tags" AS "tags" ON "post"."id" = "tags"."post_id"
LEFT OUTER JOIN "tag" AS "tags->tag" ON "tags"."tag_id" = "tags->tag"."id"
INNER JOIN "comments" AS "c" ON "post"."id" = "c"."post_id" AND ("c"."text_search" ## 'who:*')
ORDER BY "post"."id" DESC;
As you can see the WHERE clause has a new added
(SELECT "post_id"
FROM "comments" AS "c"
WHERE "comments"."post_id" = "post"."id" AND ("c"."text_search" ## 'who:*'))
This is basically killing the performance of the query.
After a lot research I figured out the solution.
We need to add subQuery: false within the association.
PostModel.findAll({
where: {
id: 1,
},
include: [
{
subQuery: false,
model: CommentsModel,
required: true,
}
]
})
Query output:
SELECT "post".*
FROM (SELECT "post"."*"
FROM "posts" AS "post"
WHERE "post"."id" = 2
ORDER BY "post"."id" DESC
LIMIT 50 OFFSET 0) AS "post"
LEFT OUTER JOIN "post_tags" AS "tags" ON "post"."id" = "tags"."post_id"
LEFT OUTER JOIN "tag" AS "tags->tag" ON "tags"."tag_id" = "tags->tag"."id"
INNER JOIN "comments" AS "c" ON "post"."id" = "c"."post_id" AND ("c"."text_search" ## 'who:*')
ORDER BY "post"."id" DESC;

How to write nested where condition with left join using npm Sequqlize

I am trying to implement left join query by clubbing
select product.* from products
left outer join cart as c on c.prodId = product.prodId
left outer join order as order on c.cid = p.pid
where product.secondaryId = 10 and c.cpid = 210;
How can I write, Sequelize code for this.
EDIT:
Here is the actual MYSQL query, what I am implementing to:
select products.* from products
left outer join Cart as cart on cart.CartId = products.CartId
left outer join Orders as order on order.OrderId = cart.PartId
where cart.CartId = 1 and order.CustId = 12;
Association at Sequelize end
Cart.hasMany(models.Products, {as: 'Products', foreignKey: 'CartId'})
Cart.belongsTo(models.Orders, { as: 'Orders', foreignKey: 'PartId'})
Products.belongsTo(models.Cart, {onDelete: "CASCADE", foreignKey: { name: 'CartId', allowNull: false})
Products -> PrimaryKey: ProduceId
Cart -> PrimaryKey: CartId
Orders -> PrimaryKey: OrderId
First of all left outer join cart as c on c.prodId = product.prodId will work as a usual join because of c.cpid = 210 condition. Either remove it or move it to on clause like this:
left outer join cart as c on c.prodId = product.prodId and c.cpid = 210
As of Sequelize query because you didn't show your model definitions and associations I'll try to guess and write like this (so you can get the whole idea):
const products = await db.Products.findAll({
where: {
secondaryId: 10
},
include: [{
model: db.Cart,
required: false, // this is LEFT OUTER JOIN
where: {
cpid: 210
},
include: [{
model: db.Order,
as: 'Orders',
required: false // this is LEFT OUTER JOIN
}]
}]
})
If you indicate your model definitions and associations then I can correct my answer as well (if necessary).

How to do a SQL JOIN where multiple joined rows need to contain things

When someone on my site search for an image that has multiple tags I need to query and find all images that have the searched tags, but can't seem to figure out this query.
I have an Images table.
The Images table has a relation to Posts_Images.
Posts_Images would have a relation to Posts table.
Posts has a relation to Posts_Tags table.
Posts_Tags table will have the relations to Tags table.
The query I have so far:
SELECT "images".* FROM "images"
INNER JOIN "posts_images" ON posts_images.image_id = images.id
INNER JOIN "posts" ON posts.id = posts_images.post_id AND posts.state IS NULL
INNER JOIN "posts_tags" ON posts_tags.post_id = posts.id
INNER JOIN "tags" ON posts_tags.tag_id = tags.id
WHERE (("images"."order"=0) AND ("images"."has_processed"=TRUE)) AND (LOWER(tags.tag)='comic') AND ("tags"."tag" ILIKE '%Fallout%') ORDER BY "date_uploaded" DESC LIMIT 30
It gets no results, it is checking if the tags equals both values, but I want to see if any of the tags that were joined have all the values I need.
The desired result would be any Post that has a tag matching Comic and ILIKE '%Fallout%'
You seem to want something like this:
SELECT i.*
FROM images JOIN
posts_images pi
ON pi.image_id = i.id JOIN
posts p
ON p.id = pi.post_id AND p.state IS NULL JOIN
posts_tags pt
ON pt.post_id = p.id JOIN
tags t
ON pt.tag_id = t.id
WHERE i."order" = 0 AND
i.has_processed AND
(LOWER(t.tag) = 'comic') OR
(t.tag ILIKE '%Fallout%')
GROUP BY i.id
HAVING COUNT(DISTINCT tag) >= 2
ORDER BY date_uploaded DESC
LIMIT 30;
The logic is in the HAVING clause. I'm not 100% sure that this is exactly what you want for multiple matches.
In addition to gordon-linoff’s response - query can be described using ActiveQuery:
Images::find()
->alias('i')
->joinWith([
'postsImages pi',
'postsImages.posts p',
'postsImages.posts.tags t'
])
->where([
'p.state' => null,
'i.order' => 0,
'i.has_processed' => true,
])
->andWhere(
'or'
'LOWER(t.tag) = "comic"',
['like', 't.tag', 'Fallout']
])
->groupBy('id')
->having('COUNT(DISTINCT tag) >= 2')
->orderBy('date_uploaded DESC')
->limit(30)
->all()
$images = Images::find()
->innerJoin('posts_images', 'posts_images.image_id = images.id')
->innerJoin('posts', 'posts.id = posts_images.post_id AND posts.state IS NULL')
->where(['images.order' => 0, 'images.has_processed' => true]);
if (!is_null($query)) {
$tags = explode(',', $query);
$images = $images
->innerJoin('posts_tags', 'posts_tags.post_id = posts.id')
->innerJoin('tags', 'posts_tags.tag_id = tags.id');
$tagsQuery = ['OR'];
foreach ($tags as $tag) {
$tag = trim(htmlentities($tag));
if (strtolower($tag) == 'comic') {
$tagsQuery[] = ['tags.tag' => $tag];
} else {
$tagsQuery[] = [
'ILIKE',
'tags.tag', $tag
];
}
}
if (!empty($tagsQuery)) {
$images = $images->andWhere($tagsQuery)
->having('COUNT(DISTINCT tags.tag) >= ' . sizeof($tags));
}
}
$images = $images
->groupBy('images.id')
->orderBy(['date_uploaded' => SORT_DESC])
->offset($offset)
->limit($count);
return $images->all();

Doctrine - Access metadata saved on linking table

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.