Join subquery in Laravel model - mysql

Please suggest a better title for this question. I have problems to name my question properly.
Background
I'm creating a comic database for personal use, to track my comic reads. Every comic belongs to a series. Every comics has a release date. The release date of a series is the first release of the according comic. I have a eloquent function seriesByDate() for that:
class Series extends Model
{
public $timestamps = false;
protected $primaryKey = 'series_id';
protected $fillable = ['series_name', 'publisher_id'];
public function publisher()
{
return $this->belongsTo(Publisher::class, 'publisher_id');
}
public function comics()
{
return $this->hasMany(Comic::class, 'series_id', 'series_id');
}
// instead of saving the release date of a complete series
// we look for the first comic in this series and get the
// comic's release date.
public static function seriesByDate()
{
$firstRelease = DB::table('comics')
->select('series_id', DB::raw('MIN(comic_release_date) as first_release'))
->groupBy('series_id');
$seriesByDate = DB::table('series')
->leftJoinSub($firstRelease, 'first_release', function ($join) {
$join->on('series.series_id', '=', 'first_release.series_id');
})
->join('publishers', 'publishers.publisher_id', '=', 'series.publisher_id')
->select('series.series_id', 'series.series_name', 'first_release', 'publishers.publisher_name')
->get();
return $seriesByDate;
}
What i want
I want the release_date somehow be permanent to my Series model. Meaning: When I do a App\Series::all() i already want to have the release_date as a column in my returned data. Similar to App\Series::with('publishers')->get()
With my solution above i have to eplicitly execute App\Series::seriesByDate()
Is this even possible? Can you please give me a hint?
Edit / Update
The linked video by #Musa shows how to properly do this in a model: https://stackoverflow.com/a/61558482/5754486

You can't. There is no magic for this. You might eventually write your own custom Relation but that would be unnecessarily complex, just for the sake of having a pretty related/accessor. Both solutions are not great performance-wise.
Not sure why you choose such a structure. Without any further context/explanation, I would strongly recommend you to have a release_date column directly inside your Series model as well. That will be waaaay faster than your current structure.
If you still want to stick with that structure, I would personally retrieve the release_date "php side" instead of "database side" :
$series = App\Series::query()
->with([
'publishers',
'comics' => function ($query) {
$query->orderBy('created_at');
},
])
->get();
foreach ($series as $serie) {
$serieTitle = $serie->title;
$releaseDate = $serie->comics->first()->created_at;
echo $serieTitle.' was first released '.$releaseDate->diffForHumans().'<br/>';
}
(not tested)
the only downside is that it will return a Collection of every "comics" a "serie" has. If you do not have 10k comics per serie and you do not load 1k serie per page, that should be fine. In any case, this looks more elegant and optimized/faster than your seriesByDate method.
edit: also, you should watch "Advanced Querying With Eloquent" by Jonathan Reinink, at Laracon 2018 I believe. He discusses subqueries like the one you need. I am 100% sure you will find the best and most optimal Eloquent subquery one can forge for what you are trying to achieve : https://vimeo.com/showcase/7060635/video/255049572

you can defined an accessor then append the value
class Series extends Model
{
protected $appends = ['series_date'];
public function getSeriesDateAttribute()
{
return self::seriesByDate();
//OR build 'seriesByDate' manually, returning whatever you like.
}
}

Related

How to make query according to my problem in Laravel 8?

Image drive link: Click
My database looks like the photos. Now I need a query result that i describe in two step for better understandind.
step 01: I need these result where syllab_subjects have a specific syllab id. $query_step1 = SyllabSubject::where('syllab_id',$given_id)->get()
step 02: I need only these subject which is not exist in $query_step1. Simply, In $query_step1 check which subject_id is not available when compare with subject table.
For better understanding, if we make query for syllab_id 10, then query result returns "science" and if syllab_id 5 then it returns "English and Science"
Have you set up models with relationships for these tables? It looks like you haven't. What you need is a model like
class Subject extends Model
{
public function syllab() {
return $this->hasMany(Syllab::class):
}
}
and
class Syllab extends Model
{
public function subject(){
return $this->belongsTo(Subject::class);
}
}
Then you can get the details like
$syllab = Syllab::find(10);
$syllab->subjects->pluck('name');

getting records by id and querying there relation with 'wherebetween' using Laravel and eloquent

i'm trying to get all measurements from a certain recorder between a certain timespan.
If i remove the "->wherebetween()" part of the query and view the results then I get all the sensors of that recorder and all related measurements of that sensor.
But I'm not able to execute a wherebetween on the relation.
query in the controller
public function getChart(Request $request) {
$sensorCollection = Sensor::where('recorder_id', $request->recorder_id)
->with('getMeasurementsRelation')
->wherebetween('getMeasurementsRelation', function ($query) use ($request) {
return $query->wherebetween('timestamp',[$request->start_chart, $request->end_chart]);})
->get();
}
Relationship in Sensor model
public function getMeasurementsRelation() {
return $this->hasmany('App\Models\measurement', 'sensor_id', 'id');}
You can use callback in with method like below.Since you have not mentioned start and end chart value format .So i assume its Y-m-d format .if not let me know in comment i can modify my answer according to your need
$sensorCollection = Sensor::where('recorder_id', $request->recorder_id)
->with(['getMeasurementsRelation'=>function($query)use($request){
$startChart=\Carbon\Carbon::createFromFormat('Y-m-d',$request->start_chart)->startOfDay();
$endChart=\Carbon\Carbon::createFromFormat('Y-m-d',$request->end_chart)->endOfDay();
$query->wherebetween('timestamp',[$startChart, $endChart]);
}])
->get();

Yii2 relation based on attribute values instead of keys

I have 2 tables in the db (mysql), and between the 2 there is no classic relationship through keys or ids. The only way I could define relationship would be through attribute values. E.g. table wheel and car and certain wheels would match certain cars because of the size only. Can it be defined on DB level, and/or in yii2, and if yes, how?
In the relations I can add an onCondition(), but you have to define an attribute (???), too:
public function getWheels() {
return $this->hasMany(\app\models\Wheel::className(), ['???' => '???'])->onCondition(['<', 'wheelsize', $this->wheelsize]);
}
I could use a fake attribute and set it in all records like to 1, but it seems a little bit odd for me.
I find nothing on the web regarding this or maybe I'm just searching the wrong way, or maybe I'm trying something that's totally bad practice. Can you please point me to the right direction?
Hypothetically you can set an empty array as a link, but for security reasons (I think) the condition "0 = 1" is automatically added in the select.
I faced your own problem several times and the best solution I could find was to use ActiveQuery explicitly (similar to what happens for hasOne and hasMany):
public function getWheels() {
return new ActiveQuery(\app\models\Wheel::className(), [
'where' => 'my condition' // <--- inserte here your condition as string or array
'multiple' => true // true=hasMany, false=hasOne
// you can also add other configuration params (select, on condition, order by, ...
]);
}
This way you can get both the array and the ActiveQuery to add other conditions:
var_dump($model->wheels); // array of wheels objects
var_dump($model->getWheels()); // yii\db\ActiveQuery object
$model->getWheels()->andWhere(...); // customize active query
I don't think that you could achieve this through relation.
But there is a way to work around the limitation.
<?php
namespace app\models;
class Car extend \yii\db\ActiveRecord
{
/**
* #var \app\models\Wheel
*/
private $_wheels;
/**
* #return \app\models\Wheel[]
*/
public function getWheels()
{
if (!$this->_wheels) {
$this->_wheels = Wheel::find()
->where(['<', 'wheelsize', $this->wheelsize])
//->andWhere() customize your where here
->all();
}
return $this->_wheels;
}
}
Then you could access the wheels attribute just as relation does.
<?php
$car = Car::find(1);
$car->wheels;
Beware that this way does not support Eager Loading

Disable Doctrine automatic queries

With Symfony 4.2 and Doctrine, I want to disable automatic queries.
If I execute this simple example :
$posts = $em->getRepository(Post::class)->findAll();
foreach ($posts as $post) {
dump($post->getCategory()->getName();
}
Doctrine will search categories by itself. But I want to disable that. to force me to join (LEFT or INNER in repository).
It's possible ? Thanks
Implicit data fetching from database by accessing linked entity properties is one of core principles of Doctrine and can't be disabled. If you want to just fetch some data explicitly - you need to construct your own partial query and hydrate data either as array or simple object so your fetched results will not became entities.
Nothing can automatically disable this behavior and force you to write JOIN clauses, except your wishes.
This behavior (which is called lazy loading) is one of the main common behavior of all ORMs.
If you are not happy with this (and you probably have good reasons), then consider writing your own DQL queries, which are limited to your selected fields. What is not in your query will not be fetched automatically afterwards.
Write this method in your custom PostRepository class :
public function findAll(){
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('p')
->from('Post', 'p');
return $qb->getQuery()->getResult();
}
Then in your controller, you can do the same as before:
$posts = $em->getRepository(Post::class)->findAll();
foreach ($posts as $post) {
dump($post->getCategory()->getName();
}
Calling the getName() method from the Category entity will now throws an error, and will not launch any hidden SQL queries behind. If you want to play with Posts and Categories together, then you can adapt your findAll() method like this :
public function findAll(){
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('p, c')
->from('Post', 'p');
->join('p.category', 'c')
return $qb->getQuery()->getResult();
}

Laravel - Eloquent - Filter based on latest HasMany relation

I have this two models, Leads and Status.
class Lead extends Model
{
public function statuses() {
return $this->hasMany('App\LeadStatus', 'lead_id', 'id')
->orderBy('created_at', 'DESC');
}
public function activeStatus() {
return $this->hasOne('App\LeadStatus', 'lead_id', 'id')
->latest();
}
}
class LeadStatus extends Model
{
protected $fillable = ['status', 'lead_id'];
}
This works fine, now I'm trying to get all Leads based on the 'status' of the last LeadStatus.
I've tried a few combinations with no success.
if ($search['status']) {
$builder = $builder
->whereHas('statuses', function($q) use ($search){
$q = $q->latest()->limit(1);
$q->where('status', $search['status']);
});
}
if ($search['status']) {
$builder = $builder
->whereHas('status', function($q) use ($search){
$q = $q->latest()->Where('status', $search['status']);
});
}
Has anybody done this with Eloquent? Do I need to write some raw SQL queries?
EDIT 1: I'll try to explain again :D
In my database, the status of a lead is not a 1 to 1 relation. That is because I want to have a historic list of all the statuses which a Lead has had.
That means that when a Lead is created, the first LeadStatus is created with the status of 'new' and the current date.
If a salesman comes in, he can change the status of the lead, but this DOES NOT update the previous LeadStatus, instead it creates a new related LeadStatus with the current date and status of 'open'.
This way I can see that a Lead was created on 05/05/2018 and that it changed to the status 'open' on 07/05/2018.
Now I'm trying to write a query using eloquent, which only takes in count the LATEST status related to a Lead.
In the previous example, if I filter by Lead with status 'new', this Lead should not appear as it has a status of 'open' by now.
Hope this helps
Try this:
Lead::select('leads.*')
->join('lead_statuses', 'leads.id', 'lead_statuses.lead_id')
->where('lead_statuses.status', $search['status'])
->where('created_at', function($query) {
$query->selectRaw('max(created_at)')
->from('lead_statuses')
->whereColumn('lead_id', 'leads.id');
})->get();
A solution using the primary key (by Borjante):
$builder->where('lead_statuses.id', function($query) {
$query->select('id')
->from('lead_statuses')
->whereColumn('lead_id', 'leads.id')
->orderBy('created_at', 'desc')
->limit(1);
});
I had this same problem and posted my solution here but I think it's worth re-posting as it improves on the re-usability. It's the same idea as the accepted answer but avoids using joins, which can cause issues if you want to eager load relations or use it in a scope.
The first step involves adding a macro to the query Builder in the AppServiceProvider.
use Illuminate\Database\Query\Builder;
Builder::macro('whereLatestRelation', function ($table, $parentRelatedColumn)
{
return $this->where($table . '.id', function ($sub) use ($table, $parentRelatedColumn) {
$sub->select('id')
->from($table . ' AS other')
->whereColumn('other.' . $parentRelatedColumn, $table . '.' . $parentRelatedColumn)
->latest()
->take(1);
});
});
This basically makes the sub-query part of the accepted answer more generic, allowing you to specify the join table and the column they join on. It also uses the latest() function to avoid referencing the created_at column directly. It assumes the other column is an 'id' column, so it can be improved further. To use this you'd then be able to do:
$status = $search['status'];
Lead::whereHas('statuses', function ($q) use ($status) {
$q->where('status', $userId)
->whereLatestRelation((new LeadStatus)->getTable(), 'lead_id');
});
It's the same logic as the accepted answer, but a bit easier to re-use. It will, however, be a little slower, but that should be worth the re-usability.
If I understand it correctly you need / want to get all Leads with a specific status.
So you probably should do something like this:
// In your Modal
public function getLeadById($statusId)
{
return Lead::where('status', $statusId)->get();
// you could of course extend this and do something like this:
// return Lead::where('status', $statusId)->limit()....->get();
}
Basically I am doing a where and returning every lead with a specific id.
You can then use this function in your controller like this:
Lead::getLeadById(1)