Eloquent join using "USING" clause with N query - php

I'm using Slim Framework with Illuminate Database.
I want to make JOIN query with USING clausa. Let's say given Sakila database. Diagram:
How to make join with USING clause (not ON) in eloquent model ?
SELECT film_id,title,first_name,last_name
FROM film_actor
INNER join film USING(film_id) -- notice
INNER join actor USING(actor_id) -- notice
What I want is an eager loading with EXACT 1 query. The use of eloquent relationships described in the API is not meeting my expectation, since any eager relation use N+1 query. I want to make it less IO to database.
FilmActor model :
class FilmActor extends Model
{
protected $table = 'film_actor';
protected $primaryKey = ["actor_id", "film_id"];
protected $increamenting = false;
protected $appends = ['full_name'];
// i need to make it in Eloquent model way, so it easier to manipulate
public function getFullNameAttribute()
{
$fn = "";
$fn .= isset($this->first_name) ? $this->first_name ." ": "";
$fn .= isset($this->last_name) ? $this->last_name ." ": "";
return $fn;
}
public function allJoin()
{
// how to join with "USING" clause ?
return self::select(["film.film_id","title","first_name","last_name"])
->join("film", "film_actor.film_id", '=', 'film.film_id')
->join("actor", "film_actor.actor_id", '=', 'actor.actor_id');
//something like
//return self::select("*")->joinUsing("film",["film_id"]);
//or
//return self::select("*")->join("film",function($join){
// $join->using("film_id");
//});
}
}
So, in the controller I can get the data like
$data = FilmActor::allJoin()
->limit(100)
->get();`
But there's a con, if I need to add extra behavior (like where or order).
$data = FilmActor::allJoin()
->where("film.film_id","1")
->orderBy("film_actor.actor_id")
->limit(100)
->get();`
I need to pass table name to avoid ambiguous field. Not good. So I want for further use, I can do
$kat = $request->getParam("kat","first_name");
// ["film_id", "title", "first_name", "last_name"]
// from combobox html
// adding "film.film_id" to combo is not an option
// passing table name to html ?? big NO
$search = $request->getParam("search","");
$order = $request->getParam("order","");
$data = FilmActor::allJoin()
->where($kat,"like","%$search%")
->orderBy($order)
->limit(100)
->get();`

In Eloquent (and I think that was already available in 2018) the feature is not named using but with and should give something like :
ForumActor::with(['film', 'actor'])->get();
Of course this has to be adapted to your cases, you may even nest relationships :
ForumActor::with('actor.contacts')->get();
For instance.
Have a look : https://laravel.com/docs/8.x/eloquent-relationships#eager-loading
Even though it's labelled as "Eager Loading" (which is great btw) it also works without eager loading, and moreover, when foreign keys are properly set (e.g. with migrations), then it uses only ONE query, so that keeps away the N+1 Problem.

You can try find in code is possible to make USING JOIN, or add some proxy dictionary:
$kat_dict = ["film_id" => "film.film_id", "title"=> 'title', "first_name" => 'first_name', "last_name" => 'last_name'];
$kat = $kat_dict[$request->getParam("kat","first_name")];
BTW: better way is using function like Arr::get($arr, $index, $default) (See at code example)

You can simply call raw query via select
$query = "SELECT
film_id,title,first_name,last_name
FROM
film_actor
INNER join film USING(film_id)
INNER join actor USING(actor_id)
WHERE
film.film_id = :filmId
ORDER BY film_actor.actor_id
LIMIT 0, 100";
$data = DB::select($query, [
'filmId' => 1
]);
// or like this, if not using default connection
/**
$data = DB::connection('test')->select($query, [
'filmId' => 1
]);
*/

Related

Model return empty array when use select in model (Laravel)

I have a hasOne(Many) relation function like this:
return $this->hasOne('App\Models\ProductTranslation','product_id','id')->where('language_id', $language_id['id']);
Also, I tried to use
return $this->hasOne('App\Models\ProductTranslation','product_id','id')->select('product_translations.name')->where('language_id', '1');
In Controller use this
$value=Product::with('translation:name')->find(1);
I try to receive only one specific column from the table, but it returned an empty array. In debug bar I see the query when I use it in PHPMyAdmin it return only one column as I want.
Is this possible?
P.S (use Laravel 5.8.34)
Updating
I choose 'Entity Layers for Translated Fields and Non-Translated Fields' approach for translation in the project and have a database like this Database picture
If you want to get only that language_id type of translation then you might do like this.
$language_id = 1;
$products = Product::with(array('translation'=>function($query) use($language_id){
$query->where('language_id',$language_id);
}))->get();
and if you want to select name then like this
make sure you've to select id,name id is a must.
$language_id = 1;
$products = Product::with(array('translation'=>function($query) use($language_id){
$query->where('language_id',$language_id)->select('id','name');
}))->get();
Remove where conditions from models.
As per your DB structure in Language it's belongsToMany with role
Languages.php model
public function role(){
returh $this->belongsToMany('App\Models\Role','role_translations','language_id','role_id')
}
$language_id = 1;
$products = Product::with(array('translation.role'=>function($query) use($language_id){
$query->where('role_translations.language_id',$language_id)->select('languages.id','languages.name');
}))->get();

Building Laravel query adding where statement by relationship's field

I am not able to construct simple Laravel query.
I have translation with categories (translation.category_id is foreign key to category.id). Moroever category also has property is_technical.
What I need is:
- get all translations where translation's category.is_technical = 1.
Currently I am haveing this query:
$match = ['lang1_code' => $langfrom, 'lang2_code' => $langto];
$translation = Translation::where($match)->orderByRaw("RAND()")->take(4)->get();
But this query doesn't join category (I have relationship in my db and also in my models). Thus how to join category and set where is_Technical = 1?
I believe this is basic question, but I am new to Laravel and I cannot find answer in documentation.
you need whereHas. See Laravel Document for more info
http://laravel.com/docs/5.1/eloquent-relationships
Here is an example, correct your model name.
//Translation.php
public function category() {
return $this->belongsTo('Category', 'category_id');
}
// query
$translation = Translation::whereHas('category', function($q) {
$q->where('is_technical', '=', 1);
})->where($match)->orderByRaw("RAND()")->take(4)->get();

QueryBuilder/Doctrine Select join groupby

So recently I have been thinking and can't find a solution yet to this problem since my lack of development with doctrine2 and symfony query builder.
I have 2 tables:
Goals: id,user_id,target_value...
Savings: id,goal_id,amount
And I need to make a select from goals (all the informations in my table are from the goals table, except that I need to make a SUM(amount) from the savings table on each goal, so I can show the user how much did he saved for his goal)
This is the MySQL query:
select
admin_goals.created,
admin_goals.description,
admin_goals.goal_date,
admin_goals.value,
admin_goals.budget_categ,
sum(admin_savings.value)
from admin_goals
inner join admin_savings on admin_savings.goal_id=admin_goals.id
where admin_goals.user_id=1
group by admin_goals.id
It returns what I want but I have no idea how to implement it with doctrine or query builder, can you please show me an example in both ways?
I highly appreciate it !
I am going to assume you need this fields only and not your AdminGoals entity. On your AdminGoalsRepository you can do something like this:
public function getGoalsByUser(User $user)
{
$qb = $this->createQueryBuilder('goal');
$qb->select('SUM(savings.value) AS savings_value')
->addSelect('goal.created')
->addSelect('goal.description')
->addSelect('goal.goalDate')
->addSelect('goal.value')
->addSelect('goal.budgetCat') //is this an entity? it will be just an ID
->join('goal.adminSavings', 'savings', Join::WITH))
->where($qb->expr()->eq('goal.user', ':user'))
->groupBy('goal.id')
->setParameter('user', $user);
return $qb->getQuery()->getScalarResult();
}
Keep in mind that the return object will be an array of rows, each row is an associated array with keys like the mappings above.
Edit
After updating the question, I am going to change my suggested function but going to leave the above example if other people would like to see the difference.
First things first, since this is a unidirectional ManyToOne between AdminSavings and AdminGoals, the custom query should be in AdminSavingsRepository (not like above). Also, since you want an aggregated field this will "break" some of your data fetching. Try to stay as much OOP when you are not just rendering templates.
public function getSavingsByUser(User $user)
{
$qb = $this->createQueryBuilder('savings');
//now we can use the expr() function
$qb->select('SUM(savings.value) AS savings_value')
->addSelect('goal.created')
->addSelect('goal.description')
->addSelect('goal.goalDate')
->addSelect('goal.value')
->addSelect('goal.budgetCat') //this will be just an ID
->join('savings.goal', 'goal', Join::WITH))
->where($qb->expr()->eq('goal.user', ':user'))
->groupBy('goal.id')
->setParameter('user', $user);
return $qb->getQuery()->getScalarResult();
}
Bonus
public function FooAction($args)
{
$em = $this->getDoctrine()->getManager();
$user = $this->getUser();
//check if user is User etc depends on your config
...
$savings = $em->getRepository('AcmeBundle:AdminSavings')->getSavingsByUser($user);
foreach($savings as $row) {
$savings = $row['savings_value'];
$goalId = $row['id'];
$goalCreated = $row['created'];
[...]
}
[...]
}
If you use createQuery(), then you can do something like this:
$dqlStr = <<<"DSQL"
select
admin_goals.created,
admin_goals.description,
admin_goals.goal_date,
admin_goals.value,
admin_goals.budget_categ,
sum(admin_savings.value)
from admin_goals
inner join admin_savings on admin_savings.goal_id=admin_goals.id
where admin_goals.user_id=1
group by admin_goals.id
DSQL;
$em = $this->getDoctrine()->getManager();
$query = $em->createQuery($dqlStr);
$query->getResult();
On the other hand, if you would like to use createQueryBuilder(), you can check this link: http://inchoo.net/dev-talk/symfony2-dbal-querybuilder/

Laravel, Datatables, column with relations count

I have two models, User and Training, with Many to many relationship between them. I'm using the Laravel Datatables package to display a table of all the users. This is how the data controller method (which retrieves the query results and creates a Datatables table) looks like:
public function getData()
{
$users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->remove_column('id')
->make();
}
How can I add a column to the created table which displays the total number of relations for each user (that is, how many Trainings does each User have)?
The brute force way would be to try a User::selectRaw(...) which has a built in subquery to get the count of trainings for the user and expose it as a field.
However, there is a more built-in way to do this. You can eager load the relationship (to avoid the n+1 queries), and use the DataTables add_column method to add in the count. Assuming your relationship is named trainings:
public function getData() {
$users = User::with('trainings')->select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->add_column('trainings', function($user) {
return $user->trainings->count();
})
->remove_column('id')
->make();
}
The name of the column in add_column should be the same name as the loaded relationship. If you use a different name for some reason, then you need to make sure to remove the relationship column so it is removed from the data array. For example:
return \Datatables::of($users)
->add_column('trainings_count', function($user) {
return $user->trainings->count();
})
->remove_column('id')
->remove_column('trainings')
->make();
Edit
Unfortunately, if you want to order on the count field, you will need the brute force method. The package does its ordering by calling ->orderBy() on the Builder object passed to the of() method, so the query itself needs the field on which to order.
However, even though you'll need to do some raw SQL, it can be made a little cleaner. You can add a model scope that will add in the count of the relations. For example, add the following method to your User model:
Note: the following function only works for hasOne/hasMany relationships. Please refer to Edit 2 below for an updated function to work on all relationships.
public function scopeSelectRelatedCount($query, $relationName, $fieldName = null)
{
$relation = $this->$relationName(); // ex: $this->trainings()
$related = $relation->getRelated(); // ex: Training
$parentKey = $relation->getQualifiedParentKeyName(); // ex: users.id
$relatedKey = $relation->getForeignKey(); // ex: trainings.user_id
$fieldName = $fieldName ?: $relationName; // ex: trainings
// build the query to get the count of the related records
// ex: select count(*) from trainings where trainings.id = users.id
$subQuery = $related->select(DB::raw('count(*)'))->whereRaw($relatedKey . ' = ' . $parentKey);
// build the select text to add to the query
// ex: (select count(*) from trainings where trainings.id = users.id) as trainings
$select = '(' . $subQuery->toSql() . ') as ' . $fieldName;
// add the select to the query
return $query->addSelect(DB::raw($select));
}
With that scope added to your User model, your getData function becomes:
public function getData() {
$users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->selectRelatedCount('trainings')
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->remove_column('id')
->make();
}
If you wanted the count field to have a different name, you can pass the name of the field in as the second parameter to the selectRelatedCount scope (e.g. selectRelatedCount('trainings', 'training_count')).
Edit 2
There are a couple issues with the scopeSelectRelatedCount() method described above.
First, the call to $relation->getQualifiedParentKeyName() will only work on hasOne/hasMany relations. This is the only relationship where that method is defined as public. All the other relationships define this method as protected. Therefore, using this scope with a relationship that is not hasOne/hasMany throws an Illuminate\Database\Query\Builder::getQualifiedParentKeyName() exception.
Second, the count SQL generated is not correct for all relationships. Again, it would work fine for hasOne/hasMany, but the manual SQL generated would not work at all for a many to many relationship (belongsToMany).
I did, however, find a solution to both issues. After looking through the relationship code to determine the reason for the exception, I found Laravel already provides a public method to generate the count SQL for a relationship: getRelationCountQuery(). The updated scope method that should work for all relationships is:
public function scopeSelectRelatedCount($query, $relationName, $fieldName = null)
{
$relation = $this->$relationName(); // ex: $this->trainings()
$related = $relation->getRelated(); // ex: Training
$fieldName = $fieldName ?: $relationName; // ex: trainings
// build the query to get the count of the related records
// ex: select count(*) from trainings where trainings.id = users.id
$subQuery = $relation->getRelationCountQuery($related->newQuery(), $query);
// build the select text to add to the query
// ex: (select count(*) from trainings where trainings.id = users.id) as trainings
$select = '(' . $subQuery->toSql() . ') as ' . $fieldName;
// add the select to the query
return $query->addSelect(DB::raw($select));
}
Edit 3
This update allows you to pass a closure to the scope that will modify the count subquery that is added to the select fields.
public function scopeSelectRelatedCount($query, $relationName, $fieldName = null, $callback = null)
{
$relation = $this->$relationName(); // ex: $this->trainings()
$related = $relation->getRelated(); // ex: Training
$fieldName = $fieldName ?: $relationName; // ex: trainings
// start a new query for the count statement
$countQuery = $related->newQuery();
// if a callback closure was given, call it with the count query and relationship
if ($callback instanceof Closure) {
call_user_func($callback, $countQuery, $relation);
}
// build the query to get the count of the related records
// ex: select count(*) from trainings where trainings.id = users.id
$subQuery = $relation->getRelationCountQuery($countQuery, $query);
// build the select text to add to the query
// ex: (select count(*) from trainings where trainings.id = users.id) as trainings
$select = '(' . $subQuery->toSql() . ') as ' . $fieldName;
$queryBindings = $query->getBindings();
$countBindings = $countQuery->getBindings();
// if the new count query has parameter bindings, they need to be spliced
// into the existing query bindings in the correct spot
if (!empty($countBindings)) {
// if the current query has no bindings, just set the current bindings
// to the bindings for the count query
if (empty($queryBindings)) {
$queryBindings = $countBindings;
} else {
// the new count query bindings must be placed directly after any
// existing bindings for the select fields
$fields = implode(',', $query->getQuery()->columns);
$numFieldParams = 0;
// shortcut the regex if no ? at all in fields
if (strpos($fields, '?') !== false) {
// count the number of unquoted parameters (?) in the field list
$paramRegex = '/(?:(["\'])(?:\\\.|[^\1])*\1|\\\.|[^\?])+/';
$numFieldParams = preg_match_all($paramRegex, $fields) - 1;
}
// splice into the current query bindings the bindings needed for the count subquery
array_splice($queryBindings, $numFieldParams, 0, $countBindings);
}
}
// add the select to the query and update the bindings
return $query->addSelect(DB::raw($select))->setBindings($queryBindings);
}
With the updated scope, you can use the closure to modify the count query:
public function getData() {
$users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->selectRelatedCount('trainings', 'trainings', function($query, $relation) {
return $query
->where($relation->getTable().'.is_creator', false)
->where($relation->getTable().'.is_speaker', false)
->where($relation->getTable().'.was_absent', false);
})
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->remove_column('id')
->make();
}
Note: as of this writing, the bllim/laravel4-datatables-package datatables package has an issue with parameter bindings in subqueries in the select fields. The data will be returned correctly, but the counts will not ("Showing 0 to 0 of 0 entries"). I have detailed the issue here. The two options are to manually update the datatables package with the code provided in that issue, or to not use parameter binding inside the count subquery. Use whereRaw to avoid parameter binding.
I would setup your DB tables and Eloquent models using the conventions provided at http://laravel.com/docs/4.2/eloquent. In your example you would have three tables.
trainings
training_user
users
Your models would look something like this.
class Training {
public function users() {
return $this->belongsToMany('User');
}
}
class User {
public function trainings() {
return $this->belongsToMany('Training');
}
}
You can then use Eloquent to get a list of users and eager load their trainings.
// Get all users and eager load their trainings
$users = User::with('trainings')->get();
If you want to count the number of trainings per user you can simply iterate over $users and count the size of the trainings array.
foreach ( $users as $v ) {
$numberOfTrainings = sizeof($v->trainings);
}
Or you can simply do it in pure SQL. Note that my example below assumes you follow Laravel's conventions for naming tables and columns.
SELECT
u.*, COUNT(p.user_id) AS number_of_trainings
FROM
users u
JOIN
training_user p ON u.id = p.user_id
GROUP BY
u.id
Now that you have a couple of ways to count the number of relations, you can use whatever method you like to store that value somewhere. Just remember that if you store that number as a value in the user table you'll need to update it every time a user creates/updates/deletes a training (and vice versa!).

What is a good approach in dealing with a set of select statements but different conditions?

One problem that I am facing is having many queries with similar select statements but different join/where statements.
Below is an example of a code that I am working on via CodeIgniter. What I normally do is make one function, get(), that accepts an array of random keys/values. Depending on what keys/values are passed, it will generate and run the appropriate query. Now, I am wondering is this a good way of doing things? Because as you can see, this function becomes more and more complex. Initially, I had a bunch of functions such as get_all(), get_only_lessons(), etc but it becomes kinda annoying having to repeat the same set of code with one or two lines that are different.
My question is what is the best way with dealing with this problem.
function get($param = NULL)
{
/*
SELECT m.id AS id, CAST(m.order_number AS SIGNED) AS order_number, m.name AS name, m.permalink as permalink,
m.suplesson_id as suplesson_id, CAST(sm.order_number AS SIGNED) AS suplesson_order_number
FROM lessons m
JOIN courses c ON m.course_id = c.id
LEFT JOIN lessons sm ON m.suplesson_id = sm.id
WHERE [various]
*/
$select = 'm.id AS id, CAST(m.order_number AS SIGNED) AS order_number, m.name AS name, m.permalink as permalink, ';
$select .= ' m.suplesson_id as suplesson_id';
if (isset($param['id']) || isset($param['suplesson_order_number']) || isset($param['permalink']))
$select .= ', CAST(sm.order_number AS SIGNED) AS suplesson_order_number ';
$this->db->select($select);
$this->db->from($this->table_name.' m');
$this->db->join($this->courses_table_name.' c', 'm.course_id = c.id');
if (isset($param['id']) || isset($param['suplesson_order_number']) || isset($param['permalink']))
$this->db->join($this->table_name.' sm', 'm.suplesson_id = sm.id', 'left');
// where clauses
if (isset($param['course_id']))
$this->db->where(array('c.id' => $param['course_id']));
if (isset($param['id']))
$this->db->where(array('m.id' => $param['id']));
if (isset($param['order_number']))
$this->db->where(array('m.order_number' => $param['order_number']));
if (isset($param['permalink']))
$this->db->like('m.permalink', $param['permalink'], 'none');
if (isset($param['suplesson_id']))
$this->db->where(array('m.suplesson_id' => $param['suplesson_id']));
if (isset($param['suplesson_order_number']))
$this->db->where(array('sm.order_number' => $param['suplesson_order_number']));
if (isset($param['NULL']))
$this->db->where('m.'.$param['NULL'].' IS NULL');
if (isset($param['NOT NULL']))
$this->db->where('m.'.$param['NOT NULL'].' IS NOT NULL');
$this->db->order_by('order_number');
// filter based on num_rows/offset
if (isset($param['id']) || isset($param['permalink']))
$this->db->limit(1);
if (isset($param['num_rows']) && isset($param['offset']))
$this->db->limit($param['num_rows'], $param['offset']);
$query = $this->db->get();
// return row if expecting 1 result
if (isset($param['id']) || isset($param['suplesson_order_number']) || isset($param['permalink']))
return ($query->num_rows() == 1) ? $query->row_array() : NULL;
return ($query->num_rows() > 0) ? $query->result_array() : NULL;
}
The usual way of running DB queries is to structure your model code to have multiple function calls, each one relating to one SQL statement, for example:
function get_user($userId)
{
$this->db->get_where('user', array('userId' => $userId))
//...
//...
}
function delete_user($userId)
{
$this->db->delete('user',array('userId' => $userId))
}
You might create a model class called User_model which contains all of the functions you need for reading/updating the user table, so in your controller you call down to the specific model function
$user = $this->User_model->get_user($userId)
It looks like you're trying to construct one giant model function which checks various parameters in order to determine what SQL statement to run. This isn't good design and doesn't fit well with Codeignitors MVC model.
Instead, create a separate model for each table, then in each model create separate functions for each SQL operation you wish to run. Call these models from your controllers to get/update/delete data in your tables.

Categories