One to many relationship count - difference in accessing relationship - laravel

I have one to many relation - Entry can have many Visits.
In my Entry model I have the following methods:
public function visits() {
return $this->hasMany ('Visit', 'entry_id','id');
}
public function visitsCount() {
return $this->hasMany('Visit', 'entry_id','id')
->selectRaw('SUM(number) as count')
->groupBy('entry_id');
}
In Blade I can get number of visits for my entry using:
{{$entry->visits()->count() }}
or
{{ $entry->visitsCount()->first()->count }}
If I want to create accessor for getting number of visits I can define:
public function getNrVisitsAttribute()
{
$related = $this->visitsCount()->first();
return ($related) ? $related->count : 0;
}
and now I can use:
{{ $entry->nr_visits }}
Questions:
In some examples I saw defining such relation this way:
public function getNrVisitsAttribute()
{
if (!array_key_exists('visitsCount', $this->relations)) {
$this->load('visitsCount');
}
$related = $this->getRelation('visitsCount')->first();
return ($related) ? $related->count : 0;
}
Question is: what's the difference between this and the "simple method" I showed at the beginning? Is it quicker/use less resource or ... ?
Why this method doesn't work in this case? $related is null so accessor return 0 whereas using "simple method" it returns correct number of visits
I've tried also changing in visitsCount method relationship from hasMany to hasOne but it doesn't change anything.

1 Your relation won't work because you didn't select the foreign key:
public function visitsCount() {
// also use hasOne here
return $this->hasOne('Visit', 'entry_id','id')
->selectRaw('entry_id, SUM(number) as count')
->groupBy('entry_id');
}
2 Your accessor should have the same name as the relation in order to make sense (that's why I created those accessors in the first place):
public function getVisitsCountAttribute()
{
if ( ! array_key_exists('visitsCount', $this->relations)) $this->load('visitsCount');
$related = $this->getRelation('visitsCount');
return ($related) ? $related->count : 0;
}
This accessor is just a handy way to call the count this way:
$entry->visitsCount;
instead of
$entry->visitsCount->count;
// or in your case with hasMany
$entry->visitsCount->first()->count;
So it has nothing to do with performance.
Also mind that it is not defining the relation differently, it requires the relation to be defined like above.

Assuming your schema reflects one record / model per visit in your visits table, The best method would be to get rid of the visitsCount() relation and only use $entry->visits->count() to retrieve the number of visits to the entry.
The reason for this is that once this relation is loaded, it will simply count the models in the collection instead of re-querying for them (if using a separate relationship)
If your concern is overhead and unnecessary queries: My suggestion would be to eager-load these models in a base controller somewhere as children of the user object and cache it, so the only time you really need to re-query for any of it is when there have been changes.
BaseController:
public function __construct(){
if(!Cache::has('user-'.Auth::user()->id)){
$this->user = User::with('entries.visits')->find(Auth::user()->id);
Cache::put('user-'.Auth::user()->id, $this->user, 60);
} else {
$this->user = Cache::get('user-'.Auth::user()->id);
}
}
Then set up an observer on your Entry model to flush the user cache on save. Another possibility if you are using Memcached or Reddis would be to use cache tags so you don't have to flush the whole user's cache every time an Entry model is added or modified.
Of course, this also assumes that each Entry is related to a user, however, if it isn't and you need to use Entry alone as the parent, the same logic could apply, by moving the Cache class calls in your EntryController

Related

Check if an Eloquent element is related to another one (with MorphToMany relationship)

I have those tables :
- blocks
- bloackables
--reports
--modalities
--reportGroups
Block.php :
public function reports()
{
return $this->morphedByMany(Report::class, 'blockable');
}
public function modalities()
{
return $this->morphedByMany(Modality::class, 'blockable');
}
public function reportsGroups()
{
return $this->morphedByMany(ReportsGroup::class, 'blockable');
}
Report.php :
public function blocks()
{
return $this->morphToMany(Block::class, 'blockable');
}
The same type of relationship exists for ReportGroups and Modalities...
Basically I'd like to load a Block instance and then check if the instances of Report, Modality and ReportGroup are related to this block.
The idea is to create an edition form with a checkbox for every Report, Modality or ReportGroup instances).
What are the strategies ? I read the Eloquent documentation but I am still confused...
Best regards,
Take care...
Nicolas
i'm not sure i understand you completely:
you have
$reportInstances,$modalityInstances,$reportGroupInstances
and you want to determine who has relation to the current block;
ok, you should load the model 'block' with its relations, and check the relation existance:
$blcokWithRelations=Block::with(['reports','modalities','reportsGroups'])->find($block->id):
foreach($reportInstances as $reportInstance)
{
if($blcokWithRelations->reports->where('id','=',$reportInstance->id)->get->first()!=null)
// this report has relation to the current block
else
// this report doesn't have relation to the current block
}
the same goes for the rest of relations

Best Practice - Laravel Controller Eloquent merge

I have a scope on my Supplier model that returns results where active = true.
This works great when creating new entries, as I only want the user to see active suppliers.
Current entries may have an inactive supplier; When I edit it, I want to see all active Suppliers, plus the current supplier (if it is inactive)
I have this code in my controller:
$suppliers = Supplier::active()->get();
if (!$suppliers->contains('id', $record->supplier->id))
{
$suppliers->add(Supplier::find($record->supplier->id));
}
Two questions: Is this the correct way to do this? Should this code be in my controller or should I have it somewhere else? (perhaps a scope but I wouldn't know how to code that).
Edit:
Thanks for the help guys. I have applied advice from each of the answers and refactored my code into a new scope:
public function scopeActiveIncluding($query, Model $model = null)
{
$query->where('active', 1);
if ($model && !$model->supplier->active)
{
$query->orWhere('id', $model->supplier->id);
}
}
What you've written will work, but the Collection::contains function can potentially be pretty slow if the collection is large.
Since you have the id, I would probably make the following change:
$suppliers = Supplier::active()->get();
$supplier = Supplier::find($record->supplier->id);
if (!$supplier->active) {
$suppliers->add($supplier);
}
Of course, the downside to this is that you may be making an unnecessary query on the database.
So you have to consider:
is the record's supplier more likely to be active or inactive?
is the size of the collection of active suppliers large enough to justify another (potentially wasted) call to the database?
Make the choice that makes the most sense, based on what you know of your application's data.
As for the second question, if you will only need this specific set of suppliers in this one part of your application, then the controller is a good place for this code.
If, however, you will need this particular set of suppliers in other parts of your application, you should probably move this code elsewhere. In that case, it might make sense to create a function on the the related model (whatever type $record is...) that returns that model's suppliers set. Something like:
public function getSuppliers()
{
$suppliers = Supplier::active()->get();
$supplier = $this->supplier;
if (!$supplier->active) {
$suppliers->add($supplier);
}
return $suppliers;
}
I saw #Vince's answer about 1st question, and I'm agree with him.
About 2nd question:
Write scope in Supplier model like this:
public function scopeActive($query){
$query->where('active', 1); // for boolean type
}
For good practice, you need to write the logic parts in services like "App\Services\SupplierService.php". And there write the function you want:
public function activeSuppliersWithCurrent($record) {
$suppliers = Supplier::active()->get();
$supplier = Supplier::find($record->supplier->id);
if (!$supplier->active) {
$suppliers->add($supplier);
}
}
In your SupplierController's constructor inject the instance of that service and use the function, for example:
use App\Servives\SupplierService;
protected $supplierService = null;
public function __construct(SupplierService $supplierService) {
$this->supplierService = $supplierService;
}
public function getActiveSuppliersWithCurrent(...) {
$result = $this->supplierService->activeSuppliersWithCurrent($record);
}
As you can see, later you will not need to change anything in controller. If you'll need to change for example the query of suppliers selection, you will just have to change something only in service. This way will make your code blocks separated and shorter.
Also the sense for this pattern: you don't need to access the models from controller. All logic related with models will implemented in services.
For other projects you can grab only services or only controllers, and implement another part differently. But in that case if you had all codes in controller, that will prevent you to grab the portions of necessary codes, cuz may you don't remember what doing each blocks...
You could add a where clause to the query to also find that id.
$suppliers = Supplier::active()->orWhere('id', $record->supplier->id)->get();
You could potentially slide this into the active scope by passing the 'id' as an argument.
public function scopeActive($query, $id = null)
{
$query->where('active', true);
if ($id) {
$query->orWhere('id', $id);
}
}
Supplier::active($record->supplier->id)->get();
Or make another scope that does this.

Laravel returns a Collection with duplicates of the first model

I'm developing a Laravel 5.7 (API) application with a PostgreSQL database behind it. The relevant Models are: User (customers and employees), Car, and Request.
An employee User creates a Request for a Car, that belongs to a customer User.
The relationships are:
Car (as customer) : User = n:m
Car : Request = 1:n
User : Request (as employee) = 1:n
(The data design is suboptimal, to put it mildly, but anyway, it's the given reality for now.)
Now to the actual issue. I want to display all Requests of a customer User:
Request::query()
->join('user_car', 'user_car.car_id', '=', 'request.car_id')
->join('user', 'user.id', '=', 'user_car.user_id')
->where('user.id', '=', $customer->id)
->select()
->get();
The customer with the given $customer->id has n Requests. And the length of the result Collection of the call above is correct. But all these n entries are duplicates of the first one. Means: I'm getting a list with n instances of Request#1.
Why does the first call return a list of references to the same Model object? Is it a (known) bug?
ADDITIONAL INFORMATION
Relationships:
class User extends \Illuminate\Foundation\Auth\User
{
// ...
public function cars()
{
return $this->belongsToMany('App\Car', 'user_car')->withTimestamps();
}
public function requests()
{
return $this->hasMany(Request::class, 'user_id');
}
}
class Car extends Model
{
// ...
public function users()
{
return $this->belongsToMany('App\User', 'user_car')->withTimestamps();
}
public function requests()
{
return $this->hasMany(Request::class);
}
}
class Request extends Model
{
// ...
public function car()
{
return $this->belongsTo(Car::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
The query is correct.
I logged the database requests, got the generated statement
SELECT *
FROM "request"
INNER JOIN "user_car" ON "user_car"."car_id" = "request"."car_id"
INNER JOIN "user" ON "user"."id" = "user_car"."user_id"
WHERE "user"."id" = 1;
..., and executed it manually. The result table contains as expected n different entries.
NOT just references
The result Collection's entries instances references to the different objects:
$test1 = $resultCollection->first();
$test2 = $resultCollection->last();
$test3 = spl_object_hash($test1);
$test4 = spl_object_hash($test2);
Xdebug output:
$test3 = "0000000077505ccd000000007964e0a8" <-- ccd0
$test4 = "0000000077505c33000000007964e0a8" <-- c330
Workaround
I found a workaround. This call
Request::whereIn('car_id', $customer->cars()->pluck('id')->toArray())->get();
... retrieves the correct/expected set of model.
First, note that your object hashes are not actually identical, and you're likely dealing with two separate instances.
What you're likely experiencing is an issue with ambiguous column names. When you JOIN together multiple tables, any matching/duplicate column names will contain the value of the last matching column. Your SQL GUI/client usually separates these. Unfortunately Laravel doesn't have a prefixing mechanism, and just uses an associative array.
Assuming all of your tables have a primary key column of id, every Request object in your result set will likely have the same ID - the User's ID you pass in the WHERE condition.
You can fix this in your existing query by explicitly selecting the columns you need to prevent ambiguity. Use ->select(['request.*']) to limit the returned info to the Request object data.

Laravel Has One Relation changing the identifier value

I'm not sure this is a real relation. I will try to explain the best way I can.
So first of all, I have three models :
Appartement,
AppartementPrice
The AppartementPrice depends on :
- appartement_id
I would like the AppartementPrice to be retrieve like that :
If there is a specific price for the appartement, then retrieve it, If not retrieve the price for all appartement which is stored in the database with an appartement_id = 0.
So basically what I would like is to do something like that :
public function price()
{
if(isset($this->hasOne('AppartementPrice')->price) // Check that relation exists
return $this->hasOne('AppartementPrice');
else
return $this->hasOne('AppartementPrice')->where('appartement_id', '0');
}
But this is not working.
It does not retrive me the default price.
I guess anyway this is not a best practice ?
I first tried to get the informations like that :
//Check if appartment has a specific price or retrieve default
if($priceAppartement = AppartementPrice::getPriceByCompanyAppartement($this->id))
return $priceAppartement;
else
return AppartementPrice::getDefaultPrice();
But I had this error :
Relationship method must return an object of type Illuminate\Database\Eloquent\Relations\Relation
when doing :
echo $app->price->price;
How can I check that a relation exists ? And is there a way to do as I describe ?
Thank you
You can't replace relation like this, as what you intend is not logical - you want to retrieve relation that doesn't exist.
Instead you can do this:
public function getPriceAttribute()
{
return ($this->priceRelation) ?: $this->priceDefault();
}
public function priceDefault()
{
// edit: let's cache this one so you don't call the query everytime
// you want the price
return AppartmentPrice::remember(5)->find(0);
}
public function priceRelation()
{
return $this->hasOne('AppartementPrice');
}
Then you achieve what you wanted:
$app->price; // returns AppartmentPrice object related or default one
HOWEVER mind that you won't be able to work on the relation like normally:
$price = new AppartmentPrice([...]);
$app->price()->save($price); // will not work, instead use:
$app->priceRelation()->save($price);
First of all something really important in Laravel 4.
When you do not use parentheses when querying relationship it means you want to retreive a Collention of your Model.
You have to use parentheses if you want to continue your query.
Ex:
// for getting prices collection (if not hasOne). (look like AppartementPrice)
$appartment->price;
// for getting the query which will ask the DB to get all
//price attached to this appartment, and then you can continue querying
$priceQuery = $appartment->price();
// Or you can chain your query
$appartment->price()->where('price', '>', 0)->get() // or first() or count();
Secondly, your question.
//Appartement Model
// This function is needed to keep querying the DB
public function price()
{
return $this->hasOne('AppartementPrice')
}
// This one is for getting the appartment price, like you want to
public function getAppartmentPrice()
{
$price_object = $this->price;
if (!$price_object) // Appartment does not have any price {
return AppartementPrice->where('appartement_id', '=', 0)->get();
}
return $price_object;
}

Inspect Model relations

I am trying to inspect my Eloquent models to find out their relations to other Models. The problem is that relations are simply defined as a single method and no central index of relations exists:
public function posts()
{
return $this->hasMany('Post');
}
In order to inspect all relations I need to extract the list of methods, take out the ones inherited from Eloquent, execute each single one and check the return type:
$all = get_class_methods($model);
$inherited = get_class_methods('Eloquent');
$unique = array_diff($all, $inherited);
foreach($unique AS $method)
{
$relation = $model->$method();
if(is_a($relation, 'Illuminate\Database\Eloquent\Relations\Relation'))
{
//... this is a relation, do something with it
}
}
Needless to say, this is very dangerous. Is there a way to do this kind of inspection in a different, more secure way?
You could add PHPDoc comments to your relationship methods and then use the PHP reflection API to extract those from the source.

Resources