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 Batch Laravel Package

mavinoo/laravel-batch

Laravel Batch provides bulk insert and update helpers for Laravel models. Use Batch::update with a key column or updateMultipleCondition for per-row conditions, making large data changes faster and cleaner without repetitive queries. Includes a facade and batch() helper.

View on GitHub
Deep Wiki
Context7

Product Decisions This Supports

  • Security-Driven Upgrade (v3.0.0 is still the floor): Treat v3.0.0 as a required upgrade for any product still using 2.x. All 2.x versions build SQL by escaping values manually, which allows SQL injection always on PostgreSQL and through index values/JSON keys on every database. v3.0.0 sends values as bound parameters and lets Laravel quote column/table names. This is a risk-reduction decision, not just a feature upgrade. v3.3.0 does not change this: everything since 3.1 has been additive on top of the fix.
  • Zero-Cost Upgrade Path (3.1 → 3.3):
    • v3.1.0 was additive on 3.0; v3.2 and v3.3.0 are likewise additive — composer update mavinoo/laravel-batch and no code changes from 3.2. That makes this a low-risk, high-return scheduling decision rather than a migration project.
    • Performance is now a product lever, not just an implementation detail: v3.1.0's chunking change dropped 50,000-row updates from 30.5s → 3.1s (MySQL 8.4), 25.3s → 2.4s (MariaDB 10.11), and over 15 minutes → 2.5s on PostgreSQL 16 (SQLite roughly unchanged at 3.7s → 3.4s). Each UPDATE ... CASE statement now holds at most 100 rows.
    • PostgreSQL moves from "avoid for bulk" to "fully viable": v3.1.0 removed the previous PostgreSQL slowness penalty; v3.3.0 builds on that with COPY-based fast imports.
  • New Capabilities That Change Feature Feasibility:
    • Bulk CRUD (from v3.1.0):
      • upsert(): insert-or-update in one query with timestamps (created_at only on new rows), enums, and automatic splitting in one transaction. Removes the previous "read-then-decide-then-write" pattern from ETL code.
      • deleteByKeys(): bulk deletes matched on one or more key columns; models using SoftDeletes are soft deleted unless $force is true. Bulk cleanup, retention/GDPR-style purge flows, and admin "delete selected" become configuration.
      • updateByKeys(): update rows matched on any number of key columns — covers composite/natural keys (e.g. tenant_id + external_id).
      • insertRows(): insert rows as column => value pairs — the shape imports actually arrive in.
    • Counter & conditional upserts (new in v3.3.0): This unlocks product workflows that previously required bespoke SELECT ... FOR UPDATE + application math:
      • Batch::upsert($model, $rows, ['sku'], ['qty' => ['+']]) — atomic counter adjustments. ['-'], ['max'], ['min'] also supported, on every database. This is the primitive behind inventory adjustments, wallet balance credits, leaderboard scores, quota counters, and "stock-on-hand" reconciliations that are natural upsert candidates.
      • Batch::upsert($model, $rows, ['sku'], null, ['onlyIf' => ['updated_at' => '>']]) — conditional upserts: only overwrite an existing row when the incoming value wins. New rows always insert. This enables last-write-wins sync semantics without an external reconciler.
    • Returning rows & duplicate reporting (new in v3.3.0):
      • upsertReturning() returns the affected rows (id, natural keys, other columns), removing the follow-up SELECT in sync pipelines that need to link inserted/updated rows to downstream work.
      • insertOrIgnoreRows() returns inserted count and the actual skipped duplicate rows. Tested with 8 concurrent writers inserting overlapping keys on MySQL 8.4, MariaDB, and PostgreSQL — every row is attributed to the connection that inserted it. This makes concurrent dedup at scale a reportable, auditable operation rather than "trust the insert count."
    • Fast imports (new in v3.3.0):
      • Batch::import($model, $path, ['fast' => true]) uses COPY on PostgreSQL and LOAD DATA LOCAL INFILE on MySQL/MariaDB. 1,000,000-row CSV: PostgreSQL 18.7s → 6.6s, MySQL 8.4 18.7s → 8.4s, MariaDB 12.6s → 6.4s.
      • Bad values that MySQL would silently store with a warning now throw and nothing is written — this is a correctness and data-quality feature, not just a speed feature.
    • Row validation & error reporting in imports (new in v3.3.0):
      • Batch::import($model, 'users.csv', ['rules' => ['email' => 'required|email']]) returns totalRows, skipped, and errors keyed by row number. This is the primitive behind any "upload a CSV, show row-level validation errors" UI — previously a custom job.
    • Export (new in v3.3.0):
      • User::where('active', true)->exportTo(storage_path('users.csv'), ['maxRows' => 100000]) writes CSV, TSV, or JSON Lines (optionally gzipped), chunked at constant memory, and splits into numbered parts via maxRows. Reporting exports, compliance dumps, and "download my data" features become a method call rather than a bespoke streaming job.
    • Timestamps control (new in v3.3.0 docs/tests):
      • User::withoutTimestamps(fn () => Batch::update(new User, $rows)) is documented and tested for every method. Removes a class of "why did updated_at move on a sync?" surprises.
  • pretend() Enables Product-Visible Previews: See the SQL and bindings a call would run without executing it or connecting to the database. This unlocks:
    • Admin UI previews ("this will update 4,213 rows") before a destructive bulk action.
    • Dry-run migrations and backfills in CI or staging without touching data.
    • Query review/audit for regulated or financial batch changes.
    • Cheap unit tests that assert generated SQL with no database dependency.
  • Static HasBatch Methods: Every operation now has a static entry point (Model::batchUpdateMultipleCondition(), batchUpdateByKeys(), batchInsertRows(), batchUpsert(), batchDeleteByKeys(), batchUpdateWithTwoIndex(), and the v3.3.0 additions). This standardizes call sites, plays well with static analysis and IDE discovery, and is the direction the package is committing to.
  • Roadmap Prioritization:
    • Phase 1 (security, non-negotiable): Upgrade any 2.x integration to v3.0.0+ before shipping new bulk-operation features; budget a short migration for breaking changes.
    • Phase 1b (quick win): Roll v3.3.0 in the same dependency bump or immediately after. No code changes from 3.2; expect the v3.1.0 latency wins on existing bulk paths, plus the new import/export/counter capabilities.
    • Phase 2: Use the package for common bulk CRUD; adopt upsert() for sync/import jobs, deleteByKeys() for cleanup, and import() for CSV ingestion. Add DB::transaction() only around multi-step business workflows that must be atomic end-to-end.
    • Phase 3: Replace instance-style calls with the static HasBatch methods; the instance updateMultipleCondition() is deprecated and slated for removal in 4.0.
    • Phase 4: Add observers/events if audit hooks or partial-failure workflows become important; use pretend() where a preview or approval step is required. Expose import(errors) and insertOrIgnoreRows(skipped) results directly in admin UIs.
  • Build vs. Buy:
    • Buy: Avoids reinventing batch logic for common CRUD, sync, and data-movement operations, reducing dev time by roughly 30–50% (higher for data-heavy products now that import/export is included). The case is now materially stronger than v3.1: counters, conditional updates, returning rows, duplicate reporting, COPY/LOAD DATA fast imports, row validation, and streaming export are all first-class. Backed by PHPStan level 9 with Larastan in CI and a multi-database test matrix.
    • Custom Build: Justify only for distributed consistency, complex multi-statement atomic workflows, non-Laravel data stores, or highly specialized SQL generation. Ingestion and export are no longer good reasons to build custom — that case was weaker even at v3.1 and is weaker still now.
    • Do Not Stay on 2.x for SQLite or Transaction Reasons: v3.0.0 added SQLite support and transactional behavior for automatically split large batches; staying on 2.x leaves known SQL-injection exposure. There is no remaining capability argument for 2.x.
  • Use Cases:
    • High-Volume Imports: Process 10K+ records efficiently via insertRows()/insert(); use import(['fast' => true]) for CSVs up to millions of rows.
    • Validated User Uploads: import(['rules' => ...]) with row-level errors returned — a ready-made primitive for self-service CSV onboarding.
    • Idempotent Syncs / ETL: Nightly third-party or API syncs via upsert() (with created_at set only on new rows) and upsertReturning() when downstream steps need the resulting rows.
    • Counter-Style Adjustments: Inventory, wallet balances, quotas, scores — batch atomic +/-/max/min upserts in one round trip.
    • Conflict-Aware Syncs: onlyIf conditional upserts to keep the newest updated_at without a reconciler.
    • Concurrent Ingestion with Dedup Reporting: insertOrIgnoreRows() to ingest overlapping feeds and report exactly which rows were skipped and why.
    • Bulk Cleanup & Retention: deleteByKeys() with automatic soft-delete behavior; $force for hard purges.
    • Composite-Key Updates: updateByKeys() for tenant/external-ID style matching.
    • Conditional Bulk Updates: Target subsets such as inactive users with last_login < 30 days.
    • Arithmetic Operations: Batch increments/decrements such as percentage discounts for premium users.
    • Data Export / Reporting: exportTo() CSV/TSV/JSONL, gzipped, chunked, or split into numbered parts via maxRows.
    • Audit Logs / Backfills: Backfill historical records without manual loops.
    • Admin Preview & Approvals: pretend() to show affected SQL/counts before execution.
    • SQLite-Backed Apps: Supported and covered by the test matrix.
    • PostgreSQL-Backed Apps: Now a first-class target after the v3.1.0 chunking fix and v3.3.0 COPY-based fast imports.

When to Consider This Package

  • Adopt When:

    • You need to reduce database queries for bulk operations.
    • Your use case fits simple batch inserts/updates/upserts/deletes without complex joins or subqueries.
    • You need insert-or-update semantics for sync/import jobs (upsert()).
    • You need counter-style upserts (['+'], ['-'], ['max'], ['min']) for inventory, balances, quotas, or scores.
    • You need conditional upserts so only "winning" updates overwrite existing rows (onlyIf).
    • You need returned rows from an upsert (upsertReturning()).
    • You need bulk deletes that respect SoftDeletes without writing custom logic.
    • You need bulk CSV/TSV/JSONL import with fast mode (COPY on PostgreSQL, LOAD DATA LOCAL INFILE on MySQL/MariaDB).
    • You need row-level validation and error reporting on imported CSVs (rules option).
    • You need streaming export with constant memory, gzip, and maxRows splitting.
    • You need to preview generated SQL before running it (pretend()).
    • You are on PHP 8.1+ and Laravel 10–13.
    • You use MySQL/MariaDB, PostgreSQL, or SQLite. PostgreSQL is a first-class target after v3.1.0, and its import path uses COPY in v3.3.0; still validate SQL Server separately if your product depends on it.
    • Your team lacks bandwidth to build and maintain a custom batch/import/export layer.
    • You want minimal boilerplate such as User::batchUpdate() / User::batchUpsert() / Batch::import() instead of manual foreach loops.
    • You are currently on 2.x and need to close the SQL-injection exposure.
    • You already run PHPStan/Larastan and want a dependency that holds itself to level 9.
  • Look Elsewhere If:

    • You require atomic transactions across mixed operations (e.g. update X, then delete Y only if X succeeds) or across mixed batch kinds (import + upsert + export in one all-or-nothing unit). Wrap such workflows explicitly or use a domain service.
    • Your data model needs complex conditions such as WHERE EXISTS (SELECT ...).
    • You need fine-grained control over generated SQL syntax beyond what DB::raw() in values/keys/conditions provides.
    • You process unbounded streaming data where batch size and row shape are unpredictable. Import handles large CSVs well, but for continuous streams still chunk upstream first.
    • MySQL/MariaDB server flags prevent LOAD DATA LOCAL INFILE (e.g. local_infile=0, hardened hosting). Standard-mode import() still works; ['fast' => true] will not.
    • You are on PHP <8.1 or Laravel <10. In that case, plan a stack upgrade; do not treat staying on vulnerable 2.x as an acceptable long-term option.
    • Your team requires strict static type safety for dynamic column handling: v3.1.0+ materially improves this (PHPStan level 9 in CI, typed return values, clearer InvalidArgumentExceptions for non-array rows, non-string column names, and invalid arithmetic operators), but arbitrary runtime-supplied column names still deserve review at the call site.
    • You depend on the instance updateMultipleCondition(): it is deprecated and slated for removal in 4.0. Migrate to Model::batchUpdateMultipleCondition() — this is not a reason to avoid the package, but it is a reason to schedule the change now rather than at 4.0.

How to Pitch It (Stakeholders)

For Executives: "Two things are true right now. First, v3.0.0 was a mandatory security and reliability upgrade: the 2.x line had a SQL-injection flaw because it built SQL by escaping values manually — exploitable on PostgreSQL and through index/JSON keys on every database. v3.0.0 fixed that with bound parameters and Laravel-native identifier quoting. If any product is still on 2.x, that remains the top priority.

*Second, every release since — including the newest v3.3.0 — is a free upgrade on top of it. It is additive; from 3.2 this is composer update, no code changes. It delivers:

  • Performance: updating 50,000 rows goes from 30.5s to 3.1s on MySQL, 25.3s to 2.4s on MariaDB, and from over 15 minutes to 2.5s on PostgreSQL 16. Fast imports handle 1,000,000-row CSVs in 6–8 seconds on PostgreSQL, MySQL 8.4 and MariaDB.
  • New product capabilities without custom engineering: insert-or-update for nightly syncs; atomic counter adjustments (+/-/max/min) for inventory, wallets, quotas and scores; conditional "only-if-newer" upserts; bulk deletes that respect soft deletes; key-based bulk updates; returned rows from upserts; reported duplicate skips (verified under 8 concurrent writers); CSV import with row-level validation and error reports; and streaming export to CSV/TSV/JSONL, gzipped, split into parts when needed.
  • Lower maintenance burden: PHPStan level 9 with Larastan now runs in CI, plus a test matrix across PHP 8.1–8.5, Laravel 10–13, MySQL, MariaDB, PostgreSQL, and SQLite.

Cost: 2.x → 3.0 is a short but real breaking-change migration ($raw removed, return types changed, invalid input now throws) — budget a focused task. Everything since 3.0 is a dependency bump with zero code changes. It remains MIT-licensed with no vendor lock-in."

For Engineers: "Laravel Batch v3.3.0 is additive over 3.2 — composer update mavinoo/laravel-batch, no code changes. If you are still on 2.x, the v3.0.0 breaking changes and the SQL-injection fix still apply first.

Added across 3.1 → 3.3 (all additive):

  • Bulk CRUD: updateByKeys() (match on any number of key columns), insertRows() (column => value pairs), upsert() (one query, created_at only on new rows, enums supported, large batches split automatically in one transaction), deleteByKeys() (respects SoftDeletes unless $force is true).
  • pretend(): returns the SQL and bindings a call would run, without executing it or connecting to the database. Use it for dry runs, admin previews, and DB-free tests.
  • Static HasBatch methods for every operation: batchUpdateMultipleCondition(), batchUpdateWithTwoIndex(), batchUpdateByKeys(), batchInsertRows(), batchUpsert(), batchDeleteByKeys().
  • New in 3.3.0 — counter upserts: Batch::upsert($model, $rows, ['sku'], ['qty' => ['+']]) (also ['-'], ['max'], ['min'], on every database). Arithmetic happens in the upsert, atomically.
  • New in 3.3.0 — conditional upserts: Batch::upsert(..., null, ['onlyIf' => ['updated_at' => '>']]) updates an existing row only when the incoming value wins; new rows always insert. upsert() also gained an optional fifth $options argument.
  • New in 3.3.0 — rows back & duplicate reporting: upsertReturning() returns affected rows; insertOrIgnoreRows() returns ['inserted' => n, 'skipped' => [...detailed rows]] and was validated with 8 concurrent writers on overlapping keys across MySQL 8.4, MariaDB and PostgreSQL. Every row is attributed to the connection that inserted it.
  • New in 3.3.0 — fast imports: Batch::import($model, $path, ['fast' => true]) uses COPY on PostgreSQL and LOAD DATA LOCAL INFILE on MySQL/MariaDB. 1M-row CSV: PostgreSQL 18.7s → 6.6s, MySQL 18.7s → 8.4s, MariaDB 12.6s → 6.4s. Bad values that MySQL would silently store with a warning now throw, and nothing is written.
  • New in 3.3.0 — row validation in imports: ['rules' => [...]] returns totalRows, skipped, and errors keyed by row number. import() also returns an errors key in its result.
  • New in 3.3.0 — export: Model::where(...)->exportTo($path, ['maxRows' => n]) writes CSV/TSV/JSONL (optionally gzipped), chunked at constant memory, split into numbered parts via maxRows.
  • New in 3.3.0 — timestamps control: User::withoutTimestamps(fn () => Batch::update(new User, $rows)) is documented and tested for every method (#97).

Performance (from 3.1.0): each UPDATE ... CASE statement is now capped at 100 rows — the database evaluates every row against every CASE branch, so large statements degraded badly. 50,000-row updates: MySQL 30.5s → 3.1s, MariaDB 25.3s → 2.4s, PostgreSQL 16 over 15 min → 2.5s, SQLite 3.7s → 3.4s.

Deprecated: the instance (new Model)->updateMultipleCondition() — use Model::batchUpdateMultipleCondition(). Removal in 4.0; migrate call sites now.

Also: clearer InvalidArgumentExceptions for non-array rows, non-string column names, and invalid arithmetic operators; PHPStan level 9 with Larastan in CI.

Transactions: automatically split batches run in one transaction on the model's connection, insert() rolls back on its connection, and upsert() runs its splits in one transaction. Multi-step business workflows that must be atomic end-to-end still belong inside an explicit DB::transaction().

Operational note: ['fast' => true] will not work where LOAD DATA LOCAL INFILE is disabled server-side; standard-mode import() remains a fallback."*

For PMs: "If anything is still on 2.x, upgrade now — it is a known SQL-injection fix and a breaking release that needs a scheduled engineering task, not an opportunistic dependency bump.

Then ship v3.3.0 immediately. It is additive, needs no code changes from 3.2, and gives us a large performance win (PostgreSQL large updates go from over 15 minutes to about 2.5 seconds) plus a much wider capability set we would otherwise build ourselves:

  • Upsert for nightly syncs, with upsertReturning() when we need the resulting rows.
  • Atomic counter updates for inventory, wallets, quotas and scores.
  • Conditional "only-if-newer" updates so last-write-wins syncs don't clobber newer data.
  • Bulk deletes that respect soft deletes.
  • CSV import at scale — 1,000,000 rows in 6–8 seconds with fast, with a fallback — plus row-level validation errors returned for self-service uploads.
  • Duplicate-skip reporting from insertOrIgnoreRows(), validated under concurrent writers — we can tell users exactly which rows were rejected and why.
  • Streaming export to CSV/TSV/JSONL (gzipped, chunked, splittable) for reporting and compliance downloads.
  • A preview mode (pretend()) we can surface in admin flows before destructive actions.

Schedule one small cleanup: the old instance updateMultipleCondition() is deprecated and goes away in 4.0 — have engineering move call sites to the static methods while they are already in that code. Keep the same boundaries as before: avoid the package for complex multi-statement atomic workflows or highly specialized SQL, wrap critical paths in DB::transaction() where needed, and be aware that ['fast' => true] requires LOAD DATA LOCAL INFILE to be enabled on MySQL/MariaDB."

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.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin