How to control sql join order in Yii CDbCriteria "with" - mysql

I have the following criteria for a findAll statement
$with=array(
'tumor'=>array(
'select'=>false,
'joinType'=>'INNER JOIN',
),
'tumorLibraryType'=>array(
'select'=>false,
'joinType'=>'INNER JOIN',
'condition'=>'tumorLibraryType.id = 1 OR tumorLibraryType.id = 6',
),
'tumorPatient'=>array(
'select'=>false,
'joinType'=>'INNER JOIN',
)
);
$libPairs=LibraryPairs::model()->with($with)->findAll();
These are the relevant model relations:
'tumor' => array(self::BELONGS_TO, 'Libraries', array('tumor_library'=>'id')),
'normal' => array(self::BELONGS_TO, 'Libraries', array('normal_library'=>'id')),
// making separate AR routes for tumor and normal. only tumor used currently
'tumorLibraryType'=>array(self::HAS_ONE,'LibraryTypes','','on'=>'tumor.library_type_id = tumorLibraryType.id'),
'tumorLibrariesIsolates'=>array(self::HAS_MANY,'LibrariesIsolates',array('id'=>'library_id'),'through'=>'tumor'),
'tumorSamplesIsolates'=>array(self::HAS_MANY,'SamplesIsolates',array('isolate_id'=>'isolate_id'),'through'=>'tumorLibrariesIsolates'),
'tumorSamples'=>array(self::HAS_MANY,'Samples',array('sample_id'=>'id'),'through'=>'tumorSamplesIsolates'),
'tumorPatient'=>array(self::HAS_ONE,'Patients',array('patient_id'=>'id'),'through'=>'tumorSamples'),
This code generates the following sql:
SELECT `t`.`tumor_library` AS `t0_c0`, `t`.`normal_library` AS `t0_c1`, `t`.`created` AS `t0_c2`, `t`.`created_by` AS `t0_c3`, `t`.`last_modified` AS `t0_c4`, `t`.`last_modified_by` AS `t0_c5`, `tumor`.`library_type_id` AS `t1_c2`, `tumor`.`id` AS `t1_c0`
FROM `library_tumor_normal_pairs` `t`
INNER JOIN `library_types` `tumorLibraryType` ON (tumor.library_type_id = tumorLibraryType.id)
INNER JOIN `libraries` `tumor` ON (`t`.`tumor_library`=`tumor`.`id`)
LEFT OUTER JOIN `libraries_isolates` `tumorLibrariesIsolates` ON (`tumor`.`id`=`tumorLibrariesIsolates`.`library_id`)
LEFT OUTER JOIN `samples_isolates` `tumorSamplesIsolates` ON (`tumorLibrariesIsolates`.`isolate_id`=`tumorSamplesIsolates`.`isolate_id`)
LEFT OUTER JOIN `samples` `tumorSamples` ON (`tumorSamplesIsolates`.`sample_id`=`tumorSamples`.`id`)
INNER JOIN `patients` `tumorPatient` ON (`tumorSamples`.`patient_id`=`tumorPatient`.`id`)
WHERE (tumorLibraryType.id = 1 OR tumorLibraryType.id = 6)
But that sql throws an error:
"Column not found: 1054 Unknown column 'tumor.library_type_id' in 'on clause'. "
However if I simply move the tumor line in the sql query up to be the first join statement, and run the query manually, then the query works.
SELECT `t`.`tumor_library` AS `t0_c0`, `t`.`normal_library` AS `t0_c1`, `t`.`created` AS `t0_c2`, `t`.`created_by` AS `t0_c3`, `t`.`last_modified` AS `t0_c4`, `t`.`last_modified_by` AS `t0_c5`, `tumor`.`library_type_id` AS `t1_c2`, `tumor`.`id` AS `t1_c0`
FROM `library_tumor_normal_pairs` `t`
INNER JOIN `libraries` `tumor` ON (`t`.`tumor_library`=`tumor`.`id`)
INNER JOIN `library_types` `tumorLibraryType` ON (tumor.library_type_id = tumorLibraryType.id)
LEFT OUTER JOIN `libraries_isolates` `tumorLibrariesIsolates` ON (`tumor`.`id`=`tumorLibrariesIsolates`.`library_id`)
LEFT OUTER JOIN `samples_isolates` `tumorSamplesIsolates` ON (`tumorLibrariesIsolates`.`isolate_id`=`tumorSamplesIsolates`.`isolate_id`)
LEFT OUTER JOIN `samples` `tumorSamples` ON (`tumorSamplesIsolates`.`sample_id`=`tumorSamples`.`id`)
INNER JOIN `patients` `tumorPatient` ON (`tumorSamples`.`patient_id`=`tumorPatient`.`id`)
WHERE (tumorLibraryType.id = 1 OR tumorLibraryType.id = 6)
So my question is, how can I control the sql join order of "with" criteria in Yii? Is it possible? As you can see my 'with' array and relations have 'tumor' before the others, but the join order is not preserved.

I encountered a similar problem: Yii generates joins in such order that makes SQL statement invalid. This situation occurs, for example, when you try to write custom $CDBCriteria->join which relies on tables specified in relations passed by $CDBCriteria->with. This happens because join is processed in CJoinQuery::__constructor whereas all "standard" joins (from with) are generated by Yii in CJoinQuery::join, that is after the constructor.
Unfortunately there is no solution other than a patch. Here is an example of how I did it my copy of Yii (the code is provided "as is" - please, check if it's applicable for your case).
First, we need to add a field into CDbCriteria, which will switch on/off a new option.
CDbCriteria.php
class CDbCriteria extends CComponent
{
...
/**
* #var string how to join with other tables. This refers to the JOIN clause in an SQL statement.
* For example, <code>'LEFT JOIN users ON users.id=authorID'</code>.
*/
public $join='';
/**
* Patch begins:
*/
public $joinreorder = false; // new option
...
Second, we need to extend CJoinQuery (please, note, it's in CActiveFinder.php):
CActiveFinder.php
class CJoinQuery
{
...
/**
* #var array list of join element IDs (id=>true)
*/
public $elements=array();
/**
* Patch begins:
*/
private $joinreorder = false; // the same new option
private $postjoins; // the variable to store our custom joins
...
Now we can alter the CJoinQuery constructor:
CActiveFinder.php (continuation)
public function __construct($joinElement,$criteria=null)
{
if($criteria!==null)
{
$this->joinreorder = $criteria->joinreorder;
$this->selects[]=$joinElement->getColumnSelect($criteria->select);
$this->joins[]=$joinElement->getTableNameWithAlias();
if($this->joinreorder) //
{ //
$this->postjoins=$criteria->join; // new lines
} //
else //
{ //
$this->joins[]=$criteria->join;
} //
$this->conditions[]=$criteria->condition;
$this->orders[]=$criteria->order;
If joinreorder is true we store custom joins in our new member variable postjoins. Otherwise, all should work as before.
And now the actual fix in CJoinQuery::createCommand:
CActiveFinder.php (continuation)
public function createCommand($builder)
{
$sql=($this->distinct ? 'SELECT DISTINCT ':'SELECT ') . implode(', ',$this->selects);
$sql.=' FROM ' . implode(' ',$this->joins);
if($this->joinreorder) //
{ //
$sql .= $this->postjoins; // new lines
} //
...
Finally we add the custom joins into SQL statement, and they are appended (not prepended as in standard implementation) to other joins generated from Yii's relations.
Now we can use it like so:
$criteria = new CDbCriteria;
$criteria->joinreorder = true;
$criteria->with = array('product', 'shop');
$criteria->join = 'LEFT OUTER JOIN `shop2address` `s2a` ON (`shop`.`id` = `s2a`.`shop_id`)';
Without joinreorder = true this generates the error stating that shop.id is unknown column in ON clause, bacause the 'shop' table is not yet added into SQL-statement. With joinreorder = true it works as expected.
As for the cases when only with is used, and incorrect sequence of joins is generated, one should apply more complicated patch. It involves CJoinQuery::join method. It should, possibly, have an optional parameter $priority, which can be again populated via corresponding member added into CDbCriteria. Then in CJoinQuery::join change these lines:
$this->joins[$element->priority]=$element->getJoinCondition();
$this->joins[$element->priority]=$element->relation->join;
This allows for re-ordering joins in arbitrary manner according to specified priorities.

This line doesn't look correct:
'tumorLibraryType'=>array(self::HAS_ONE,'LibraryTypes','','on'=>'tumor.library_type_id = tumorLibraryType.id'),
Maybe it should be
'tumorLibraryType'=>array(self::HAS_ONE,'LibraryTypes',array('id'=>'library_type_id'),'through'=>'tumor'),

guys, I believe I'm late to the party
I had similar problem
I've criteria with merges:
$criteria = new CDbCriteria();
$criteria->with = [
'codebaseName' => [
'alias' => 'cn'
],
'codebaseProducer' => [
'alias' => 'cp'
],
'registrationDocumentLast' => [
'alias' =>'rdl'
]
];
It ended up in such order by statement:
ORDER BY COALESCE(cn.name_our,cn.name_supplier), id DESC LIMIT 50
I didn't specify ordering by id DESC explicitly!
After playing for around, I discovered that it came from relation registrationDocumentLast , which was defined as
'registrationDocumentLast' => [
self::HAS_ONE, RegistrationDocument::class, 'codebase_product_pharm_id',
'joinType' => 'LEFT JOIN',
'order' => 'id DESC'
],
Look at order key. Changing it from
'order' => 'id DESC'
to
'order' => 't.id DESC'
solved the problem

Related

how to optimize mysql query in phalcon

i used this query:
$brands = TblBrand::find(array("id In (Select p.brand_id From EShop\\Models\\TblProduct as p Where p.id In (Select cp.product_id From EShop\\Models\\TblProductCategory as cp Where cp.group_id_1='$id'))", "order" => "title_fa asc"));
if($brands != null and count($brands) > 0)
{
foreach($brands as $brand)
{
$brandInProductCategory[$id][] = array
(
"id" => $brand->getId(),
"title_fa" => $brand->getTitleFa(),
"title_en" => $brand->getTitleEn()
);
}
}
TblBrand => 110 records
TblProduct => 2000 records
TblProductCategory => 2500 records
when i used this code, my site donot show and loading page very long time ...
but when i remove this code, my site show.
how to solve this problem?
The issue is your query. You are using the IN statement in a nested format, and that is always going to be slower than anything else. MySQL will need to first evaluate what is in the IN statement, return that and then do it all over again for the next level of records.
Try simplifying your query. Something like this:
SELECT *
FROM Brands
INNER JOIN Products ON Brand.id = Products.brand_id
INNER JOIN ProductCategory ON ProductCategory.product_id = Products.id
WHERE ProductCategory.group_id_1 = $id
To achieve the above, you can either use the Query Builder and get the results that way
https://docs.phalconphp.com/en/latest/api/Phalcon_Mvc_Model_Query_Builder.html
or if you have set up relationships in your models between brands, products and product categories, you can use that.
https://docs.phalconphp.com/en/latest/reference/model-relationships.html
example:
$Brands = Brands::query()
->innerJoin("Products", "Products.brand_id = Brand.id")
->innerJoin("ProductCategory", "ProductCategory.product_id = Products.id")
->where("ProductCategory.group_id_1 = :group_id:")
->bind(["group_id" => $id])
->cache(["key" => __METHOD__.$id] // if defined modelCache as DI service
->execute();
$brandInProductCategory[$id] = [];
foreach($Brands AS $Brand) {
array_push($brandInProductCategory[$id], [
"id" => $Brand->getId(),
"title_fa" => $Brand->getTitleFa(),
"title_en" => $Brand->getTitleEn()
]);
}

Need help building sql query with join and where on many to many relationship

I have two objects that are related by a many to many relationship. These two objects are Video and Category. A video can have many categories and vice versa. What I want to do, is that if I have a category, to retrieve all the videos that have this category. Also, I have a Section object which has a oneToMany relationship with Video, that is a section contains many videos. What I'm trying to do right now is the following:
public function findAllBySectionAndCategory($section, $category) {
$query = $this->getEntityManager()
->createQuery(
'SELECT v FROM OSCMySportBundle:Video v WHERE v.section = :section AND :category IN v.categories'
)
->setParameter('section', $section)
->setParameter('category', $category);
return $query->getResult();
}
When I try to test this query, I get the following error:
[Syntax Error] line 0, col 83: Error: Expected Doctrine\ORM\Query\Lexer::T_OPEN_PARENTHESIS, got 'v'
It probably means that it is looking for an array but it found a v. I know this is probably a simple question but I can't figure it out.
Edit 1:
I was able to modify it the following way so that I can do a JOIN:
'SELECT v FROM OSCMySportBundle:Video v JOIN v.categories category WHERE category.id = :category '
However, I also need to query the videos based on the section, so I did the following:
'SELECT v FROM (SELECT a FROM OSCMySportBundle:Video a WHERE a.section = :section) JOIN v.categories category WHERE category.id = :category '
But got the following error:
[Semantical Error] line 0, col 14 near '(SELECT a FROM': Error: Class '(' is not defined.
It looks like I cannot make a subquery ?
The Doctrine IN function expects a format of (1, 2, 3, 4, ...) after the IN statement. Unfortunately, it is not meant for column conditionals to prove membership.
However, I believe you're looking for the MEMBER OF Doctrine function:
public function findAllBySectionAndCategory($section, $category) {
$query = $this->getEntityManager()
->createQuery(
'SELECT v FROM OSCMySportBundle:Video v WHERE v.section = :section AND :category MEMBER OF v.categories'
)
->setParameter('section', $section)
->setParameter('category', $category);
return $query->getResult();
}
You can pass a valid Doctrine object or the identifier into $category using this functionality.
The example for this is hidden deep in the Doctrine docs:
$query = $em->createQuery('SELECT u.id FROM CmsUser u WHERE :groupId MEMBER OF u.groups');
$query->setParameter('groupId', $group);
$ids = $query->getResult();

DB Selecting from multiple table with join using alias in Yii2

I am trying to do a check against 3 table that I join together. I do not want to use the real table name hard coded as my project is highly under develop and table prefix may be changed. What is the best way in Yii2 to select from 3 table where I have where statement on the joined table?
I can get what I want from the code below. But as I said, I do not want to use the table alias hard coded. Any idea how to fix this or suggestion of other ideas would be very appreciated.
$userId = Yii::$app->user->id;
$result = \app\models\UserPermission::find()->joinWith([
'permission',
'permission.service'
])->where([
'prefix_user_permission.user_id' => $userId,
'prefix_permission.flag' => Permission::LOGIN,
'prefix_service.login_available' => Service::LOGIN_AVAIABLE,
])->all();
I would like to end up with this query:
SELECT *
FROM `prefix_user_permission` `up`
INNER JOIN `prefix_permission` `p` ON `up`.`permission_id` = `p`.`id`
INNER JOIN `prefix_service` `s` ON `p`.`service_id` = `s`.`id`
WHERE (`up`.`user_id`=43)
AND (`p`.`flag`='LOGIN')
AND (`s`.`login_available`=1);
The table prefix can be configured using the 'tablePrefix' param along with the main db config as follows:
'components' => [
'db' => [
//other db config params
'tablePrefix' => 'pre_'
]
This prefix can be used as follows:
There's a special variant on this syntax specific to tablenames: {{%Y}} automatically appends the application's table prefix to the provided value, if a table prefix has been set:
$sql = "SELECT COUNT([[$column]]) FROM {{%table}}";
$rowCount = $connection->createCommand($sql)->queryScalar();
Or if you are using active record for models then you can also use the tableName() function to replace the hard-coded table names.

MYSQL fetch records from table 1 that do not exist in table 2

I created a php function to fetch records from a sql table subscriptions, and I want to add a condition to mysql_query to ignore the records in table subscriptions that exists in table removed_items, here is my code;
function subscriptions_func($user_id, $limit){
$subs = array();
$sub_query = mysql_query("
SELECT `subscriptions`.`fo_id`, `subscriptions`.`for_id`, `picture`.`since`, `picture`.`user_id`, `picture`.`pic_id`
FROM `subscriptions`
LEFT JOIN `picture`
ON `subscriptions`.`fo_id` = `picture`.`user_id`
WHERE `subscriptions`.`for_id` = $user_id
AND `picture`.`since` > `subscriptions`.`timmp`
GROUP BY `subscriptions`.`fo_id`
ORDER BY MAX(`picture`.`since_id`) DESC
$limit
");
while ($sub_row = mysql_fetch_assoc($sub_query)) {
$subs [] = array(
'fo_id' => $sub_row['fo_id'],
'for_id' => $sub_row['for_id'],
'user_id' => $sub_row['user_id'],
'pic_id' => $sub_row['pic_id'],
'since' => $sub_row['since']
);
}
return $subs ;
}
My solution is to create another function to fetch the records from table removed_items and set a php condition where I call subscriptions_func() to skip/unset the records that resemble the records in subscriptions_func(), as the following
$sub = subscriptions_func($user_id);
foreach($sub as $sub){
$rmv_sub = rmv_items_func($sub[‘pic_id’]);
If($rmv_sub[‘pic_id’] != $sub[‘pic_id’]){
echo $sub[‘pic_id’];
}
}
This solution succeeded to skip the items in the table removed_items however this solution makes gaps in the array stored in the variable $sub which makes plank spots in the echoed items.
Is there a condition I can add to the function subscriptions_func() to cut all the additional conditions and checks?
Assuming id is the primary key of subscriptions and subs_id is the foreign key in removed_items, then you just have to add a condition to the WHERE clause. Something like this should work :
...
AND `subscriptions`.id NOT IN (SELECT `removed_items`.subs_id FROM `removed_items`)
...
Not related to your problem :
Your code seems vulnerable to SQL injection : use prepared statement to prevent this.
The original Mysql API is deprecated, it is highly recommended to switch to Mysqli instead.

Rails ActiveRecord escape variable in join clause

This query works, but is totally open to SQL injection:
products = Product.find(pids,
:select => 'products.*, P.code',
:joins => "left join product_dist_match P on
(P.pid = products.pid and P.cid = #{cid})",
)
How can I properly escape the cid variable? The conditions parameter allows the format ['foo = ?', bar] for this purpose, but joins does not.
I don't want to use find_by_sql because then I would need to add the joins and conditions which are part of the model's default scope (that would not be DRY).
Edit: My table structure is essentially this:
products: pid (primary key)
product_dist_match: pid, cid, code
customers (not used in the query): cid (primary key)
Note that this is a read-only database which Rails only has limited involvement with. I am not planning to set up models for all the tables; I just want to do a simple query as described above, without exposing myself to SQL injection attacks.
The answer I found is to use the .sanitize method on the model:
products = Product.find(pids,
:select => 'products.*, P.code',
:joins => 'left join product_dist_match P on
(P.pid = products.pid and P.cid = ' + Product.sanitize(cid) + ')',
)
If you find a better solution, please post it!
This seems to be more what you were trying to do.
products = Product.find(pids,
:select => 'products.*, P.code',
:joins => sanitize_sql_array [
'left join product_dist_match P on P.pid = products.pid and P.cid = ?',
cid
]