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.
insert() helper, a batch of "we'll fire the events manually later" code. This package is a candidate to buy (MIT, ~zero cost) instead of building — the decision is whether a thin third-party dependency beats an internally owned helper we'd have to maintain forever.foreach ($rows as $row) { $row->save(); } loop, we're paying N round-trips per batch. Adopting this collapses that to "a few queries per chunk," which can remove a whole performance epic from the roadmap and free the quarter for feature work.upsert() / insert() do not fire Eloquent events, so any bulk path silently bypasses observers, audit logs, updated_by stamping, derived fields, and cache invalidation. Teams often don't notice until an audit or a "why is this field null" incident. Choosing an events-preserving bulk writer is a correctness/governance decision, not just a perf one.createAndReturn / upsertAndReturn give back hydrated models with IDs, which determines whether downstream work (child records, search indexing, notifications) can happen in the same job — or whether we need a second read query / extra round-trip architecture.chunk() + createOrAccumulate() accumulator pattern lets a queue worker hold constant memory while draining a large feed. That influences whether we size jobs for "load whole file" or "stream rows," which affects worker memory and infra cost.Adopt-candidate when most of these are true:
save() query counts are the measured bottleneck.Look elsewhere when:
save() loops or insert() are simpler, and the dependency isn't earning its keep.upsert() / insertOrIgnore and skip the abstraction entirely.save() semantics (mutators, casts, timestamps, transaction boundaries, model-state consistency). Divergences here are silent correctness bugs, and low adoption means fewer people have found them for us.Non-negotiable validation before committing: a timeboxed spike (1–2 days) proving our existing observers fire exactly once per model, created_at/updated_at and casts behave, rollback semantics are understood, and query counts and p95 latency drop materially on our real schema.
To executives: We have a class of features — imports, syncs, bulk edits — that are slow and expensive because we write one database row at a time. Adopting a small MIT-licensed library lets us write those in batches while still running all our existing business logic (audit trails, derived fields, notifications) that the fast path normally skips. Expected outcome: large cuts in ingestion time and database load for the same feature set, and it removes a "make imports faster" project from the roadmap. Cost is effectively zero in license fees; the real investment is a short evaluation spike and a thin internal wrapper so we're not locked in. The one honest caveat: it's a young package with minimal community adoption, so we'd be an early production user — we mitigate that by wrapping it, testing it against our own observer contracts, and keeping a fallback.
To engineering: This gives us Eloquent's event lifecycle (creating/created, saving/saved, deleting/deleted, restoring/restored, plus collection-level *Many events) on top of chunked bulk writes — semantically "a foreach of save(), but a few queries per chunk." It handles ragged field sets without manual alignment, returns hydrated collections, supports soft/force delete and restore in bulk, and exposes BulkRows (original payload + model + unique attributes) so observers can chain related writes — e.g., upsert users, then upsert their comments from the same payload in savedMany. Requirements: PHP 8.0+, Laravel 8+, MySQL 5.7+/Postgres 9.6+/SQLite 3.32+, and the Bulkable trait on participating models. The ask: a spike plus a thin BulkWriter interface so the rest of the codebase depends on our contract, not on Lapaliv\BulkUpsert directly — that keeps this a reversible decision and lets us swap in native upsert() or a fork if the package stalls.
How can I help you explore Laravel packages today?