It is hard to tell what you mean without showing code, but this might help? https://laravel.com/docs/master/eloquent-relationships#eager-loading
hasMany. Possible to avoid select in foreach?
Problem: whenever I foreach over a hasMany (even if I don't call the method) it seems to cause Laravel to SELECT * FROM relation.
Statement: It's so infuriating that it is doing this.
Question: Is there any other way to loop over in-memory models for a relationship only?
@lewiscowles1986 Why/How would the relations be in memory already?
@tykus I do not need Laravel to go off and fetch things saved (Ever) without explicitly asking it to do so.
The same behaviour for an unsaved model should be preserved when not calling the method-form.
Thanks for replies.
So taking this example from the official docs. https://laravel.com/docs/master/eloquent-relationships#one-to-many
I can call $model->comments() or $model->comments. Using $model->comments() I expect to be talking to the database. If I write to $model->comments I could add a number of comments to the intermediate collection.
I wouldn't want to ->save the existing ones. (Probably better to think Twitter than personal website comments. It's not what I'm dealing with, but the multiple new records per-save is an important facet of what I'm trying).
foreach($model->comments as $comment) {
$comment->save();
}
I think basically this is a hint that although my Model hasMany of Something. I should be coordinating that outside of the class. This is a little infuriating as I was sure there was a setting to make an Invoice save LineItems, their associated Tax entries etc. Same with Comments, Linked media and file assets etc.
@LewisCowles1986 As far as I understand, you need to use createMany.
https://laravel.com/docs/8.x/eloquent-relationships#the-create-method
$model->comments()->createMany([
['message' => 'A new comment.'],
['message' => 'Another new comment.'],
]);
To be clear, what seems strange is that I am able to do the following without altering or accessing any database.
// define $comment as an instance of `Comment` model (really does not matter how you do this)
$model->comments[] = $comment;
//
$model->comments->count();
$model->comments->count() can therefore diverge from $model->comments()->count();
Perhaps I need to use collection methods, rather than PHP foreach... I'll experiment with this.
using ->each seems to do the trick without trying to make a database call if the record is not saved. If the record is persistent, it seems Laravel cannot help itself but call out to the database. Which is infuriating.
Its hard to understand your question and determine what it is you are having a problem with.
@Snapey I've updated the question to make it explicit for you. If you are still confused, please state your confusion. What are you confused about?
@LewisCowles1986 If you reference attributes of a relation, and that relation is not loaded, then Eloquent will try and resolve the relation for you.
If this happens inside a loop, then this is called an n+1 issue
You should eager load all the relations you are likely to use inside your loop so that only one query is used to load all related records.
then Eloquent will try and resolve the relation for you.
This is a problem. This is THE problem I want to overcome. How to disable that unwanted behaviour.
>>> get_class($post->revisions)
=> "Illuminate\Database\Eloquent\Collection"
>>> get_class($post->revisions())
=> "Illuminate\Database\Eloquent\Relations\HasMany"
>>>
This holds true. Regardless of a Model being saved.
Same class, same inputs. Different behaviour from Illuminate\Database\Eloquent\Collection
I'm looking for a way to overcome a design flaw that makes a Model talk to an external system, depending on if it thinks it exists in that system...
The createMany linked above removes the n+1, but would fetch all existing PostRevision instances when calling $post->revisions as well as those that are staged. This is broken behaviour and means more work needs to be done in a database using Laravel. I've seen other posts about using $hidden, but that seems to be about omitting relation members from serialization.
If comments / revisions is confusing. The model is like Post in this instance a shell. It has a regular unsigned incrementing id, timestamps, and one additional column for the relationship.
I'd like the HasMany to continue working this way. It makes sense. I'd perhaps like Illuminate\Database\Eloquent\Collection to be a regular Illuminate\Support\Collection
For now, I think adding an index to the belongsTo side of this relationship, and calling saveMany seems to prevent the duplicates and avoid the performance cost. But even if it takes 0.0 seconds. It is bizarre and fragile behaviour that is more difficult to reason about than it needs to be.
It looks like this is the way Laravel works.
If a record is not saved, the hasMany seems smart enough to not query the database. What I am looking to do is have it not do things I have not asked for. I would like to preserve the behaviour of an unsaved record.
In Artisan Tinker:
Setup:
// Log all SQL queries (don't commit this, you'll only want to do it locally
DB::listen(function($query) {
Log::info(
$query->sql,
$query->bindings,
$query->time
);
});
Firstly, what happens when a Post is not saved
it's a blank Model with primary key, timestamps
// We create a non-persistent Model
$post = new App\Models\Post();
// This makes no query. It works with the in-memory collection. It's result is 0.
$post->revisions->count();
// This makes a database query. It ignores the local in-memory collection. It's result is 0.
$post->revisions()->count();
// create 10 revisions
foreach(range(1, 10) as $number) {
$revision = new \App\Models\PostRevision();
$revision->post_id = $post->id;
$post->revisions[] = $revision;
}
// This makes no query. It works with the in-memory collection. It's result is 10.
$post->revisions->count();
// This makes a database query. It ignores the local in-memory collection. It's result is 0.
$post->revisions()->count();
// This will get all staged PostRevisions from the in-memory collection. It's result is a list of the 10 revisions.
$post->revisions->all();
// This makes a database query. It ignores the local in-memory collection. It's result is []
$post->revisions()->get();
Now lets save the post
// We create a non-persistent Model
$post = new App\Models\Post();
$post->save();
// This makes a query. It's result is zero because I just created the post, but it seems like an unnecessary call, and I'd like to not make it.
$post->revisions->count();
// This makes a database query. It's result is 0.
$post->revisions()->count();
// create 10 revisions
foreach(range(1, 10) as $number) {
$revision = new \App\Models\PostRevision();
$revision->post_id = $post->id;
$post->revisions[] = $revision;
}
// This makes no query. It works with the in-memory collection. It's result is 10.
$post->revisions->count();
// This makes a database query. It ignores the local in-memory collection. It's result is 0.
$post->revisions()->count();
// This will get all staged PostRevisions from the in-memory collection. It's result is a list of the 10 revisions.
$post->revisions->all();
// This makes a database query. It ignores the local in-memory collection. It's result is []
$post->revisions()->get();
I am looking for a way to preserve the behaviour before the object is saved with the property based interactions.
Not really sure what you expected. $post->revisions() returns a query builder instance, count() in this case is a terminating statement for the query builder. It tells eloquent to run the query and return the result. It is different to $post->revisions->count() because this is a collection method and acts directly on the object.
@Snapey if you call it on a saved object without first staging more entries, then $post->revisions->count() makes a database call; whereas on a non-saved object it will not do that, it just makes an empty collection.
It is not consistently working on the internal state of the model and that is the problem. Making it consistently work using the internal state of the model is the desired outcome.
if the relation is null then eloquent attempts to resolve the relation by querying the database (lazy loading)
You can prevent this if you wish
https://laravel.com/docs/8.x/eloquent-relationships#preventing-lazy-loading
In lieu of a setting, I'm going to do something to override the magic
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class Post extends Model
{
use HasFactory;
/** @var Collection */
public $revisionsLocal;
public function __construct() {
$this->revisionsLocal = new Collection([]);
}
public function revisions() {
return $this->hasMany(PostRevision::class);
}
}
Now the revisions property cannot be magically overwritten, so it avoids the database and I can saveMany on the hasMany collection revisions(). :smile: I'm sure if I PR this Taylor would not be happy, but I think it's a better design. It also avoid's @snapey concerns over n+1 :wink:
Update: privoted to not override this behaviour, but introduce a new property for the logical behaviour.
Please or to participate in this conversation.