matching associations and no associated record cakephp 3 - orm

I have an association of Price belongsTo Season
I am trying to query all prices that match a specific date range when passed in the season as well as any that have none (Prices.season_id=0)
Here is what I have:
// build the query
$query = $this->Prices->find()
->where(['product_id'=>$q['product_id']])
->contain(['Seasons']);
if(!empty($to_date) && !empty($from_date)) {
$query->matching('Seasons', function ($q) {
return $q->where([
'from_date <= ' => $to_date,
'to_date >= ' => $from_date
]);
});
}
However, this will only return Prices explicitly associated with a Season. How do I make it return Prices.season_id=0 also?

The $query->matching() call internally creates a INNER JOIN and places the where-statements of the callback-function into the ON clause of the join. For retrieving items without the association you need a LEFT JOIN. So your codesnippet would look like this:
if(!empty($to_date) && !empty($from_date)) {
$query->leftJoinWith('Seasons', function ($q){return $q;});
$query->where([[
'or' => [
'Season.id IS NULL',
[
'from_date <= ' => $to_date,
'to_date >= ' => $from_date,
],
],
]]);
}
So we create a normal INNER JOIN and place the conditions in the normal (outmost) where clause of the query.
The double array is for disambiguation of probably other where conditions with an or connection.
I myself stumbled over the column IS NULL instead of 'column' => null syntax.
PS: This works for all associations. For hasMany and belongsToMany you have to group the results with $query->group('Prices.id')

Related

Yii2 - hasMany relation with multiple columns

I have a table message_thread:
id
sender_id
recipient_id
I want to declare a relation in my User model that will fetch all message threads as follows:
SELECT *
FROM message_thread
WHERE sender_id = {user.id}
OR recipent_id = {user.id}
I have tried the following:
public function getMessageThreads()
{
return $this->hasMany(MessageThread::className(), ['sender_id' => 'id'])
->orWhere(['recipient_id' => 'id']);
}
But it generates an AND query. Does anyone know how to do this?
You cannot create regular relation in this way - Yii will not be able to map related records for eager loading, so it not supporting this. You can find some explanation int this answer and related issue on GitHub.
Depending on use case you may try two approach to get something similar:
1. Two regular relations and getter to simplify access
public function getSenderThreads() {
return $this->hasMany(MessageThread::className(), ['sender_id' => 'id']);
}
public function getRecipientThreads() {
return $this->hasMany(MessageThread::className(), ['recipient_id' => 'id']);
}
public function getMessageThreads() {
return array_merge($this->senderThreads, $this->recipientThreads);
}
In this way you have two separate relations for sender and recipient threads, so you can use them directly with joins or eager loading. But you also have getter which will return result ofboth relations, so you can access all threads by $model->messageThreads.
2. Fake relation
public function getMessageThreads()
{
$query = MessageThread::find()
->andWhere([
'or',
['sender_id' => $this->id],
['recipient_id' => $this->id],
]);
$query->multiple = true;
return $query;
}
This is not real relation. You will not be able to use it with eager loading or for joins, but it will fetch all user threads in one query and you still will be able to use it as regular active record relation - $model->getMessageThreads() will return ActiveQuery and $model->messageThreads array of models.
Why orOnCondition() will not work
orOnCondition() and andOnCondition() are for additional ON conditions which will always be appended to base relation condition using AND. So if you have relation defined like this:
$this->hasMany(MessageThread::className(), ['sender_id' => 'id'])
->orOnCondition(['recipient_id' => new Expression('id')])
->orOnCondition(['shared' => 1]);
It will generate condition like this:
sender_id = id AND (recipent_id = id OR shared = 1)
As you can see conditions defined by orOnCondition() are separated from condition from relation defined in hasMany() and they're always joined using AND.
For this query
SELECT *
FROM message_thread
WHERE sender_id = {user.id}
OR recipent_id = {user.id}
You Can use these
$query = (new \yii\db\Query)->from("message_thread")
$query->orFilterWhere(['sender_id'=>$user_id])->orFilterWhere(['recipent_id '=>$user_id]);

Yii 2 can rewrite WHERE condition from model?

I have some where condition in my model .
Its check is field active or no.
Now I need to write a join relation. But I need to remove where condition. Is it possible?
My model.
...
public static function find() {
return (new AssetgroupsQuery(get_called_class()))->active();
}
My relation
public function getAssetgroup(): \app\models\AssetgroupsQuery {
return $this->hasOne(Assetgroups::class, ['asg_id' => 'ass_group'])->andOnCondition(['asg_active' => '1'])
->viaTable('assets', ['ass_id' => 'log_ass_id',]);
}
I need to got all active assets and join, if asset is empty I need to got null fields, but
model where condition added to my current sql query and remove all fields which assets are null.
I try to add some where Condition to remove old where, but it don't work.
Can you help me?
You can reset existing conditions by using where(null).
On relation level:
public function getAssetgroup(): \app\models\AssetgroupsQuery {
return $this->hasOne(Assetgroups::class, ['asg_id' => 'ass_group'])
->andOnCondition(['asg_active' => '1'])
->where(null)
->viaTable('assets', ['ass_id' => 'log_ass_id',]);
}
Or directly on join:
$query = MyModel::find()
->joinWith([
'assetgroup' => function (ActiveQuery $query) {
$query->where(null);
},
])

Left Join CakePHP3

I'm trying to do a LEFT JOIN in CakePHP3.
But all I get is a "is not associated"-Error.
I've two tables BORROWERS and IDENTITIES.
In SQL this is what I want:
SELECT
identities.id
FROM
identities
LEFT JOIN borrowers ON borrowers.id = identities.id
WHERE
borrowers.id IS NULL;
I guess this is what I need:
$var = $identities->find()->select(['identities.id'])->leftJoinWith('Borrowers',
function ($q) {
return $q->where(['borrowers.id' => 'identities.id']);
});
But I'm getting "Identities is not associated with Borrowers".
I also added this to my Identities Table:
$this->belongsTo('Borrowers', [
'foreignKey' => 'id'
]);
What else do I need?
Thanx!
The foreign key cannot just be 'id', that's not a correct model association. You'd need to put a 'borrower_id' field in identities, and declare it like this in the Identities model:
class Identities extends AppModel {
var $name = 'Identities';
public $belongsTo = array(
'Borrower' => array (
'className' => 'Borrower',
'foreignKey' => 'borrower_id'
)
);
Note the capitalization and singular/plural general naming conventions which your example doesn't follow in the least - ignoring those will get you some really hard to debug errors..
Yup. It was an instance of \Cake\ORM\Table, due to my not well chosen table name (Identity/Identities). I guess it's always better not to choose those obstacles, for now I renamed it to Label/Labels.
This query now works perfectly:
$var = $identities
->find('list')
->select(['labels.id'])
->from('labels')
->leftJoinWith('Borrowers')
->where(function ($q) {
return $q->isNull('borrowers.id');
});

How do I translate a partial expression of SQL functions to SQL::Abstract?

I have a query that I would like to run as a DBIC result instead of query string.
The query is this:
"SELECT posts.*, (((LOG10(SUM(points.point) + 1) * 287015) +
UNIX_TIMESTAMP(posts.create_time))) as total FROM posts left join points ON
post.post_id = points.post_id GROUP BY posts.post_id ORDER BY total DESC"
Since I got relationships set up, I think i can avoid left join part on the query (my relationships in schema Post.pm file have has_many relationship set up to points table, so I am guessing that will do a left join with vibe points.
What I have now is the following:
$resultset->search(
{},
{
select => [({sum => points.point} + 1) * 287015],
as => [ 'total' ],
group_by [qw/ id /],
order_by => { -desc => 'total' },
}
);
I am having a problem integrating the LOG10 and sum points with addition of 1 and multiplication of 287015.
Any help is appreciated, I know that whatever I have there as a result, is not the way to do it, but I tried something and it didn't work. Thanks!
Untested because you didn't provide a schema and example data:
'+select' => [{
\'(LOG10(SUM(points.point) + 1) * 287015) + UNIX_TIMESTAMP(me.create_time)'
-as => 'total'
}],
join => 'points',
group_by => 'me.post_id',
order_by => { -desc => 'total' },

How do I make DBIx::Class join tables using other operators than `=`?

Summary
I've got a table of items that go in pairs. I'd like to self-join it so I can retrieve both sides of the pair in a single query. It's valid SQL (I think), the SQLite engine actually does accept it, but I'm having trouble getting DBIx::Class to bite the bullet.
Minimal example
package Schema::Half;
use parent 'DBIx::Class';
__PACKAGE__->load_components('Core');
__PACKAGE__->table('half');
__PACKAGE__->add_columns(
whole_id => { data_type => 'INTEGER' },
half_id => { data_type => 'CHAR' },
data => { data_type => 'TEXT' },
);
__PACKAGE__->has_one(dual => 'Schema::Half', {
'foreign.whole_id' => 'self.whole_id',
'foreign.half_id' => 'self.half_id',
# previous line results in a '='
# I'd like a '<>'
});
package Schema;
use parent 'DBIx::Class::Schema';
__PACKAGE__->register_class( 'Half', 'Schema::Half' );
package main;
unlink 'join.db';
my $s = Schema->connect('dbi:SQLite:join.db');
$s->deploy;
my $h = $s->resultset('Half');
$h->populate([
[qw/whole_id half_id data /],
[qw/1 L Bonnie/],
[qw/1 R Clyde /],
[qw/2 L Tom /],
[qw/2 R Jerry /],
[qw/3 L Batman/],
[qw/3 R Robin /],
]);
$h->search({ 'me.whole_id' => 42 }, { join => 'dual' })->first;
The last line generates the following SQL:
SELECT me.whole_id, me.half_id, me.data
FROM half me
JOIN half dual ON ( dual.half_id = me.half_id AND dual.whole_id = me.whole_id )
WHERE ( me.whole_id = ? )
I'm trying to use DBIx::Class join syntax to get a <> operator between dual.half_id and me.half_id, but haven't managed to so far.
Things I've tried
The documentation hints towards SQL::Abstract-like syntax.
I tried writing the has_one relationship as such:
__PACKAGE__->has_one(dual => 'Schema::Half', {
'foreign.whole_id' => 'self.whole_id',
'foreign.half_id' => { '<>' => 'self.half_id' },
});
# Invalid rel cond val HASH(0x959cc28)
Straight SQL behind a stringref doesn't make it either:
__PACKAGE__->has_one(dual => 'Schema::Half', {
'foreign.whole_id' => 'self.whole_id',
'foreign.half_id' => \'<> self.half_id',
});
# Invalid rel cond val SCALAR(0x96c10b8)
Workarounds and why they're insufficient to me
I could get the correct SQL to be generated with a complex search() invocation, and no defined relationship. It's quite ugly, with (too) much hardcoded SQL. It has to imitated in a non-factorable way for each specific case where the relationship is traversed.
I could work around the problem by adding an other_half_id column and joining with = on that. It's obviously redundant data.
I even tried to evade said redundancy by adding it through a dedicated view (CREATE VIEW AS SELECT *, opposite_of(side) AS dual FROM half...) Instead of the database schema it's the code that got redundant and ugly, moreso than the search()-based workaround. In the end I wasn't brave enough to get it working.
Wished SQL
Here's the kind of SQL I'm looking for. Please note it's only an example: I really want it done through a relationship so I can use it as a Half ResultSet accessor too in addition to a search()'s join clause.
sqlite> SELECT *
FROM half l
JOIN half r ON l.whole_id=r.whole_id AND l.half_id<>r.half_id
WHERE l.half_id='L';
1|L|Bonnie|1|R|Clyde
2|L|Tom|2|R|Jerry
3|L|Batman|3|R|Robin
Side notes
I really am joining to self in my full expanded case too, but I'm pretty sure it's not the problem. I kept it this way for the reduced case here because it also helps keeping the code size small.
I'm persisting on the join/relationship path instead of a complex search() because I've got multiple uses for the association, and I didn't find any "one size fits all" search expression.
Late update
Answering my own question two years later, it used to be a missing functionality that has since then been implemented.
For those still interested by this, it's finally been implemented as of 0.08192 or earlier. (I'm on 0.08192 currently)
One correct syntax would be:
__PACKAGE__->has_one(dual => 'Schema::Half', sub {
my $args = shift;
my ($foreign,$self) = #$args{qw(foreign_alias self_alias)};
return {
"$foreign.whole_id" => { -ident => "$self.whole_id" },
"$foreign.half_id" => { '<>' => { -ident => "$self.half_id" } },
}
});
Trackback: DBIx::Class Extended Relationships on fREW Schmidt's blog where I got to first read about it.
I think that you could do it by creating a new type of relationship extending DBIx::Class::Relationship::Base but it doesn't seem incredibly well documented. Have you considered the possibility of just adding a convenience method on the resultset set for Half that does a ->search({}, { join => ... } and returns the resultset from that to you? It's not introspectable like a relationship but other than that it works pretty much as well. It uses DBIC's ability to chain queries to your advantage.
JB, notice that instead of:
SELECT *
FROM half l
JOIN half r ON l.whole_id=r.whole_id AND l.half_id<>r.half_id
WHERE l.half_id='L';
You can write the same query using:
SELECT *
FROM half l
JOIN half r ON l.whole_id=r.whole_id
WHERE l.half_id<>r.half_id AND l.half_id='L';
Which will return the same data and is definitely easier to express using DBIx::Class.
Of course, this doesn't answer the question "How do I make DBIx::Class join tables using other operators than =?", but the example you showed doesn't justify such need.
Have you tried:
__PACKAGE__->has_one(dual => 'Schema::Half', {
'foreign.whole_id' => 'self.whole_id',
'foreign.half_id' => {'<>' => 'self.half_id'},
});
I believe the matching criteria in the relationship definition is the same used for searches.
Here is how to do it:
...
field => 1, # =
otherfield => { '>' => 2 }, # >
...
'foreign.half_id' => \'<> self.half_id'