Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Bulk Upsert Laravel Package

lapaliv/laravel-bulk-upsert

Laravel Bulk Upsert saves collections of Eloquent models with bulk queries while still firing creating, updating, saving, deleting, and restoring events. It auto-aligns uneven fields and returns the inserted rows. Supports MySQL, PostgreSQL, and SQLite.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit. The package is a thin augmentation of Eloquent rather than a parallel ORM: a Bulkable trait on the model, a Bulk facade-ish object constructed via Model::bulk() or new Bulk(User::class), and a fluent builder that terminates in create/update/upsert/delete/forceDelete plus ...AndReturn and ...OrAccumulate variants. That is architecturally idiomatic for Laravel — no service provider ceremony, no repository abstraction to adopt, no schema changes. For a team already on Eloquent models with observers, the mental model is "the same foreach ($models as $model) { $model->save(); } loop, batched into a few queries per chunk."

The genuine differentiator is not bulk insert throughput — Laravel's Model::upsert already does that — it is event fidelity. Native Model::query()->insert() bypasses creating/created/saving/saved and observers, which is the single most common reason teams revert to per-row saves and eat the N+1 cost. This package closes that gap and adds collection-scoped *Many events plus a BulkRows payload that carries original, model, and unique — useful for the "parent saved, now fan out children" pattern shown in the README.

Integration feasibility. Low friction. composer require lapaliv/laravel-bulk-upsert, one trait per model, replace a loop. No infra, no queue, no config file evidenced. The ...OrAccumulate + chunk($n) API is a good fit for streaming imports and queue workers where you want to bound batch size without rewriting the caller.

Technical risk. Material, and concentrated in three areas:

  • Adoption and bus factor. 15 stars, 0 dependents, single maintainer, no visible ecosystem. This is the profile of a package you should assume you may have to own. Any Laravel major upgrade becomes a compatibility question you resolve yourself.
  • Semantic divergence from Eloquent. Events firing does not guarantee equivalence. Unresolved: whether casts, mutators, accessors, $fillable/$guarded, HasUuids/HasUniqueStringIds, soft-delete scopes, and updated_at semantics behave identically to per-model saves. Divergence here produces silent data corruption, not errors.
  • Return-path correctness. ...AndReturn necessarily re-selects after the write. Under concurrency that read-back is not atomic with the write; on replicas it is worse. The correctness of uniqueBy mapping to ON DUPLICATE KEY (MySQL) vs ON CONFLICT (Postgres) vs SQLite's constraint handling is driver-dependent and needs verification per driver.
  • Halting semantics. Several on* callbacks return false to suppress a row. In a chunked batch this implies partial application — atomicity guarantees are undocumented. If a chunk half-applies under an exception, the caller's retry is non-idempotent unless uniqueBy is correct.

Key questions for a spike (not marketing questions — verification questions):

  1. Does a saving observer that mutates the model actually affect the persisted row, or is the write path pre-computed from raw input?
  2. What is the exact SQL emitted per driver, and does uniqueBy accept composite keys consistently?
  3. Are bulk operations wrapped in a transaction, and can I opt in/out?
  4. Does returning false from onCreatingMany abort the chunk or skip models — and is the remainder still written?
  5. Is there a CI matrix (Laravel versions × DB drivers)? The release cadence suggests activity, but the extracted metadata doesn't confirm test coverage.
  6. What does "last release 2026-07-22" reflect — a forward-dated tag or a metadata artifact? Resolve before trusting the cadence signal.

Integration Approach

Stack fit. Requires PHP 8.0+, Laravel 8.0+, and MySQL 5.7+ / PostgreSQL 9.6+ / SQLite 3.32+. Confirm the installed Laravel major is explicitly supported by the latest tag rather than inferred from "8.0+". Verify the DB driver matches the primary target — teams running MySQL in prod and SQLite in test can get divergent conflict-resolution behavior, so the test matrix must include the prod driver.

Migration path. Do not do a repository-wide swap. Sequence:

  1. Spike (1–2 days): one isolated path — an import job or audit/log write — where the existing code is a foreach + save(). Instrument query count before/after and diff observer side effects (emails, audit rows, cache busts) against the legacy path for an identical dataset.
  2. Pilot (1 sprint): one production model behind a config flag or a dedicated job. Keep the legacy loop reachable for rollback. Measure query count, p99 job duration, and lock-wait time.
  3. Adopt selectively: high-volume write paths where per-row events are actually needed. For paths with no observers, use native Model::upsert — the package adds read-back and event overhead you don't need.
  4. Fork-or-drop decision: if pilots pass and the package proves load-bearing, vendor a fork under your org with CI pinned to your Laravel/DB matrix. Budget ~1–2 days per Laravel major for compatibility patches; treat that as the real cost of adoption.

Compatibility and sequencing. Add the dependency, then add a single integration test asserting observer parity for create/update/upsert/delete on your prod driver. Gate broader rollout on that test staying green through the next framework upgrade. Watch for interplay with SoftDeletes — delete() silently means soft or hard depending on the trait, and forceDelete bypasses the restore path.

Operational Impact

Maintenance. Expect to own it. The package is small enough to fork and maintain (a handful of classes around the Eloquent builder), which is the mitigant for the bus-factor risk. Pin the version explicitly; do not float on ^2. Track the fork in your dependency-review process alongside first-party code.

Support. No vendor, no SLA, community-only. Internal expertise will sit with whoever wrote the spike — document the event-ordering contract and the halting semantics in-repo, because they are non-obvious and the README lists order but not failure behavior.

Scaling. Two levers: chunk size and connection concurrency. Larger chunks reduce round-trips but increase lock-hold duration and deadlock surface on MySQL ON DUPLICATE KEY under parallel workers — serializing upserts per uniqueness key, or using partitioned batches, is usually necessary. The ...OrAccumulate path holds full model instances in memory, so chunk size is a memory bound as well as a throughput knob. ...AndReturn adds a read-back per chunk; route it to the write connection, never a replica.

Failure modes to instrument:

  • Partial chunk application on exception — no documented atomicity; log chunk boundaries and require idempotent retries keyed on uniqueBy.
  • Silently suppressed rows via false-returning callbacks — emit a counter; a misbehaving observer can drop writes with no error.
  • Duplicate-key or conflict-target errors from a mismatched uniqueBy vs actual DB constraint.
  • Observers that assume a single model with a populated id — in bulk, created fires after the batch, and ID availability depends on the return path.
  • Replica lag or concurrency producing stale/incorrect rows from ...AndReturn.
  • Deadlock/timeout spikes on batch size changes — alert on lock-wait and retry rate.

Ramp-up. API surface is small and the README is well-structured with the event-ordering table and a realistic parent/child example — a mid-level Laravel developer is productive in under a day. The real ramp-up cost is not the API; it is building the team's intuition for where bulk-write semantics diverge from per-model saves. That knowledge should live in a short internal ADR alongside the fork, not in tribal memory.

Recommendation. Conditional adopt. The problem it solves is real and the API is well-matched to Laravel. The risk is not technical — it is that a 15-star, single-maintainer package becomes load-bearing in your write path. Adopt only if (a) the parity spike passes on your production driver, (b) you are willing to fork and pin, and (c) you scope it to paths that genuinely need Eloquent events in bulk. For everything else, native Model::upsert remains the lower-risk default.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package