简体   繁体   中英

PHP - Save to database outside foreach loop

I have below foreach loop, which iterates over a bunch of different custom rules my users can apply.

public function parse(Document $document)
{
        $content = $document->text;
        $rules = $document->stream->fieldrules;

        foreach ($rules as $rule) {
            $class = $this->getClass($rule);
            $content = $class->apply($content);

            //Save to database.
            $fieldresult = new FieldRuleResult();
            $fieldresult->create([
                'field_rule_id' => $rule->field_id,
                'document_id' => $document->id,
                'content' => $content
            ]);
        }
}

As you can see, I am calling the database on each iteration. Some users may have up to 50 rules defined, which will then result in 50 insert queries. So I believe I may have encountered a n+1 problem.

I was wondering - what is the best practice here? I've tried searching the Laravel documentation, and found the createMany method. So I tried to refactor the iteration to below:

$result = [];
foreach($rules as $rule)
{
  $class = $this->getClass($rule);
  $content = $class->apply($content);
  $fieldresult = new FieldRuleResult();

  $result[] = [
     'field_rule_id' => $rule->field_id, 
     'document_id' => $document->id, 
     'content' => $content];
}

return $rules->createMany($result);

Which gives me below error:

Method Illuminate\Database\Eloquent\Collection::createMany does not exist.

Now I imagine that is because $rules returns a collection. I tried to alter it to:

return $document->stream->fieldrules()->createMany($result);

Which gives me below error:

Call to undefined method Illuminate\Database\Eloquent\Relations\HasManyThrough::createMany()

There is a method available in the Illuminate/Database/Query/Builder named insert . This method allows you to insert many models in one query. Noted is that this will not, as all multiple in one query methods do, trigger eloquent events.

Like you have already done, store the data in an array and execute the query after your loop is done. However this time you could use insert .

$content = 'your content here...';

$data = [];

foreach($rules as $rule) {

    $class = $this->getClass($rule);

    $content = $class->apply($content);

    $data[] = [
        // ...
        'content' => $content
    ];
}

FieldRuleResult::insert($data);

Not 100% sure this works directly on the model. However in the documentation you can find an example with the DB facade.

If Eloquent events need to be triggered for whichever reason, and you would like to optimize for the n+1 problem, I would suggest using a transaction. This will still yield n queries, but the database can optimize this via bulk insert (because of the transaction). It has the extra benefit of data consistency (either all inserts succeed, or none of them do).

$result = getResult();

\DB::transaction(function() use ($result) {
    foreach($result as $item) {
        FieldRuleResult::create($item);
    }
});

If you do not care about Eloquent events being triggered, and you just want to "bulk insert", you can use the insert method, as mentioned by Fabian. I would still advise to use a database transaction though, for data consistency.

$result = getResult();

\DB::transaction(function() use ($result) {
    FieldRuleResult::insert($result);
});

In this latter case, if the amount of entries you're about to insert can be "large", you might also want to chunk these inserts. (eg. 100 at a time, rather than all at once)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM