Symfony2 + Doctrine - Filtering - sql

I've got a OneToMany relationship where one football team has many players. I want to list all football teams and display the name of the captain for each team.
Each player entity has a foreign key (team_id) and a field 'captain' which is set to 0 or 1. I'm currently running the following query:
$teams = $this
->getDoctrine()
->getRepository('FootballWebsiteBundle:Team')
->createQueryBuilder('t')
->setFirstResult(($pageNumber * $resultPerPage) - $resultPerPage)
->setMaxResults($resultPerPage)
->add('where','t.deleted = 0')
->add('orderBy', 't.name DESC')
->getQuery()->getResult();
Then when I loop through each team in twig I run team.getTeamCaptain().getName() which is a filter within my Team entity:
public function getTeamCaptain() {
$them = $this->players->filter(function($p) {
return $p->getCaptain() == 1;
});
return $them->first();
}
Is there a better way to run this query?

First of all, you may want to fetch-join the players of each retrieved team to avoid having them lazy loaded during rendering of the template. Here's the DQL:
SELECT
t, p
FROM
FootballWebsiteBundle:Team t
LEFT JOIN
t.players p
WHERE
t.deleted = 0
ORDER BY
t.name DESC
Which can be built with following query builder API calls:
$teamsQuery = $this
->getDoctrine()
->getRepository('FootballWebsiteBundle:Team')
->createQueryBuilder('t')
->addSelect('p')
->leftJoin('t.players', 'p')
->add('where','t.deleted = 0')
->add('orderBy', 't.name DESC')
->getQuery()
Then you wrap this query into a Paginator object (since setMaxResults and setFirstResult cannot be trusted when fetch-joining):
$paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($teamsQuery, true);
$teamsQuery
->setFirstResult(($pageNumber * $resultPerPage) - $resultPerPage)
->setMaxResults($resultPerPage)
In your view you can then iterate on the teams like following pseudo-code:
foreach ($paginator as $team) {
echo $team->getTeamCaptain() . "\n";
}
You can also gain some extra performance in your getTeamCaptain method by using the Selectable API:
public function getTeamCaptain() {
$criteria = new \Doctrine\Common\Collections\Criteria();
$criteria->andWhere($criteria->expr()->eq('captain', 1));
return $this->players->matching($criteria)->first();
}
The advantage here is mainly relevant when the association players is not yet initialized, since this will avoid loading it entirely. This is not the case, but I consider it a good practice (instead of re-inventing collection filtering logic).

Related

Transforming Raw Sql to Laravel equolent

I have written this SQL code
SELECT drugs.*, COUNT(*) as 'views' from drugs INNER JOIN drug_seen on drugs.id = drug_seen.drug_id GROUP BY drugs.id order by views ASC
And now I am trying to write in in the Laravel equolent but I am facing some troubles.
This is what I have tried
$drugs = Drug::select(DB::raw('drugs.*,count(*) as views'))
->join('drug_seen', 'drugs.id', 'drug_seen.drug.id')
->groupBy('drug.id')->orderByRaw('views');
I am having errors like column not found i think the code is not written properly
Drug class
class Drug extends Model
{
use HasFactory;
use SoftDeletes;
...
...
...
public function drugVisits()
{
return $this->hasMany(DrugSeen::class);
}
Hop this will solve your problem.
$drugs = Drug::with('drugVisits')->get();
$drugs->count(); //for total records in drugs table.
You have typo error in join instead on drug_id you use drug.id
Try this:
$drugs = Drug::select(DB::raw('drugs.*,count(*) as views'))
->join('drug_seen', 'drugs.id', 'drug_seen.drug_id')
->groupBy('drugs.id')->orderByRaw('views');
}
As soon as you use join() you're leaving Eloquent and entering Query\Builder, losing the benefits of Model configurations in the process. And with() eager-loads aren't the answer, if you're looking to filter the results by both tables. What you want is whereHas().
Also, as far as your grouping and count manipulation there, I think you're looking more for Collection handling than SQL groups.
$drugModel = app(Drugs::class);
$results = $drugModel->whereHas('drugVisits')->with('drugVisits')->get();
$organizedResults = $results
->groupBy($drugModel->getKey())
->sortyBy(function (Drugs $drugRecord) {
return $drugRecord->drugVisits->count();
});
If you want to have a 'views' property that carries the count in the root-level element, it would look like this:
$drugModel = app(Drugs::class);
$results = $drugModel->whereHas('drugVisits')->with('drugVisits')->get();
$organizedResults = $results
->groupBy($drugModel->getKey())
->map(function (Drugs $drugRecord) {
$drugRecord->views = $drugRecord->drugVisits->count();
return $drugRecord;
});
->sortyBy('views');

Optimizing EF flattening

I have a similar case to the following:
Say there's a number of jobs to be done and for each job there's a history of workers where only one worker is active per job. There's three tables: the Job itself, a mapping table JobWorkers which holds the history of workers for a given job (including a datetime "To" which indicates whether still active (null) or when assignment was cancelled (end date)) and Workers which have a first and last name.
I'd like to query a list of all jobs and the first and last name of the currently assigned worker as flat model. This is the code I'm executing:
var jobExample = dbContext.Jobs.Select(j => new
{
j.JobId,
// ...some other columns from jobs table
j.JobWorker.FirstOrDefault(jw => jw.To == null).Worker.FirstName, // first name of currently assigned worker
j.JobWorker.FirstOrDefault(jw => jw.To == null).Worker.LastName // last name of currently assigned worker
}).First();
The following SQL query is generated:
SELECT TOP (1)
[Extent1].[JobId] AS [JobId],
[Extent3].[FirstName] AS [FirstName],
[Extent5].[LastName] AS [LastName]
FROM [tables].[Jobs] AS [Extent1]
OUTER APPLY (SELECT TOP (1)
[Extent2].[WorkerId] AS [WorkerId]
FROM [tables].[JobWorkers] AS [Extent2]
WHERE ([Extent1].[JobId] = [Extent2].[JobId]) AND ([Extent2].[To] IS NULL) ) AS [Limit1]
LEFT OUTER JOIN [tables].[Workers] AS [Extent3] ON [Limit1].[WorkerId] = [Extent3].[WorkerId]
OUTER APPLY (SELECT TOP (1)
[Extent4].[WorkerId] AS [WorkerId]
FROM [tables].[JobWorkers] AS [Extent4]
WHERE ([Extent1].[JobId] = [Extent4].[JobId]) AND ([Extent4].[To] IS NULL) ) AS [Limit2]
LEFT OUTER JOIN [tables].[Workers] AS [Extent5] ON [Limit2].[WorkerId] = [Extent5].[WorkerId]
As one can see there're two outer apply/left outer joins that are identical. I'd like to get rid of one of those to make the query more performant.
Note that the select statement is dynamically generated based on what information the user actually wants to query. But even if this didn't apply I'm not sure how to do this without having a hierarchic structure and then only afterwards flatten it in .NET
Thanks for your help and if I can improve this question in any way please comment.
You've probably seen that there are two types of LINQ methods: the ones that return IQueryable<...>, and the other ones.
Methods of the first group use deferred execution. This means, that the query is made, but not executed yet. Your database is not contacted.
Methods of the second group, like ToList(), FirstOrDefault(), Count(), Any(), will execute the query: they will contact the database, and fetch the data that is needed to calculate the result.
This is the reason, that you should try to postpone any method of the second group to as last as possible. If you do it earlier, and you do something LINQy after it, changes are that you fetch to much data, or, as in your case: that you do execute the same code twice.
The solution is: move your FirstOrDefault to a later moment.
var jobExample = dbContext.Jobs.Select(job => new
{
Id = job.JobId,
... // other job properties
ActiveWorker = job.JobWorkers
.Where(jobWorker => jobWorker.To == null)
.Select(worker => new
{
FirstName = worker.FirstName,
LastName = worker.LastName,
})
.FirstOrDefault(),
})
.FirstOrDefault();
The result is slightly different than yours:
Id = 10;
... // other Job properties
// the current active worker:
ActiveWorker =
{
FirstName = "John",
LastName = "Doe",
}
If you really want an object with Id / FirstName / LastName, add an extra Select before your final FirstOrDefault:
.Select(jobWithActiveWorker => new
{
Id = jobWithActiveWorker.Id,
... // other Job properties
// properties of the current active worker
FirstName = jobWithActiveWorker.FirstName,
LastName = jobWithActiveWorker.LastName,
})
.FirstOrDefault();
Personally I think that you should not mix Job properties with Worker properties, so I think the first solution: "Job with its currently active worker" is neater: the Job properties are separated from the Worker properties. You can see why that is important if you also wanted the Id of the active worker:
.Select(job => new
{
Id = job.JobId,
... // other job properties
ActiveWorker = job.JobWorkers
.Where(jobWorker => jobWorker.To == null)
.Select(jobworker => new
{
Id = jobworker.Id,
FirstName = jobworker.FirstName,
LastName = jobworker.LastName,
})
.FirstOrDefault(),
})
.FirstOrDefault();
Try rewriting your query like this:
var query =
from j in dbContext.Jobs
let ws = j.JobWorker
.Where(jw => jw.To == null)
.Select(jw => jw.Worker)
.Take(1)
from w in ws.DefaultIfEmpty()
select new
{
j.JobId,
// other properties
w.FirstName,
w.LastName,
};
The query processor probably could not have optimized any further to know it could use the subquery once.

Laravel/SQL: How to fetch data from multiple table in a single query? that too using 'where'

Working on a search functionality on Laravel App(Blog/Posts).
There are multiple types of posts (each having a separate table in the database)
Like Business posts, Social Life posts etc..
Below is the search function on SearchController
class SearchController extends Controller
{
public function search(Request $request, $query = null)
{
if($query == null)
return redirect()->route('home');
$search = Business::where([['title','like','%'.$query.'%'],['status','=',1]])
->orWhere([['description','like','%'.$query.'%'],['status','=',1]])
->paginate(10);
return view('front.search',[
'results' => $search,
'query' => $query
]);
}
}
So basically my question is how to add other types of Post's table also?
My main motive is that when someone searches for anything, the result should be fetched from all types of posts table(business, nature, life & so on..).
You have to maintain common id in both the table
NOTE: Join is the preferable method
$querys = DB::table('Business')->where([['Business.title','like','%'.$query.'%'],['Business.status','=',1]])
->orWhere([['Business.description','like','%'.$query.'%'],['Business.status','=',1]]);
$querys->join('socialtable','socialtable.userid','=','Business.userid');
// Just join the social table
$querys->where('socialtable.title', 'like','%'.$query.'%');
$result = $querys->paginate(10);
If you have a model called Book, like this:
class Book extends Model
{
/**
* Get the author that wrote the book.
*/
public function author()
{
return $this->belongsTo('App\Author');
}
}
Then you can retrieve all of your books with authors like this:
$books = App\Book::with(['author'])->get();
Check out Eager loading from Laravel documentation.
Just add table name before every field
$querys = DB::table('Business')->where([['Business.title','like','%'.$query.'%'],['Business.status','=',1]])
->orWhere([['Business.description','like','%'.$query.'%'],['Business.status','=',1]]);
$querys->join('socialtable','socialtable.userid','=','Business.userid');
// Just join the social table
$querys->where('socialtable.title', 'like','%'.$query.'%');
$result = $querys->paginate(10);

Sub-query, group and having in SilverStripe ORM

I'm using SilverStripe ORM.
I need to get all companies that are at least in ALL of the desired Specializing (no less):
class CompanyPage extends Page {
private static $many_many = [
"Specializings" => "Specializing",
];
}
class Specializing extends DataObject {
private static $belongs_many_many = [
"CompanyPages" => "CompanyPage",
];
}
I have an array filled with desiring specializing ids ( $specIds ).
This gets companies that are at at least in one of the desired specializing, but it isn't that I need.
$companyPages = CompanyPage::get()->filter([
"Specializings.ID:ExactMatch" => $specIds,
]);
I can achive my task with this SQL-query
select * from CompanyPage c1 join (
select c.ID from CompanyPage c
join CompanyPage_Specializings cs on cs.CompanyPageID = c.ID
where cs.SpecializingID in (14,15)
group by c.ID having count(c.ID) = 2
) c2 on c1.ID = c2.ID
But how can I achive it with SilverStripe ORM?
I can't find anything about having by aggregated functions in ORM
You can use SQLQuery to build more complex DB queries in SilverStripe or if you want to run the RAW SQL you can use DB:Query()
https://docs.silverstripe.org/en/3/developer_guides/model/sql_query/
Both of these will return a MySQLResultSet rather than a SS_List which means when you iterate over the code you would use something like this
foreach($results AS $result){
$varname = $results['propertyname'];
}
rather than the below if you used the straight SS ORM.
foreach($results AS $result){
$varname = $results->propertyname;
}
Once you have the MySQLResult set you could iterate through it to build an ArrayList which you can use in your template or alternatively use this Module to convert the MySQLResultSet to a SS_List (just remember if you use this module that the relationship benefit that you get from the straight ORM isn't maintained - e.g. $has_one relationships etc).
https://github.com/burnbright/silverstripe-sqlquerylist
I hope this helps.

Preserve Order of IN in ORM Order

I'm trying to do a query where I preserve the order of the ids in a IN statement. I can't seem to do it with either the Model Manage Query Builder or the standard ORM 'order' array parameter. Am I missing something? I keep getting:
UNEXPECTED TOKEN IDENTIFIER(, NEAR TO 'id`enter code here`,17743,16688,16650
Here's my model manager:
$query = $this->modelsManager->createQuery('SELECT * FROM Projects WHERE id IN ('.implode(',', array_keys($finalIterations)).')
ORDER BY FIELD(id,'.implode(',', array_keys($finalIterations)).'');
It's pretty obvious PhQL doesn't like the FIELD key word. Is there a way for me to do what I'm trying to do with PhQL? It seems I will not be able to do what I need to.
Unfortunately as previously said, this is missing a feature in Phalcon.
Have a look at this function, I've put it into my ModelBase abstract class which is parent class of all my models. It uses PhQL variable binding, so it's safe for handling direct user input.
You could have reimplemented custom \Phalcon\Mvc\Model\Criteria but this solution seems to be easier to work with, at least for me.
ModelBase abstract
public function appendCustomOrder( \Phalcon\Mvc\Model\CriteriaInterface &$criteria, $orderField, array &$orderValues = [] ) {
if(!empty($orderValues)) {
$queryKeys = $bindParams = [];
foreach($orderValues as $key => $id) {
$queryKey = 'pho'.$key;
$queryKeys[] = ':'.$queryKey.':';
$bindParams[$queryKey] = $id;
}
// TODO: add support for multiple orderBy fields
$criteria->orderBy('FIELD('.$orderField.','.implode(',',$queryKeys).')');
// there's no 'addBind' function, need to merge old parameters with new ones
$criteria->bind( array_merge( (array) #$criteria->getParams()['bind'], $bindParams ) );
}
}
Controller usage
$projectIDs = [17743, 16688, 16650];
$projectsModel = new Projects();
$criteria = $projectsModel->query->inWhere( 'id', $projectIDs );
$projectsModel->appendCustomOrder( $criteria, 'id', $projectIDs );
$projectsData = $criteria->execute();
This will generate valid PhQL syntax similar to this one:
SELECT `projects`.`id` AS `id`, `projects`.`title` AS `title`
FROM `projects`
WHERE `projects`.`id` IN (:phi0, :phi1, :phi2)
ORDER BY FIELD(`projects`.`id`, :pho0, :pho1, :pho2)