I have the following query I want to build using CakePHP. How should I go about this?
SELECT
`Artist`.`id`,
CONCAT_WS(' ', `Person`.`first_name`, `Person`.`last_name`, `Person`.`post_nominal_letters`) AS `name`,
`Portfolio`.`count`
FROM
`people` as `Person`,
`artists` as `Artist`
LEFT OUTER JOIN
(SELECT
`Product`.`artist_id`,
COUNT(DISTINCT `Product`.`id`) AS `count`
FROM
`product_availabilities` AS `ProductAvailability`,
`products` AS `Product`
LEFT OUTER JOIN
`order_details` AS `OrderDetail`
ON
`Product`.`id` = `OrderDetail`.`product_id`
LEFT OUTER JOIN
`orders` AS `Order`
ON
`Order`.`id` = `OrderDetail`.`order_id`
WHERE
`ProductAvailability`.`id` = `Product`.`product_availability_id`
AND
`Product`.`online` = true
AND
(`ProductAvailability`.`name` = 'For sale')
OR
((`ProductAvailability`.`name` = 'Sold') AND (DATEDIFF(now(),`Order`.`order_date`) <= 30))
GROUP BY
`Product`.`artist_id`)
AS
`Portfolio`
ON
`Artist`.`id` = `Portfolio`.`artist_id`
WHERE
`Artist`.`person_id` = `Person`.`id`
AND
`Artist`.`online` = true
GROUP BY
`Artist`.`id`
ORDER BY
`Person`.`last_name`, `Person`.`first_name`;
I think that the model is Artist and It has a belongsTo Relationship with , then you could use the CakePHP ORM on this way and hasMany with Portfolio
first you mus distroy the relation between Artist and Portfolio
$this->Artist->unbindModel(array('hasMany'=>array('Portfolio')));
and then Build the relations
$this->Artist->bindModel(array('hasOne'=>array('Portfolio')));
Finally you must create the other relationsships
$this->Artist->bindModel(array(
'belongsTo'=>array(
'Product'=>array(
'clasName'=>'Product',
'foreignKey'=> false,
'conditions'=>'Product.id = Artist.product_id'
),
'ProductAvaibility'=>array(
'clasName'=>'ProductAvaibility',
'foreignKey'=> false,
'conditions'=>'ProductAvaibility.id = Product.product_avaibility_id'
),
'OrderDetail'=>array(
'clasName'=>'OrderDetail',
'foreignKey'=> false,
'conditions'=>'Product.id = OrderDetail.product_id'
),
'Order'=>array(
'clasName'=>'Order',
'foreignKey'=> false,
'conditions'=>'Order.id = OrderDetail.order_id'
),
)
));
Now, when the relationships are done, you could do your find
$this->Artist->find('all', array(
'conditions'=>array(
'Artist.online'=>true
),
'group'=>array(
'Artist.id'
),
'order'=>array(
'Person.last_name',
'Person.first_name',
)
))
I hope it could be useful for you
You should place it in your model. If you haven't read yet, I suggest you to read about skinny controllers and fat models.
class YourModel extends AppModel {
public function getArtistsAndPortfolioCounts() {
return $this->query("SELECT ... ");
}
}
So in your controller:
class ArtistsControllre extends AppController {
public function yourAction() {
debug($this->YourModel->getArtistsAndPortfolioCounts());
}
}
Do not try to build that query using ORM. It will be a disaster in slow motion.
Instead you should try to do that request as "close to metal" as possible. I am not familiar with CakePHP's API ( as i tend to avoid it like black plague ), but you should be able to create a Model which is not related to ActiveRecord, and most likely have access to anything similar to PDO wrapper. Use that to execute the query and map results to variables.
That said, you might want to examine your query ( you seem to have some compulsive quoting sickness ). One of improvements you could do would be stop using id columns in tables.
This whole setup could really benefit from creation of another column in Artists table : portfolio_size. Which you can update each time it should be changed. Yes , it would de-normalize your table, but it also will make this query trivial and blazing fast, with minor costs elsewhere.
As for names of columns, if table Artists has a id, then keep it in column artist_id , and if Products has a foreign key referring to Artists.artist_id then name it too artist_id. Same thing should have same name all over your database. This would additionally let you use in SQL USING statement. Here is a small example :
SELECT
Users.name
Users.email
FROM Users
LEFT JOIN GroupUsers USING (user_id)
LEFT JOIN Groups USING (group_id)
WHERE Groups.name = 'wheel'
I assume that this query does not need explanation as it is a simple many-to-many relationship between users and groups.
You could use the query method, but that method is treated as a last resort, when find or any of the other convenience methods don't suffice.
But it's also possible to manually specify joins in the Cake find method, see the cookbook. I'm not entirely sure, but I believe nested joins are also supported. CONCAT_WS could just be called, with the relevant fields, in the field property of the find method.
Related
So I've searched the internet for similar cases, and I just got lost from all contradicting answers, and unrelated scenarios. So I thought to put my case hoping to get some specific answers.
I am new to Laravel, and creating small application. In the application I have to search for offeres and show the result in a blade view. Since the query is complex, and output of the search does not belong to a specific Model, I just wish to keep it as a raw query.
I have placed the query in the controller, but I just don't feel it's the right place. Especially if I need to reuse the query in many places.
Here's the method from the OfferController:
public function search(Request $request)
{
$area = $request->area;
$size = $request->size;
$sql = "SELECT distinct product_name,product_offer.quantity, product_offer.price
FROM product
inner join brand on product.brand_id = brand.brand_id
inner join brand_area on brand_area.brand_id = brand.brand_id
inner join area on area.area_id = brand_area.area_id
inner join product_offer on product_offer.product_id = product.product_id
where area.area_id = :area
and product.size_id = :size ";
$params = array(
'area'=>$area,
'size'=>$size
);
$offers = DB::select( DB::raw($sql), $params);
return view('searchresult')->with($offers);
}
So in short: should I move the query to the model, create DAL class, or keep it here? Baring in mind the project is small scale.
in my opinion, if you are going to reuse it, create a service that will perform that query and gives you back a result, something like SearchService that looks like this:
<?php
class SearchService
{
public static function perform(array $params){
$sql = "SELECT distinct product_name,product_offer.quantity, product_offer.price
FROM product
inner join brand on product.brand_id = brand.brand_id
inner join brand_area on brand_area.brand_id = brand.brand_id
inner join area on area.area_id = brand_area.area_id
inner join product_offer on product_offer.product_id = product.product_id
where area.area_id = :area
and product.size_id = :size ";
return DB::select( DB::raw($sql), $params);
}
}
?>
And by doing so, you can just call
SearchService::perform([...]);
to get the results.
Obviously this is version1.0, you can improve it in a lot of ways, for example making it not static in order to make it testable, and also to allow getter and setter to exists, and a lot of other things, that might be usefull
You have a fair point saying it does not look right to place query in the controller. I would offer you to have a look at the concept of Laravel repository pattern:
https://dev.to/asperbrothers/laravel-repository-pattern-how-to-use-why-it-matters-1g9d
Also, I think you could use Laravel DB for this kind of query without the need to write it as a raw query. I do not think there is a huge need to have a raw query here. Laravel DB table(), select(), where() and other methods should be enough.
Actually, you could potentially write this using models and their relationships, but if the query is quite slow, it is better to use query builder for better efficiency.
EDIT:
also for some specific queries which do not belong to anything I remember seeing custom trait used, could also be a solution.
I'm writing my previous comment as an answer since I think its really what you are looking for:
I suggest you use a Trait to do this. The reason why is that it will keep your code clean and you'll be able to reuse this code later for other uses. A Trait is made for reusable code.
You will create a Trait:
<?php
namespace App\Traits; // Optional, but you could create a namespace for all your Traits if you need others
trait MyCustomTrait
{
public function perform(array $params) {
$sql = "SELECT distinct product_name,product_offer.quantity, product_offer.price
FROM product
inner join brand on product.brand_id = brand.brand_id
inner join brand_area on brand_area.brand_id = brand.brand_id
inner join area on area.area_id = brand_area.area_id
inner join product_offer on product_offer.product_id = product.product_id
where area.area_id = :area
and product.size_id = :size ";
return DB::select( DB::raw($sql), $params);
}
}
To use it, only write: use MyCustomTrait in the scope of your controller and call your function like this: $this->perform([...]).
Here's some links to learn more about traits:
https://www.w3schools.com/php/php_oop_traits.asp
https://www.develodesign.co.uk/news/laravel-traits-what-are-traits-and-how-to-create-a-laravel-trait/.
Hope it helps you!
Just for reference I am using Laravel 5.
I have a two tables
users
id
first name
skills
id
name
and a pivot table
skill_user
skill_id
user_id
if I do a select in MySQL as follows:
select users.id as id, users.first_name, skills.name from users
left join skill_user on users.id = skill_user.user_id
left join skills on skill_user.skill_id=skills.id
I get:
id, first_name, skill
1, Jenna, Reliable
1, Jenna, Organized
2, Alex, Hardworking
3, Barry, Capable
3, Barry, Amiable
3, Barry, Patient
4, Janine, (null)
I pass this through to a view via a Controller:
$peoples = [];
$peoples = \DB::table('users')
->select(\DB::raw('users.id as id, first_name, skill.name as name"'))
->leftJoin('skill_user','users.id','=','skill_user.user_id')
->leftJoin('skills','skill_user.skill_id','=','skills.id')
->get();
return view('find-people', compact(['peoples']));
Now, I want to loop through this in the view (pseudocode):
forelse ( peoples as people )
people - > first_name
people - > skill
empty
no people found
endforelse
Which all works fine in a sense - but the first name gets repeated when there is more than one skill.
I can probably hack a loop of the skills by doing something like comparing user_id to itself but it seems such a clumsy way to do it.
user_id = $peoples->id
while (some looping criteria)
{
write out skills
if($peoples->id != user_id){break;}
}
How do I loop through the recordset in an elegant/eloquent fashion? Or is there a better entirely to do this?
If you define the relationships in your models you don't need to try and construct raw SQL to achieve that. This is what the Laravel ORM "Eloquent" is for!
class People extends Model {
public function skills () {
return $this->hasMany('Skill');
}
}
Then you define the skill model :
class Skill extends Model {
public function People () {
return $this->belongsToMany('People');
}
}
Now you're able to iterate over the People model and for each person, get their ->skills. This way you don't end up with the duplicate issue you're experiencing and you greatly simplify the SQL you're trying to achieve by leveraging the ORM.
I have two tables in the database, namely "user" and "user_user" which links users to users (many to many)
in user_user there are two columns "user_id_0" and "user_id_1"
I need to get all users where the current logged in user id is equal to either
user_id_0 OR user_id_1.
I can achieve this simply with sql:
select * from user u
inner join user_user uu
on uu. user_id_0 = u.id
or uu.user_id_1 = u.id
[where u.id = 1]
In Yii2 I'm stuck with something like this:
($this would be of type User)
$this->hasMany(User::className(), ['id' => 'user_id_0'])
->viaTable('user_user', ['user_id_0' => 'id'],
function($query) {
$query->orOnCondition(['user_id_1' => 'id']);
});
Now this can't work as hasMany() links to only one foreign key. How do I do something like the SQL above? To join two foreign keys with 'or'.
I this possible or would you suggest a different database design? The goal here is really to enable users have "friends" or "connections" to other users like you would in a social network.
I'm not sure if this works as you expect. I didn't try it out.
IMO the query should look like this (assuming MySQL):
SET #id = 1; // or whatever
SELECT user.*
FROM user
INNER JOIN user_user
ON user.id = user_user.id1 OR user.id = user_user.id2
WHERE user.id != #id AND (user_user.id1 = #id OR user_user.id2 = #id)
GROUP BY user.id;
This could be realized in class User like this:
public function getConnectedPersons() {
return self::find()
->select('user.*')
->innerJoin('user_user', ['or',
'user.id=user_user.user_id_0',
'user.id=user_user.user_id_1'
]
)->where(['and',
['user.id' => $this->id],
['or',
['user_user.user_id_0' => $this->id],
['user_user.user_id_1' => $this->id]
]
]
)->groupBy('user.id');
}
This returns an ActiveQuery object. Append ->all() if you want to get the user array directly.
I hope this works. It is untested. But you may get the idea and give me feedback if anything is wrong.
This might not be the best solution. It doesn't use viaTable() which should also work somehow.
I have been trying to convert a right join query to left join query in order to use it inside laravel query builder. Here is my Sql statement and it result wich works flawlessly
select `weekday`.`name`, `open_time`, `close_time`
from `schedule`
join `restaurants_has_schedule` on `schedule`.`id` = `restaurants_has_schedule`.`schedule_id`
and `restaurants_has_schedule`.`restaurants_id` = 1
right join `weekday` on `weekday`.`id` = `schedule`.`weekday_id`
ORDER BY `weekday`.`id`
|------
|name|open_time|close_time
|------
|Domingo|NULL|NULL
|Lunes|NULL|NULL
|Martes|NULL|NULL
|Miercoles|NULL|NULL
|Jueves|14:11:51|14:11:51
|Vienes|09:11:21|17:00:00
|Sábado|NULL|NULL
but when convert It to left join it stop working, displaying me the same data for every single restaurants_id. This is my left join statement.
select `weekday`.`name`, `open_time`, `close_time`
from `weekday`
left join `schedule` on `weekday`.`id` = `schedule`.`weekday_id`
join `restaurants_has_schedule` on `schedule`.`id` = `restaurants_has_schedule`.`schedule_id`
and `restaurants_has_schedule`.`restaurants_id` = 1
ORDER BY `weekday`.`id`
What am I doing wrong? Is There another alternative? Thak you in advance
Try use Laravels Eloquent ORM, which handles relationships really cool! no need anymore to concat or write sql-queries
See here about: Laravels Eloquent ORM & Schema Builder
Or maybe about orm's in general, you should really give it a try:
Object Role Modeling
Object Role Modeling
Example from Laravel doc:
One Post may have many comments
One to many:
class Post extends Eloquent {
public function comments()
{
return $this->hasMany('Comment');
}
}
Where 'Comment' is the model.
The "reverse" to define:
class Comment extends Eloquent {
public function post()
{
return $this->belongsTo('Post');
}
}
Where 'Post' is the model.
And then as query:
$comments = Post::find(1)->comments; //by Primary Key
Or
$comments = Post::where('somefield', '=', 'somevalue')->comments->get();
....Really cool, for many to many see the docs#Laravels Eloquent ORM
I have two models, Plant and Emp, that have a Has And Belongs To Many relationship. I've configured them to be associated and the query to get the data for each is correct, but the problem is Plant and Emp are on different databases. Emp is on Database 1, Plant is on Database 2. Because of this they don't query the join table properly; the join table is only on Database 1.
When the Plant model tries to access the join table it's querying Database 2, which does not have this data.
This is the association Emp has for Plant.
var $hasAndBelongsToMany = array(
'Plant' =>
array(
'className' => 'Plant',
'joinTable' => 'emp_plant',
'foreignKey' => 'employee_id',
'associationForeignKey' => 'LocationID',
'unique' => true,
'conditions' => '',
)
);
Update:I tried to set a "finderQuery" attribute to let me query the join table, but I don't know how to give a raw SQL query like that and allow it to dynamically use the id for the instance of the Model instead of a predefined value.
I can set something like
SELECT * FROM [Plant] AS [Plant] JOIN [DB].[DBO].[empplant] AS
[EmpPlant] ON ([EmpPlant].[employee_id] = **4**
AND [EmpPlant].[ID] = [Plant].[LocationID])
Which will give me the correct data for one employee, but I don't know how to make this finderQuery a dynamic query. There has to be a way for this to work.
Try
var $useDbConfig = 'alternate';
in your Model Class.
I needed to use a custom finderQuery and use the special {$__cakeID__$} identifier in place of the model ID being matched. This is a fixed version of the sample above, set as the finder query in the relationship entry for the $hasAndBelongsToMany array.
'finderQuery'=>'SELECT * FROM [Plant] AS [Plant] JOIN [DB].[DBO].[empplant] AS
[EmpPlant] ON ([EmpPlant].[employee_id] = {$__cakeID__$}
AND [EmpPlant].[ID] = [Plant].[LocationID])'
This works but if anyone knows how to fix this situation without a custom finder query (what I was trying to avoid by using associations) please post an answer and I will mark that correct instead.