diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..794e99e --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "ajcastro/insert-update-many", + "description": "Laravel's batch insert or batch update for collection of eloquent models.", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Arjon Jason Castro", + "email": "ajcastro29@gmail.com" + } + ], + "require": { + "php": ">=5.4.0" + }, + "autoload": { + "psr-4": { + "AjCastro\\InsertUpdateMany\\": "src" + } + }, + "extra": { + "laravel": { + "providers": [ + "AjCastro\\InsertUpdateMany\\ServiceProvider" + ], + "aliases": { + } + } + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..9a0d65f --- /dev/null +++ b/readme.md @@ -0,0 +1,115 @@ +# Laravel Eloquent's Insert/Update Many + +Laravel's batch insert or batch update for collection of eloquent models. +Perform single query for batch insert or update operations. +This updates the `created_at` and `updated_at` column of the models and the tables. +The method names `insertMany` and `updateMany` is based from the eloquent method name `saveMany`. +Both `insertMany` and `updateMany` can accept collection of models or just plain array data. + +## Installation + +``` +composer require ajcastro/insert-update-many +``` + +## Usage + +### Insert Many + +Directly pass array or collection of models unlike Laravel's built-in `insert()` which only accept arrays. +This already sets the `created_at` and `updated_at` columns. + +```php +$users = factory(User::class, 10)->make(); +User::insertMany($users); +``` + +#### How it works + +The passed collection of models is transformed to its array form, only including fillable attributes, and passed its array +form to Laravel's native `insert()` method. + +### Update Many + +Update array or collection of models. This perform a single update query for all the passed models. +Only the dirty or changed attributes will be included in the update. +This updates the `updated_at` column of the models and the tables. + +```php +User::updateMany($users); // update many models using id as the default key +User::updateMany($users, 'id'); // same as above +User::updateMany($users, 'username'); // use username as key instead of id + +``` + +#### Specifying which columns to be updated + +```php +User::updateMany($users, 'id', ['email', 'first_name', 'last_name']); +``` + +#### How it works + +This will produce a query like this: + +```sql +UPDATE + `users` +SET + `email` = + CASE + WHEN + `id` = '426' + THEN + 'favian.russel@example.com' + WHEN + `id` = '427' + THEN + 'opurdy@example.org' + WHEN + `id` = '428' + THEN + 'kaylah.hyatt@example.com' + ELSE + `email` + END +, `first_name` = + CASE + WHEN + `id` = '426' + THEN + 'Orie' + WHEN + `id` = '427' + THEN + 'Hubert' + WHEN + `id` = '428' + THEN + 'Mikayla' + ELSE + `first_name` + END +, `last_name` = + CASE + WHEN + `id` = '426' + THEN + 'Weissnat' + WHEN + `id` = '427' + THEN + 'Wiza' + WHEN + `id` = '428' + THEN + 'Keeling' + ELSE + `last_name` + END +WHERE + `id` IN + ( + 426, 427, 428 + ); +``` diff --git a/src/InsertMany.php b/src/InsertMany.php new file mode 100644 index 0000000..d2e026f --- /dev/null +++ b/src/InsertMany.php @@ -0,0 +1,50 @@ +query = $query; + $this->timestamps = $timestamps; + $this->createdAtColumn = $createdAtColumn; + $this->updatedAtColumn = $updatedAtColumn; + } + + public function insert($rows) + { + $ts = now(); + + $rows = collect($rows)->map(function ($row) use ($ts) { + $timestamps = $this->timestamps; + $createdAtColumn = $this->createdAtColumn; + $updatedAtColumn = $this->updatedAtColumn; + + if ($row instanceof Model) { + $timestamps = $row->usesTimestamps(); + $createdAtColumn = $row->getCreatedAtColumn(); + $updatedAtColumn = $row->getUpdatedAtColumn(); + $row->{$createdAtColumn} = $ts; + $row->{$updatedAtColumn} = $ts; + $row = array_only($row->getAttributes(), $row->getFillable()); + } + + if ($timestamps) { + $row = $row + [$createdAtColumn => $ts, $updatedAtColumn => $ts]; + } + + return $row; + }) + ->all(); + + return $this->query->insert($rows); + } +} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php new file mode 100644 index 0000000..2cb6a9f --- /dev/null +++ b/src/ServiceProvider.php @@ -0,0 +1,42 @@ +from, + $key, + $columns + ))->update($rows); + }); + + EloquentBuilder::macro('updateMany', function ($rows, $key = 'id', $columns = []) { + return (new UpdateMany( + $this->getModel()->getTable(), + $key, + !empty($columns) ? $columns : $this->getModel()->getFillable() + ))->update($rows); + }); + + Builder::macro('insertMany', function ($rows) { + return (new InsertMany($this))->insert($rows); + }); + + EloquentBuilder::macro('insertMany', function ($rows) { + return $this->getQuery()->insertMany($rows); + }); + } +} diff --git a/src/UpdateMany.php b/src/UpdateMany.php new file mode 100644 index 0000000..42bc4aa --- /dev/null +++ b/src/UpdateMany.php @@ -0,0 +1,223 @@ +table = $table; + $this->key = $key; + $this->columns = $columns; + $this->updatedAtColumn = $updatedAtColumn; + } + + /** + * Set the key. + * + * @param string $key + * @return $this + */ + public function key($key) + { + $this->key = $key; + return $this; + } + + /** + * Set the columns to update. + * + * @param array $columns + * @return $this + */ + public function columns(array $columns) + { + $this->columns = $columns; + return $this; + } + + /** + * Set updated_at column. + * + * @param string $updatedAtColumn + * @return $this + */ + public function updatedAtColumn($updatedAtColumn) + { + $this->updatedAtColumn = $updatedAtColumn; + return $this; + } + + /** + * Execute update statement on the given rows. + * + * @param array|collect $rows + * @return void + */ + public function update($rows) + { + if (collect($rows)->isEmpty()) { + return; + } + + if (empty($this->columns)) { + $this->columns = $this->getColumnsFromRows($rows); + } + + if ($this->updatedAtColumn) { + $ts = now(); + foreach ($rows as $row) { + $row[$this->updatedAtColumn] = $ts; + } + } + + DB::statement($this->updateSql($rows)); + } + + /** + * Get columns from rows. + * + * @param array|collect $rows + * @return array + */ + protected function getColumnsFromRows($rows) + { + $row = []; + + foreach ($rows as $r) { + if ($r instanceof Model) { + $r = $r->getAttributes(); + } + + $row = array_merge($row, $r); + } + + return array_keys($row); + } + + /** + * Return the columns to be updated. + * + * @return array + */ + public function getColumns() + { + if ($this->updatedAtColumn) { + $this->columns[] = $this->updatedAtColumn; + } + + return array_unique($this->columns); + } + + /** + * Return the update sql. + * + * @param array|collect $rows + * @return string + */ + protected function updateSql($rows) + { + $updateColumns = implode(', ', $this->updateColumns($rows)); + $whereInKeys = implode(', ', $this->whereInKeys($rows)); + + return "UPDATE `{$this->table}` SET {$updateColumns} where `{$this->key}` in ({$whereInKeys})"; + } + + /** + * Return the where in keys. + * + * @param array|collect $rows + * @return array + */ + protected function whereInKeys($rows) + { + return collect($rows)->pluck($this->key)->all(); + } + + /** + * Return the update columns. + * + * @param array|collect $rows + * @return array + */ + protected function updateColumns($rows) + { + $updates = []; + + foreach ($this->getColumns() as $column) { + $cases = $this->cases($column, $rows); + + if (empty($cases)) { + continue; + } + + $updates[] = " `{$column}` = ". + ' CASE '. + implode(' ', $cases). + " ELSE `{$column}` END"; + } + + return $updates; + } + + /** + * Return an array of column cases. + * + * @param string $column + * @param array|collect $rows + * @return array + */ + protected function cases($column, $rows) + { + $cases = []; + + foreach ($rows as $row) { + $value = addslashes($row[$column]); + + // Set null in mysql database + if (is_null($row[$column])) { + $value = 'null'; + } else { + $value = "'{$value}'"; + } + + if ($this->includeCase($row, $column)) { + $cases[] = "WHEN `{$this->key}` = '{$row[$this->key]}' THEN {$value}"; + } + } + + return $cases; + } + + /** + * Check if the case will be included. + * + * @param array|model $row + * @param string $column + * @return bool + */ + protected function includeCase($row, $column) + { + if ($row instanceof Model) { + return $row->isDirty($column); + } + + return true; + } +}