Yii2 hasMany custom condition - php

I have sql condidtion SELECT * FROM (SELECT * FROM Prices WHERE aliasId = :aliasId order by id desc) p1 group by p1.currency and I am trying to use it in hasMany statement.
$q = $this->hasMany(Prices::className(), ['aliasId' => 'id']);
$db = \Yii::$app->db;
$query = $db
->createCommand('SELECT * FROM (SELECT * FROM Prices WHERE aliasId = :aliasId order by id desc) p1 group by p1.currency')
->bindValue(':aliasId', $this->id);
$query->prepare(true);
$q->sql = $query->getRawSql();
return $q;
But $this->id is empty when hasMany calling. Is there any way to bind custom query and link array there?
UPDATE.
I know that the reason of $this->id is empty, because I'm using Prices::find()>with('prices') in my Controller, so Yii creates query for all prices list. hasMany just adds addWhere('in', $key, $value) in empty query from $link parameter, I'm trying to override his query, but I can't.

$this->id is empty for new PriceAlias instances, and it's filled only after the model is saved in db - you are getting an empty value most likely because getPrices() is called before the model is saved in db.
You can test if $this->id != null or $this->isNewRecord == false before building the custom command, otherwise return null, an empty array or as required.
UPDATE 1: not sure I fully understand your update,
Prices::find()>with('prices') does create a WHERE ... IN (...) query, but
hasMany does not add an addWhere rule, it creates a relation for the ActiveRecord class. In your case:
$this->hasMany(Prices::className(), ['aliasId' => 'id'])
// generates: SELECT * FROM `prices` WHERE `aliasId` = :id
And the query is executed only when you specifically call getPrices() for an object.
So your problem is? after $q->sql = $query->getRawSql(); statement, $q->sql is not SELECT * FROM (SELECT * FROM Prices WHERE aliasId = :aliasId order by id desc) p1 group by p1.currency ?
UPDATE 2: I understand now. I can't think of any way of using Prices::find()->with() on relations with custom sql, at least not as the one you would like to use.
I can only suggest to find an alternative to find()->with() in your controller if you need to keep the custom query.

From official doc:
$subQuery = (new Query())->select('id')->from('user')->where('status=1');
// SELECT * FROM (SELECT `id` FROM `user` WHERE status=1) u
$query->from(['u' => $subQuery]);
In your case it should be something like this:
$subQuery = (new Query())->select('*')->from('Prices')->where('aliasId = :aliasId', ['aliasId'=>$aliasId])->orderBy('id');
$query->from(['p1' => $subQuery])->groupBy('p1.currency');

Related

Yii2 model to exclude related records

I have two related tables, posts and hidden_posts, where posts.id corresponds to hidden_posts.post_id.
In my posts model I have this relation to return a record if the post should be hidden:
public function getHiddenPosts()
{
return $this->hasOne(HiddenPost::className(), ['post_id' => 'id']);
}
Now I need to return all posts that are NOT hidden. So I am looking for the equivalent of this pseudo code:
return $this->hasNone(HiddenPost::className(), ['post_id' => 'id'])->all();
Which is saying, "show me all posts that are not in the hidden_posts table".
So does this use an outer join query or is there a statement that I can't find do do this in one line?
You can do it this way. Get all posts that are not listed in Hidden table:
$posts = Post::find()
->andFilterWhere(['not in',
'post.id',
HiddenPost::find()
->select(['hidden_post.post_id'])
->all();
In any case, it is best to proceed from the raw SQL statement. Write a statement that satisfies your results and transfer it to ActiveRecord query.
Post items could be retrieved using an inner join
$res = Post::find()
->select('post.*')
->innerJoin('hdn_post', '`post`.`id` = `hdn_post`.`post_id`')
->all();
It could be good practice using yii2 owned function instead of adding queries inside the model such as using select queries in your model.
Instead you can use ORM functions in yii2 has already done by gii inner functions created for to make u=your work easy.
Add * #property YourModel $hidden_post
and inside this model add you post_id such as ( * #property integer $post_id ) to create relation.
public function getHiddenPosts($hidden_post) {
return $this->find()->joinWith('hidden_post')
->where(['hidden_post' => $hidden_post])
->all();
}
You could retrive the Post items using an inner join
$posts = Post::find()
->select('post.*')
->innerJoin('hidden_post', '`post`.`id` = `hidden_post`.`post_id`')
->all();
for not hidden then use left join and check for null result of related table
$posts = Post::find()
->select('post.*')
->leftJoin('hidden_post', '`post`.`id` = `hidden_post`.`post_id`')
->where('ISNULL(`hidden_post`.console_id)')
->all();

Doctrine2 - How can I sort dynamicaly the results of a relation?

What I need :
I'm building an API that returns users and some relations : I have an entity called "User" which has a lot of relationships. Let's take the "comments" as example :
/**
* #ORM\OneToMany(targetEntity="Comments", mappedBy="idClient", cascade={"persist"})
*/
protected $comments;
In some cases, the client wants to get the user data and the comments data in the same query (by adding "comments" to the "include query param), and wants to sort the comments in a specific order. This order is provided by the client in the query params. In this example, the comments must be sorted by id ASC.
/api/users?include=comments&sort=comments.id
Note that order ASC is implicit in that case.
I have a search() function that build the query :
$qb = $this->createQueryBuilder($this->elementName);
/* SELECTs */
$selects = $this->getSelects($params);
foreach($selects as $select) {
$qb->addSelect($select);
}
/* WHEREs */
$wheres = $this->getWheres($params);
foreach($wheres as $where) {
$qb->andWhere($where);
}
/* ORDER BY */
foreach($sortBy as $column => $order) {
$qb->addOrderBy($column, $order);
}
/* LIMIT and OFFSET */
$qb->setFirstResult($offset)
->setMaxResults($limit);
$query = $qb->getQuery();
$results = $query->getResult();
This function is called to get the primary data of the request : the users data. Then, the users are transformed by a UserTransformer, in order to answer the client in a specific format (JSONAPI).
The relationships (as comments) are called later by querying the entity is the object transformer :
$comments = $user->getComments(); // Returning $this->comments in the User class.
return $this->collection($comments, new CommentsTransformer()); // sends the $comments data to the CommentsTransformer.
What I tried
I tried addOrderBy() to the query builder but I get an error because the DQL does not contains any association named comments.id :
Doctrine\ORM\Query\QueryException: [Semantical Error] line 0, col 110 near 'id ASC': Error: Class Foo\Users has no field or association named comments.id
Here is the DQL :
SELECT e FROM Foo\Users u WHERE [...] ORDER BY u.comments.id ASC
Is there any way I can "see" the comments properties and sort the comments on them in my Query ?
Or is there any way I can inject the sort order in my Users class so it can retrieve the comments data in that dynamical order ? like using $user->getComments($sortBy) and then catch the $sortBy in my Users class (or preferably on my entity mother class) and alter the build-in Doctrine request to add my sorting order ?
PS : sorry for (probably) bad english, it's not my mother tongue.
apply criteria in your getComments function like
use Doctrine\Common\Collections\Criteria;
public function getComments()
{
$criteria = Criteria::create()
->orderBy(['id' => Criteria::ASC]);
return $this->comments->matching($criteria);
}

Poor whereHas performance in Laravel

I want to apply a where condition to relation. Here's what I do:
Replay::whereHas('players', function ($query) {
$query->where('battletag_name', 'test');
})->limit(100);
It generates the following query:
select * from `replays`
where exists (
select * from `players`
where `replays`.`id` = `players`.`replay_id`
and `battletag_name` = 'test')
order by `id` asc
limit 100;
Which executes in 70 seconds. If I manually rewrite query like this:
select * from `replays`
where id in (
select replay_id from `players`
where `battletag_name` = 'test')
order by `id` asc
limit 100;
It executes in 0.4 seconds. Why where exists is the default behavior if it's so slow? Is there a way to generate the correct where in query with query builder or do I need to inject raw SQL? Maybe I'm doing something wrong altogether?
replays table has 4M rows, players has 40M rows, all relevant columns are indexed, dataset doesn't fit into MySQL server memory.
Update: found that the correct query can be generated as:
Replay::whereIn('id', function ($query) {
$query->select('replay_id')->from('players')->where('battletag_name', 'test');
})->limit(100);
Still have a question why exists performs so poorly and why it is the default behavior
Try this:
mpyw/eloquent-has-by-non-dependent-subquery: Convert has() and whereHas() constraints to non-dependent subqueries.
mpyw/eloquent-has-by-join: Convert has() and whereHas() constraints to join() ones for single-result relations.
Replay::hasByNonDependentSubquery('players', function ($query) {
$query->where('battletag_name', 'test');
})->limit(100);
That's all. Happy Eloquent Life!
The reason for laravel has(whereHas) sometimes slowly is that implemented with where exists syntax.
For example:
// User hasMany Post
User::has('posts')->get();
// Sql: select * from `users` where exists (select * from `posts` where `users`.`id`=`posts`.`user_id`)
The 'exists' syntax is a loop to the external table, and then queries the internal table (subQuery) every time.
However, there will be performance problems when the users table has a large amount of data, because above sql select * from 'users' where exists... unable to use index.
It can use where in instead of where exists here without damaging the structure.
// select * from `users` where exists (select * from `posts` where `users`.`id`=`posts`.`user_id`)
// =>
// select * from `users` where `id` in (select `posts`.`user_id` from `posts`)
This will greatly improve performance!
I recommend you try this package hasin, in the above example, you can use the hasin instead of the has.
// User hasMany Post
User::hasin('posts')->get();
// Sql: select * from `users` where `id` in (select `posts`.`user_id` from `posts`)
The hasin just only use where in syntax instead of where exists compared with the framework has, but everywhere else is the same, such as parameters and call mode even the code implementation, and can be used safely.
whereHas performance is poor on tables without index, put index on it and be happy!
Schema::table('category_product', function (Blueprint $table) {
$table->index(['category_id', 'product_id']);
});
This is related to the mysql not to the laravel. You can perform the same thing you wanted from the above with the both options, joins and the subqueries. Subqueries are generally much slower than joins.
Subqueries are:
less complicated
elegant
easier to understand
easier to write
logic separation
and the above facts are why ORMs like eloquent are using suquries. but there are slower! Especially when you have many rows in the database.
Join version of your query is something like this :
select * from `replays`
join `players` on `replays`.`id` = `players`.`replay_id`
and `battletag_name` = 'test'
order by `id` asc
limit 100;
but now you must change select and add group by and be careful on many other things, but why is this so it is beyond that answer. New query would be :
select replays.* from `replays`
join `players` on `replays`.`id` = `players`.`replay_id`
and `battletag_name` = 'test'
order by `id` asc
group by replays.id
limit 100;
So that are the reasons why join in more complicated.
You can write raw query in laravel, but eloquent support for join queries are not well supported, also there are no much packages that can help you with that, this one is for example : https://github.com/fico7489/laravel-eloquent-join
WhereHas() query is really as slow as lazy turtle, so I created and still using a trait that I glue to any laravel model which required a simple join requests. This trait make a scope function whereJoin(). You can just pass there a joined model class name, where clause params and enjoy. This trait take care of table names and related details in query. Well, it's for my personal use and ofc feel free to modify this monstruosity.
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
/** #mixin Model */
trait ModelJoinTrait
{
/**
* #param string|\Countable|array $on
* #param $column
* #param $whereOperator
* #param $value
* #param Model $exemplar
* #return array
*/
function _modelJoinTraitJoinPreset($on, $column, $whereOperator, $value, $exemplar){
$foreignTable = $exemplar->getTable();
$foreignId = $exemplar->getKeyName();
$localTable = $this->getTable();
$localId = $this->getKeyName();
//set up default join and condition parameters
$joinOn =[
'local' => $localTable.'.'.$localId,
'foreign'=> $foreignTable.'.'.$foreignId,
'operator' => '=',
'type'=>'inner',
'alias'=>'_joint_id',
'column'=>$column,
'where_operator'=>$whereOperator,
'value'=>$value
];
//config join parameters based on input
if(is_string($on)){
//if $on is string it treated as foreign key column name for join clause
$joinOn['foreign'] = $foreignTable.'.'.$on;
} elseif (is_countable($on)){
//if $is array or collection there can be join parameters
if(isset($on['local']) && $on['local'])
$joinOn['local'] = $localTable.'.'.$on['local'];
if(isset($on['foreign']) && $on['foreign'])
$joinOn['foreign'] = $localTable.'.'.$on['foreign'];
if(isset($on['operator']) && $on['operator'])
$joinOn['operator'] = $on['operator'];
if(isset($on['alias']) && $on['alias'])
$joinOn['alias'] = $on['alias'];
}
//define join type
$joinTypeArray = ['inner', 'left', 'right', 'cross'];
if(is_countable($on) && isset($on['type']) && in_array($on['type'], $joinTypeArray))
$joinOn = $on['type'];
return $joinOn;
}
/**
* #param Model $exemplar
* #param string|array|\Countable $joinedColumns
* #param string|array|\Countable $ownColumns
* #param string $jointIdAlias
* #return array
*/
function _modelJoinTraitSetColumns($exemplar, $joinedColumns, $ownColumns, $jointIdAlias = '_joint_id')
{
$foreignTable = $exemplar->getTable();
$foreignId = $exemplar->getKeyName();
$localTable = $this->getTable();
$localId = $this->getKeyName();
if(is_string($joinedColumns))
$foreignColumn = ["$foreignTable.$joinedColumns"];
else if(is_countable($joinedColumns)) {
$foreignColumn = array_map(function ($el) use ($foreignTable) {
return "$foreignTable.$el";
}, $joinedColumns);
} else {
$foreignColumn = ["$foreignTable.*"];
}
if(is_string($ownColumns))
$ownColumns = ["$localTable.$ownColumns"];
elseif(is_countable($ownColumns)) {
$ownColumns = array_map(function ($el) use ($localTable) {
return "$localTable.$el";
}, $ownColumns);
} else {
$ownColumns = ["$localTable.*"];
}
$columns = array_merge($foreignColumn, $ownColumns);
if($foreignId == $localId){
$columns = array_merge(["$foreignTable.$foreignId as $jointIdAlias"], $columns);
}
return $columns;
}
/**
* #param Builder $query
* #param string|array|\Countable $on
* #param Model $exemplar
*/
function _modelJoinTraitJoinPerform($query, $on, $exemplar){
$funcTable = ['left'=>'leftJoin', 'right'=>'rightJoin', 'cross'=>'crossJoin', 'inner'=>'join'];
$query->{$funcTable[$on['type']]}($exemplar->getTable(),
function(JoinClause $join) use ($exemplar, $on){
$this->_modelJoinTraitJoinCallback($join, $on);
}
);
}
function _modelJoinTraitJoinCallback(JoinClause $join, $on){
$query = $this->_modelJoinTraitJoinOn($join, $on);
$column = $on['column'];
$operator = $on['where_operator'];
$value = $on['value'];
if(is_string($column))
$query->where($column, $operator, $value);
else if(is_callable($column))
$query->where($column);
}
/**
* #param JoinClause $join
* #param array|\Countable $on
* #return JoinClause
*/
function _modelJoinTraitJoinOn(JoinClause $join, $on){
//execute join query on given parameters
return $join->on($on['local'], $on['operator'], $on['foreign']);
}
/**
* A scope function used on Eloquent models for inner join of another model. After connecting trait in target class
* just use it as ModelClass::query()->whereJoin(...). This query function forces a select() function with
* parameters $joinedColumns and $ownColumns for preventing overwrite primary key on resulting model.
* Columns of base and joined models with same name will be overwritten by base model
*
* #param Builder $query Query given by Eloquent mechanism. It's not exists in
* ModelClass::query()->whereJoin(...) function.
* #param string $class Fully-qualified class name of joined model. Should be descendant of
* Illuminate\Database\Eloquent\Model class.
* #param string|array|\Countable $on Parameter that have join parameters. If it is string, it should be foreign
* key in $class model. If it's an array or Eloquent collection, it can have five elements: 'local' - local key
* in base model, 'foreign' - foreign key in joined $class model (default values - names of respective primary keys),
* 'operator' = comparison operator ('=' by default), 'type' - 'inner', 'left', 'right' and 'cross'
* ('inner' by default) and 'alias' - alias for primary key from joined model if key name is same with key name in
* base model (by default '_joint_id')
* #param Closure|string $column Default Eloquent model::where(...) parameter that will be applied to joined model.
* #param null $operator Default Eloquent model::where(...) parameter that will be applied to joined model.
* #param null $value Default Eloquent model::where(...) parameter that will be applied to joined model.
* #param string[] $joinedColumns Columns from joined model that will be joined to resulting model
* #param string[] $ownColumns Columns from base model that will be included in resulting model
* #return Builder
* #throws \Exception
*/
public function scopeWhereJoin($query, $class, $on, $column, $operator = null, $value=null,
$joinedColumns=['*'], $ownColumns=['*']){
//try to get a fake model of class to get table name and primary key name
/** #var Model $exemplar */
try {
$exemplar = new $class;
} catch (\Exception $ex){
throw new \Exception("Cannot take out data of '$class'");
}
//preset join parameters and conditions
$joinOnArray = $this->_modelJoinTraitJoinPreset($on, $column, $operator, $value, $exemplar);
//set joined and base model columns
$selectedColumns = $this->_modelJoinTraitSetColumns($exemplar, $joinedColumns, $ownColumns, $joinOnArray['alias']);
$query->select($selectedColumns);
//perform join with set parameters;
$this->_modelJoinTraitJoinPerform($query, $joinOnArray, $exemplar);
return $query;
}
}
You can use it like this (Model Goods in example have a dedicated extended data model GoodsData with hasOne relationship between them):
$q = Goods::query();
$q->whereJoin(GoodsData::class, 'goods_id',
function ($q){ //where clause callback
$q->where('recommend', 1);
}
);
//same as previous exmple
$q->whereJoin(GoodsData::class, 'goods_id',
'recommend', 1); //where clause params
// there we have sorted columns from GoodsData model
$q->whereJoin(GoodsData::class, 'goods_id',
'recommend', 1, null, //where clause params
['recommend', 'discount']); //selected columns
//and there - sorted columns from Goods model
$q->whereJoin(GoodsData::class, 'goods_id',
'recommend', '=', 1, //where clause params
['id', 'recommend'], ['id', 'name', 'price']); //selected columns from
//joined and base model
//a bit more complex example but still same. Table names is resolved
//by trait from relevant models
$joinData = [
'type'=>'inner' // inner join `goods_data` on
'local'=>'id', // `goods`.`id`
'operator'=>'=' // =
'foreign'=>'goods_id', // `goods_data`.`goods_id`
];
$q->whereJoin(GoodsData::class, $joinData,
'recommend', '=', 1, //where clause params
['id', 'recommend'], ['id', 'name', 'price']); //selected columns
return $q->get();
Resulting SQL query will be like this
select
`goods_data`.`id` as `_joint_id`, `goods_data`.`id`, `goods_data`.`recommend`,
`goods`.`id`, `goods`.`name`, `goods`.`price` from `goods`
inner join
`goods_data`
on
`goods`.`id` = `goods_data`.`goods_id`
and
-- If callback used then this block will be a nested where clause
-- enclosed in parenthesis
(`recommend` = ? )
-- If used scalar parameters result will be like this
`recommend` = ?
-- so if you have complex queries use a callback for convenience
In your case there should be like this
$q = Replay::query();
$q->whereJoin(Player::class, 'replay_id', 'battletag_name', 'test');
//or
$q->whereJoin(Player::class, 'replay_id',
function ($q){
$q->where('battletag_name', 'test');
}
);
$q->limit(100);
To use it more efficiently, you can go like this:
// Goods.php
class Goods extends Model {
use ModelJoinTrait;
//
public function scopeWhereData($query, $column, $operator = null,
$value = null, $joinedColumns = ['*'], $ownColumns = ['*'])
{
return $query->whereJoin(
GoodsData::class, 'goods_id',
$column, $operator, $value,
$joinedColumns, $ownColumns);
}
}
// -------
// any.php
$query = Goods::whereData('goods_data_column', 1)->get();
PS I dont run any automated tests for this so be careful in use. It works just fine in my case, but there may be unexpected behaviour in yours.
I think performance does not depend on whereHas only it depends on how many records you have selected
Plus try to optimize your mysql server
https://dev.mysql.com/doc/refman/5.7/en/optimize-overview.html
and also Optimize your php server
and if you have faster query why don't you use raw query object from larval
$replay = DB::select('select * from replays where id in (
select replay_id from players where battletag_name = ?)
order by id asc limit 100', ['test']
);
You can use left join
$replies = Replay::orderBy('replays.id')
->leftJoin('players', function ($join) {
$join->on('replays.id', '=', 'players.replay_id');
})
->take(100)
->get();

SQL exists in Laravel 5 query builder

Good morning,
I've been trying for quite a lot of time to translate this query(which returns an array of stdClass) into query builder so I could get objects back as Eloquent models.
This is how the query looks like untranslated:
$anketa = DB::select( DB::raw("SELECT *
FROM v_anketa a
WHERE not exists (select 1 from user_poeni where anketa_id=a.id and user_id = :lv_id_user)
Order by redni_broj limit 1"
), array( 'lv_id_user' => $id_user,
));
I have tried this, but it gives a syntax error near the inner from in the subquery:
$anketa = V_anketa::selectRaw("WHERE not exists (select 1 from user_poeni where anketa_id=a.id and user_id = :lv_id_user)", array('lv_id_user' => $id_user,)
)->orderBy('redni_broj')->take(1)->first();
The problem is this exists and a subquery in it. I couldn't find anything regarding this special case.
Assume each table has an appropriate Eloquent model.
V_anketa is a view. The db is postgresql.
As far as the query goes I believe this should work:
$anketa = V_anketa::whereNotExists(function ($query) use ($id_user) {
$query->select(DB::raw(1))
->from('user_poeni')
->where('anketa.id', '=', 'a.id')
->where('user_id', '=', $id_user);
})
->orderBy('redni_broj')
->first();
but I'm not clear on what do you mean by "assuming every table has an Eloquent model" and "V_anketa" is a view...
Assuming the SQL query is correct, this should work:
$anketa = DB::select(sprintf('SELECT * FROM v_anketa a WHERE NOT EXISTS (SELECT 1 FROM user_poeni WHERE anketa_id = a.id AND user_id = %s) ORDER BY redni_broj LIMIT 1', $id_user));
If you want to get back an Builder instance you need to specify the table:
$anketa = DB::table('')->select('');
If you however, want to get an Eloquent Model instance, for example to use relations, you need to use Eloquent.

How to write query to count the foreign key in yii using CDbCriteria?

I have tables Ads with column AdsId,ClientId and table Assigned . Assigned has AdsId as foreign key. I want to count the number of AdsId in Assigned for given ClientId. I have the following query which works but I am not sure if its the best way. How can I run the same query in yii? Hope I made my question clear.
SELECT A. * , (
SELECT COUNT( B.AdsId )
FROM Assigned AS B
WHERE B.AdsId = A.AdsId
AND A.ClientId =1
)
FROM Ads AS A
WHERE A.ClientId =1
LIMIT 0 , 30
First declare a relation in your model relations function
public function relations(){
return array(
...
'AdsIdCount'=> array(self::STAT,'Ads','AdsId'),
//'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...additional options)
);
}
then in your in returned activeRecord results you can just call this count like this
$result = Assigned::model()->find($criteria);
$result->adsIdCount ; // do logic here
or
$results = Assigned::model()->findAll($criteria);
foreach ($results as $result){
$result->adsIdCount ; // do logic here
}
see STAT relation for more details on how this works http://www.yiiframework.com/doc/guide/1.1/en/database.arr#statistical-query

Categories