yii find models through related models by using having - activerecord

For example I have two related models with MANY_MANY relation.
Need to find all models which name contains 'test' or in which relation model2.name contains 'test'.
On sql I wrote this query and it's what I want to get from ActiveRecord in result by using standard relations mechanism and CDbCriteria.
SELECT
m1.*
FROM
model1 m1
LEFT JOIN model_model mm
ON mm.from_id = m1.id
LEFT JOIN model2 m2
ON mm.to_id = m2.id
GROUP BY m1.id
HAVING (
m1.name LIKE '%test%'
OR GROUP_CONCAT(m2.name) LIKE '%test%'
);
Simple use Activerecord.findBySql is not good solution because I have many models such as above. So for faster combination any models a relations is prefered.
When I use CDbCriteria.with Yii generate 2 query.
When I use CDbCriteria.with with CDbCriteria.together Yii tried to select all columns from related tables, it's redundantly and may be slowly in future because number of relations can become much more than in this example.
Have any idea?
Thanks.

You should define the relation in "M1" model class:
public function relations()
{
return array(
'M2s' => array(self::MANY_MANY, 'M2',
'model_model(from_id, to_id)'),
);
}
In your M2 model create a scope:
public function nameScope($name)
{
$this->getDbCriteria()->mergeWith(array(
'condition'=>'name LIKE :name',
'params'=>array(':name'=>"%{$name}%"),
));
return $this;
}
If your ready you can do this:
M1::model()->findAll(
array(
'condition' => 'name LIKE %test%',
'with' => array(
'M2s' => array(
'scopes' => array(
'nameScope' => 'test',
)
)
),
)
);

Related

Fetch Multiple Records with Joins in Laravel Eloquent

I'm very new to using Laravel, thus to Eloquent. I am getting kind of confused about Eloquent's table relationships.
Now I understand how to implement simple joins, like the example in Laravel 4.2's documentation which for one-to-many relationships, where a comment belongs to one post, but a post can have many comments. They use this syntax to get the comments from one post:
Post::find(1)->comments;
Which, in MySQL, is probably like:
SELECT * FROM comments
JOIN posts ON posts.id=comments.post_id
WHERE posts.id=1
What if the results I'm trying to get is something like this that gets more than just one row:
SELECT * FROM comments
JOIN posts ON posts.id=comments.post_id
I know it doesn't make much sense based on the example I gave above. But how do I do it in Eloquent?
To give further details, what I'm actually trying to do is to display joined results from my two tables, assets and asset_classifications. An asset belongs to one asset_classification, and an asset_classification has many assets.
I'm trying to display a tabular data of assets which include their asset_classifications. In MySQL, it's something like this:
SELECT * FROM assets
JOIN asset_classifications ON asset_classifications.id=assets.classification_id
How can I perform it in Eloquent?
I guess you're a little too attached to SQL :) Try thinking outside of joins and queries an more in models and melattionships, because Laravel handles all the abstraction for you.
So you have an Asset model:
class Asset extends Eloquent
{
public function classification()
{
return $this->belongsTo('AssetClassification');
}
}
...and an AssetClassification mdoel:
class AssetClassification extends Eloquent
{
public function assets()
{
return $this->hasMany('Asset');
}
}
And now they are linked and you can do whatever you want. If you want to output all assets and their classification, no problem:
$assets = Asset::all();
foreach($assets as $asset)
{
echo "{$asset->name}" is classified as {$asset->classification}";
}
Or the other way around:
$classifications = AssetClassification::all();
foreach($classifications as $classification)
{
echo "{$classification->name} has the following assets:";
foreach($classification->assets as $asset)
{ ... }
}
As an array, this would look something like
[0] => [
'id' => 1
'name' => 'asset_name_1',
],
[1] => [
'id' => 2
'name' => 'asset_name_2',
],
You get the idea. The problem is, that you do a separate query on each iteration. That's why you should use eager loading to not also load all Assets but also their dependencies:
$assets = Asset::with('classification')->get();
Now you would have an array like this:
[0] => [
'id' => 1
'name' => 'asset_name_1',
'classification' => AssetClassiciation-Object
],
[1] => [
'id' => 2
'name' => 'asset_name_2',
'classification' => AssetClassiciation-Object
],
So now you can loop through assets and their classification without making any further SQL-queries.

Yii 2, active record, with

how can i get a set of data, based on condition in relation table?
In Yii 1 i could use with() and 'on' statement. This doesn't work in Yii 2, or i can't find any good example.
For example in Yii 1 i could write this:
$criteria = new CDbCriteria();
$criteria->with = array('works'=>array('on' => 'works.user_id=t.id AND (works.work_id=$SOMEVALUE OR ...)'));
I tried something like this (userRight is my relation):
Foo::find()->with(['userRight'=>['on'=>['user_r'=>$this->id]]]);
Is there any solution in Yii 2?
From official example:
$orders = Order::find()->innerJoinWith([
'customer' => function ($query) {
$query->where('customer.created_at > ' . (time() - 24 * 3600))
->onCondition(['category_id' => 1]);
}
])->all();
See this link
if in your table have a relation and using gii to generate models, controller and view it will make function for the relation.
if no you can make function to declare relation 2 tables in model
public function getWorks()
{
return $this->hasOne(Works::className(), ['id' => 'works_id']);
}
if hasMany just flip id.
and call the function
Foo::find()->joinWith('works')->all();

Laravel: Eloquent how to update a model and related models in one go

Does anyone know if it is possitble to do the folowing:
Let's say we have a model called User and a model calledd BestFriend. The relation between the User and the best friend is 1:1.
I would like for these cases be able to do something like this, change my city and the city of my friend at the same time.
$me = User::find(1);
$me->update(array(
'city' => 'amsterdam',
'bestfriend.city' => 'amsterdam'
));
So basically I would like to know if Eloquent is smart enough to understand the relationship based on the array key 'bestfriend.city'.
Thanks in advance for any help!
Update:
Found the solution on the Laravel forums but im posting it here as well if someone else is looking for the same thing :)
In the model you add
// In your model...
public function setBestFriendArrayAttribute($values)
{
$this->bestfriend->update($values);
}
And then you can call it like this
$me->update(array(
'city' => 'amsterdam',
'BestFriendArray' => array(
'city' => 'amsterdam'
)
));
Works like a charm!
You don't need to set it on your model. You can do it on your controller like this.
$me = User::find(1)->bestFriend()->update(array(
'city' => 'amsterdam',
'bestfriend.city' => 'amsterdam'
));
I just modified your update a little bit.
Eloquent is pretty smart, but I don't believe it can do that. You would have to update User and BestFriend independently. But once you've done that, Eloquent does have methods for attaching the two.
$me = User::find(1);
$bff= BestFriend::find(1);
$me->city = 'amsterdam';
$bff->city = 'amsterdam';
$me->bestfriend()->associate($bff);
This is of course assuming your User model has a function that looks like...
public function bestfriend()
{
return $this->hasOne('BestFriend');
}
If you want to save same city name to both entities this will do the job:
$me = User::find(1);
$me->update(array(
'city' => 'amsterdam',
));
And in your model:
public function setCityAttribute($value)
{
$this->attributes['city'] = $value;
$this->bestfriend->city= $value;
$this->bestfriend->save();
}

Cakephp 2 Paginate Order By Closest Date

I am trying to get the list of results from the Orders table in my CakePhp 2.x application to sort themselves by the date closest to today.
In a usual mysql query I have had something similar working with say the following syntax:
ABS(DATEDIFF(Order.duedate, NOW()))
However in Cake I am not sure how to get such a custom query to work within the paginate helper. Is this something I may need to set in a finder query in the model?
Here is the controller I currently have (set to a bog standard descending sort)
public function index() {
$this->paginate = array(
'conditions' => array (
'Order.despatched' => array('Not Despatched' , 'Split Despatched')
),
'limit' => 25,
'order' => array('Order.duedate' => 'DESC')
);
$data = $this->paginate('Order');
$this->set('orders', $data);
}
Edit: Using information from the comment below I added a virtual field into the controller which causes an sql error. At first I thought this was due to the associations within the model, to attempt to rectify this I added the virtualField into the Order Model and into the constructor of the associated Account Model. However this made no change to the part of the sql which breaks. The full sql error is:
Error: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'AS `Order__closestdate`, `Account`.`id`, `Account`.`company`, `Account`.`contac' at line 1
SQL Query: SELECT `Order`.`id`, `Order`.`order_id`, `Order`.`orderdate`,
`Order`.`duedate`, `Order`.`rework`, `Order`.`rework_notes`, `Order`.`comments`,
`Order`.`status`, `Order`.`customer`, `Order`.`last_edited`, `Order`.`value`,
`Order`.`quantity`, `Order`.`order_for`, `Order`.`warehouse`, `Order`.`haulier`,
`Order`.`terms`, `Order`.`type`, `Order`.`despatched`, `Order`.`despatched_date`,
`Order`.`invoiced`, `Order`.`invoice_date`, `Order`.`bookingref`,
`Order`.`purchaseref`, `Order`.`payment_due`, `Order`.`payment_status`,
`Order`.`payment_date`, (ABS(DATEDIFF(duedate, NOW())) AS `Order__closestdate`,
`Account`.`id`, `Account`.`company`, `Account`.`contact`, `Account`.`phone`,
`Account`.`email`, `Account`.`postcode`, `Account`.`created`, `Account`.`modified`,
`Account`.`customer`, `Account`.`address1`, `Account`.`address2`, `Account`.`town`,
`Account`.`county`, (ABS(DATEDIFF(duedate, NOW())) AS `Account__closestdate` FROM
`hunter_om`.`orders` AS `Order` LEFT JOIN `hunter_om`.`accounts` AS `Account` ON
(`Order`.`customer` = `Account`.`customer`) WHERE `Order`.`despatched` IN ('Not
Despatched', 'Split Despatched') ORDER BY (ABS(DATEDIFF(duedate, NOW())) DESC LIMIT 25
For reference the code in the models are:
//Order Model
public $virtualFields = array(
'closestdate' => 'ABS(DATEDIFF(duedate, NOW())'
);
//Account Model
public function __construct($id = false, $table = null, $ds = null) {
parent::__construct($id, $table, $ds);
$this->virtualFields['closestdate'] = $this->Order->virtualFields['closestdate'];
}
Any help is appreciated thanks.
You'll need to add a 'virtualField' to the Order model before paginating;
$this->Order->virtualFields['closestdate'] = "ABS(DATEDIFF(Order.duedate, NOW()))";
Then use that field as a regular field in your query/paginate
public function index() {
$this->Order->virtualFields['closestdate'] = "ABS(DATEDIFF(Order.duedate, NOW()))";
$this->paginate = array(
'conditions' => array (
'Order.despatched' => array('Not Despatched' , 'Split Despatched')
),
'limit' => 25,
'order' => array('Order.closestdate' => 'DESC')
);
$data = $this->paginate('Order');
$this->set('orders', $data);
}

Cakephp pagination sort by calculated field ( COUNT )

I had a problem sorting a paginated list when using a calculated field such as COUNT() in cakephp 1.3
Let's say that i have two models: Article and Comments (1 article x N comments) and i want to display a paginated list of the articles including the number of comments for each one. I'd have something like this:
Controller:
$this->paginate = array('limit'=>80,
'recursive'=>-1,
'fields'=>array("Article.*","COUNT(Comment.id) as nbr_comments"),
'joins'=>array(array( 'table' => 'comments',
'alias' => 'Comment',
'type' => 'LEFT',
'conditions' => array('Comment.article_id = Article.id'))
),
'group'=>"Article.id"
);
(i had to overwrite the findCount() method in order to paginate using group by)
The problem is that in the view, the sort() method won't work:
<th><?php echo $this->Paginator->sort('nbr_comments');?></th> //life is not that easy
I was able to create a workaround by "cheating" the pagination and sort:
Controller
$order = "Article.title";
$direction = "asc";
if(isset($this->passedArgs['sort']) && $this->passedArgs['sort']=="nbr_comments")
$order = $this->passedArgs['sort'];
$direction = $this->passedArgs['direction'];
unset($this->passedArgs['sort']);
unset($this->passedArgs['direction']);
}
$this->paginate = array(... 'order'=>$order." ".$direction, ...);
$this->set('articles', $this->paginate());
if($order == "clicks"){
$this->passedArgs['sort'] = $order;
$this->passedArgs['direction'] = $direction;
}
View
<?php $direction = (isset($this->passedArgs['direction']) && isset($this->passedArgs['sort']) && $this->passedArgs['sort'] == "nbr_comments" && $this->passedArgs['direction'] == "desc")?"asc":"desc";?>
<th><?php echo $this->Paginator->sort('Hits','clicks',array('direction'=>$direction));?></th>
And it works.. but it seems that is too much code for something that should be transparent to the developper. (It feels like i'm doing cake's work) So i'm asking if there's another simpler way. Maybe cake has this functionallity but decided to hide it.. o_O.. there's nothing about this on the documentation, and i haven't found another good solution on S.O... how do you do it?
Thanks in advance!
maybe your problem can be solved using Virtual fields ?
If you create a custom field for your computed field, you will be able to sort using Paginator->sort() method on that custom field.
I found that solution there in the comments (there is no need to customize the paginate method etc. using custom fields instead of in adnan's initial solution).
What you may want to do is use Cake's counterCache functionality in your model definition. Rather than calculating the comment counts at read time, you add nbr_comments as an int field in your articles table. Every time a Comment is inserted or deleted, nbr_comments will be updated automatically.
In your Comment model:
var $belongsTo = array(
'Article' => array(
'counterCache' => 'nbr_comments'
)
);
Now you can just use $this->Paginator->sort('Article.nbr_comments'); You wont need to do anything funky in your controller.
My solution for this problem was the following:
put your calculate field as a virtual field
$this->Comment->virtualFields = array(
'nbr_comments' => 'COUNT(Comment.id)'
);
then, you can use that field, in your paginate order variable
$order = array("Comment.nbr_comments" => "DESC");
$this->Paginator->settings = array("order" => $order);

Resources