diff --git a/docs/feature-compatibility.txt b/docs/feature-compatibility.txt index bbb5767e1..0c28300ba 100644 --- a/docs/feature-compatibility.txt +++ b/docs/feature-compatibility.txt @@ -151,7 +151,7 @@ The following Eloquent methods are not supported in {+odm-short+}: - *Unsupported as MongoDB uses ObjectIDs* * - Upserts - - *Unsupported* + - ✓ See :ref:`laravel-mongodb-query-builder-upsert`. * - Update Statements - ✓ @@ -216,7 +216,7 @@ Eloquent Features - ✓ * - Upserts - - *Unsupported, but you can use the createOneOrFirst() method* + - ✓ See :ref:`laravel-modify-documents-upsert`. * - Deleting Models - ✓ diff --git a/docs/fundamentals/write-operations.txt b/docs/fundamentals/write-operations.txt index 57bbcd8bc..cc7d81337 100644 --- a/docs/fundamentals/write-operations.txt +++ b/docs/fundamentals/write-operations.txt @@ -258,6 +258,88 @@ An **upsert** operation lets you perform an update or insert in a single operation. This operation streamlines the task of updating a document or inserting one if it does not exist. +Starting in v4.7, you can perform an upsert operation by using either of +the following methods: + +- ``upsert()``: When you use this method, you can perform a **batch + upsert** to change or insert multiple documents in one operation. + +- ``update()``: When you use this method, you must specify the + ``upsert`` option to update all documents that match the query filter + or insert one document if no documents are matched. Only this upsert method + is supported in versions v4.6 and earlier. + +Upsert Method +~~~~~~~~~~~~~ + +The ``upsert(array $values, array|string $uniqueBy, array|null +$update)`` method accepts the following parameters: + +- ``$values``: Array of fields and values that specify documents to update or insert. +- ``$uniqueBy``: List of fields that uniquely identify documents in your + first array parameter. +- ``$update``: Optional list of fields to update if a matching document + exists. If you omit this parameter, {+odm-short+} updates all fields. + +To specify an upsert in the ``upsert()`` method, set parameters +as shown in the following code example: + +.. code-block:: php + :copyable: false + + YourModel::upsert( + [/* documents to update or insert */], + '/* unique field */', + [/* fields to update */], + ); + +Example +^^^^^^^ + +This example shows how to use the ``upsert()`` +method to perform an update or insert in a single operation. Click the +:guilabel:`{+code-output-label+}` button to see the resulting data changes when +there is a document in which the value of ``performer`` is ``'Angel +Olsen'`` in the collection already: + +.. io-code-block:: + + .. input:: /includes/fundamentals/write-operations/WriteOperationsTest.php + :language: php + :dedent: + :start-after: begin model upsert + :end-before: end model upsert + + .. output:: + :language: json + :visible: false + + { + "_id": "...", + "performer": "Angel Olsen", + "venue": "State Theatre", + "genres": [ + "indie", + "rock" + ], + "ticketsSold": 275, + "updated_at": ... + }, + { + "_id": "...", + "performer": "Darondo", + "venue": "Cafe du Nord", + "ticketsSold": 300, + "updated_at": ... + } + +In the document in which the value of ``performer`` is ``'Angel +Olsen'``, the ``venue`` field value is not updated, as the upsert +specifies that the update applies only to the ``ticketsSold`` field. + +Update Method +~~~~~~~~~~~~~ + To specify an upsert in an ``update()`` method, set the ``upsert`` option to ``true`` as shown in the following code example: @@ -278,8 +360,8 @@ following actions: - If the query matches zero documents, the ``update()`` method inserts a document that contains the update data and the equality match criteria data. -Upsert Example -~~~~~~~~~~~~~~ +Example +^^^^^^^ This example shows how to pass the ``upsert`` option to the ``update()`` method to perform an update or insert in a single operation. Click the @@ -291,8 +373,8 @@ matching documents exist: .. input:: /includes/fundamentals/write-operations/WriteOperationsTest.php :language: php :dedent: - :start-after: begin model upsert - :end-before: end model upsert + :start-after: begin model update upsert + :end-before: end model update upsert .. output:: :language: json diff --git a/docs/includes/fundamentals/write-operations/WriteOperationsTest.php b/docs/includes/fundamentals/write-operations/WriteOperationsTest.php index d577ef57b..39143ac09 100644 --- a/docs/includes/fundamentals/write-operations/WriteOperationsTest.php +++ b/docs/includes/fundamentals/write-operations/WriteOperationsTest.php @@ -217,22 +217,55 @@ public function testModelUpdateMultiple(): void } } + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testModelUpsert(): void + { + require_once __DIR__ . '/Concert.php'; + Concert::truncate(); + + // Pre-existing sample document + Concert::create([ + 'performer' => 'Angel Olsen', + 'venue' => 'State Theatre', + 'genres' => [ 'indie', 'rock' ], + 'ticketsSold' => 150, + ]); + + // begin model upsert + Concert::upsert([ + ['performer' => 'Angel Olsen', 'venue' => 'Academy of Music', 'ticketsSold' => 275], + ['performer' => 'Darondo', 'venue' => 'Cafe du Nord', 'ticketsSold' => 300], + ], 'performer', ['ticketsSold']); + // end model upsert + + $this->assertSame(2, Concert::count()); + + $this->assertSame(275, Concert::where('performer', 'Angel Olsen')->first()->ticketsSold); + $this->assertSame('State Theatre', Concert::where('performer', 'Angel Olsen')->first()->venue); + + $this->assertSame(300, Concert::where('performer', 'Darondo')->first()->ticketsSold); + $this->assertSame('Cafe du Nord', Concert::where('performer', 'Darondo')->first()->venue); + } + /** * @runInSeparateProcess * @preserveGlobalState disabled */ - public function testModelUpsert(): void + public function testModelUpdateUpsert(): void { require_once __DIR__ . '/Concert.php'; Concert::truncate(); - // begin model upsert + // begin model update upsert Concert::where(['performer' => 'Jon Batiste', 'venue' => 'Radio City Music Hall']) ->update( ['genres' => ['R&B', 'soul'], 'ticketsSold' => 4000], ['upsert' => true], ); - // end model upsert + // end model update upsert $result = Concert::first(); diff --git a/docs/includes/query-builder/QueryBuilderTest.php b/docs/includes/query-builder/QueryBuilderTest.php index a6a8c752d..c6ef70592 100644 --- a/docs/includes/query-builder/QueryBuilderTest.php +++ b/docs/includes/query-builder/QueryBuilderTest.php @@ -506,6 +506,29 @@ function (Collection $collection) { public function testUpsert(): void { // begin upsert + $result = DB::table('movies') + ->upsert( + [ + ['title' => 'Inspector Maigret', 'recommended' => false, 'runtime' => 128], + ['title' => 'Petit Maman', 'recommended' => true, 'runtime' => 72], + ], + 'title', + 'recommended', + ); + // end upsert + + $this->assertSame(2, $result); + + $this->assertSame(119, DB::table('movies')->where('title', 'Inspector Maigret')->first()['runtime']); + $this->assertSame(false, DB::table('movies')->where('title', 'Inspector Maigret')->first()['recommended']); + + $this->assertSame(true, DB::table('movies')->where('title', 'Petit Maman')->first()['recommended']); + $this->assertSame(72, DB::table('movies')->where('title', 'Petit Maman')->first()['runtime']); + } + + public function testUpdateUpsert(): void + { + // begin update upsert $result = DB::table('movies') ->where('title', 'Will Hunting') ->update( @@ -516,7 +539,7 @@ public function testUpsert(): void ], ['upsert' => true], ); - // end upsert + // end update upsert $this->assertIsInt($result); } diff --git a/docs/includes/query-builder/sample_mflix.movies.json b/docs/includes/query-builder/sample_mflix.movies.json index 57873754e..ef8677520 100644 --- a/docs/includes/query-builder/sample_mflix.movies.json +++ b/docs/includes/query-builder/sample_mflix.movies.json @@ -1,17 +1,10 @@ [ { - "genres": [ - "Short" - ], + "genres": ["Short"], "runtime": 1, - "cast": [ - "Charles Kayser", - "John Ott" - ], + "cast": ["Charles Kayser", "John Ott"], "title": "Blacksmith Scene", - "directors": [ - "William K.L. Dickson" - ], + "directors": ["William K.L. Dickson"], "rated": "UNRATED", "year": 1893, "imdb": { @@ -28,10 +21,7 @@ } }, { - "genres": [ - "Short", - "Western" - ], + "genres": ["Short", "Western"], "runtime": 11, "cast": [ "A.C. Abadie", @@ -40,9 +30,7 @@ "Justus D. Barnes" ], "title": "The Great Train Robbery", - "directors": [ - "Edwin S. Porter" - ], + "directors": ["Edwin S. Porter"], "rated": "TV-G", "year": 1903, "imdb": { @@ -59,11 +47,7 @@ } }, { - "genres": [ - "Short", - "Drama", - "Fantasy" - ], + "genres": ["Short", "Drama", "Fantasy"], "runtime": 14, "rated": "UNRATED", "cast": [ @@ -73,12 +57,8 @@ "Ethel Jewett" ], "title": "The Land Beyond the Sunset", - "directors": [ - "Harold M. Shaw" - ], - "writers": [ - "Dorothy G. Shore" - ], + "directors": ["Harold M. Shaw"], + "writers": ["Dorothy G. Shore"], "year": 1912, "imdb": { "rating": 7.1, @@ -94,10 +74,7 @@ } }, { - "genres": [ - "Short", - "Drama" - ], + "genres": ["Short", "Drama"], "runtime": 14, "cast": [ "Frank Powell", @@ -106,9 +83,7 @@ "Linda Arvidson" ], "title": "A Corner in Wheat", - "directors": [ - "D.W. Griffith" - ], + "directors": ["D.W. Griffith"], "rated": "G", "year": 1909, "imdb": { @@ -125,20 +100,11 @@ } }, { - "genres": [ - "Animation", - "Short", - "Comedy" - ], + "genres": ["Animation", "Short", "Comedy"], "runtime": 7, - "cast": [ - "Winsor McCay" - ], + "cast": ["Winsor McCay"], "title": "Winsor McCay, the Famous Cartoonist of the N.Y. Herald and His Moving Comics", - "directors": [ - "Winsor McCay", - "J. Stuart Blackton" - ], + "directors": ["Winsor McCay", "J. Stuart Blackton"], "writers": [ "Winsor McCay (comic strip \"Little Nemo in Slumberland\")", "Winsor McCay (screenplay)" @@ -158,22 +124,11 @@ } }, { - "genres": [ - "Comedy", - "Fantasy", - "Romance" - ], + "genres": ["Comedy", "Fantasy", "Romance"], "runtime": 118, - "cast": [ - "Meg Ryan", - "Hugh Jackman", - "Liev Schreiber", - "Breckin Meyer" - ], + "cast": ["Meg Ryan", "Hugh Jackman", "Liev Schreiber", "Breckin Meyer"], "title": "Kate & Leopold", - "directors": [ - "James Mangold" - ], + "directors": ["James Mangold"], "writers": [ "Steven Rogers (story)", "James Mangold (screenplay)", @@ -192,5 +147,45 @@ "meter": 62 } } + }, + { + "genres": ["Crime", "Drama"], + "runtime": 119, + "cast": [ + "Jean Gabin", + "Annie Girardot", + "Olivier Hussenot", + "Jeanne Boitel" + ], + "title": "Inspector Maigret", + "directors": ["Jean Delannoy"], + "writers": [ + "Georges Simenon (novel)", + "Jean Delannoy (adaptation)", + "Rodolphe-Maurice Arlaud (adaptation)", + "Michel Audiard (adaptation)", + "Michel Audiard (dialogue)" + ], + "year": 1958, + "imdb": { + "rating": 7.1, + "votes": 690, + "id": 50669 + }, + "countries": ["France", "Italy"], + "type": "movie", + "tomatoes": { + "viewer": { + "rating": 2.7, + "numReviews": 77, + "meter": 14 + }, + "dvd": { + "$date": "2007-01-23T00:00:00.000Z" + }, + "lastUpdated": { + "$date": "2015-09-14T22:31:29.000Z" + } + } } ] diff --git a/docs/query-builder.txt b/docs/query-builder.txt index 041893e34..6b84f9e78 100644 --- a/docs/query-builder.txt +++ b/docs/query-builder.txt @@ -399,7 +399,7 @@ of the ``year`` field for documents in the ``movies`` collections. .. _laravel-query-builder-aggregations: Aggregations -~~~~~~~~~~~~ +------------ The examples in this section show the query builder syntax you can use to perform **aggregations**. Aggregations are operations @@ -417,7 +417,7 @@ aggregations to compute and return the following information: .. _laravel-query-builder-aggregation-groupby: Results Grouped by Common Field Values Example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following example shows how to use the ``groupBy()`` query builder method to retrieve document data grouped by shared values of the ``runtime`` field. @@ -476,7 +476,7 @@ This example chains the following operations to match documents from the .. _laravel-query-builder-aggregation-count: Number of Results Example -^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~ The following example shows how to use the ``count()`` query builder method to return the number of documents @@ -491,7 +491,7 @@ contained in the ``movies`` collection: .. _laravel-query-builder-aggregation-max: Maximum Value of a Field Example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following example shows how to use the ``max()`` query builder method to return the highest numerical @@ -507,7 +507,7 @@ value of the ``runtime`` field from the entire .. _laravel-query-builder-aggregation-min: Minimum Value of a Field Example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following example shows how to use the ``min()`` query builder method to return the lowest numerical @@ -523,7 +523,7 @@ collection: .. _laravel-query-builder-aggregation-avg: Average Value of a Field Example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following example shows how to use the ``avg()`` query builder method to return the numerical average, or @@ -539,7 +539,7 @@ the entire ``movies`` collection. .. _laravel-query-builder-aggregation-sum: Summed Value of a Field Example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following example shows how to use the ``sum()`` query builder method to return the numerical total of @@ -556,7 +556,7 @@ collection: .. _laravel-query-builder-aggregate-matched: Aggregate Matched Results Example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following example shows how to aggregate data from results that match a query. The query matches all @@ -1035,6 +1035,63 @@ following MongoDB-specific write operations: Upsert a Document Example ~~~~~~~~~~~~~~~~~~~~~~~~~ +Starting in v4.7, you can perform an upsert operation by using either of +the following query builder methods: + +- ``upsert()``: When you use this method, you can perform a **batch + upsert** to change or insert multiple documents in one operation. + +- ``update()``: When you use this method, you must specify the + ``upsert`` option to update all documents that match the query filter + or insert one document if no documents are matched. Only this upsert method + is supported in versions v4.6 and earlier. + +Upsert Method +^^^^^^^^^^^^^ + +The ``upsert(array $values, array|string $uniqueBy, array|null +$update)`` query builder method accepts the following parameters: + +- ``$values``: Array of fields and values that specify documents to update or insert. +- ``$uniqueBy``: List of fields that uniquely identify documents in your + first array parameter. +- ``$update``: Optional list of fields to update if a matching document + exists. If you omit this parameter, {+odm-short+} updates all fields. + +The following example shows how to use the ``upsert()`` query builder method +to update or insert documents based on the following instructions: + +- Specify a document in which the value of the ``title`` field is + ``'Inspector Maigret'``, the value of the ``recommended`` field is ``false``, + and the value of the ``runtime`` field is ``128``. + +- Specify a document in which the value of the ``title`` field is + ``'Petit Maman'``, the value of the ``recommended`` field is + ``true``, and the value of the ``runtime`` field is ``72``. + +- Indicate that the ``title`` field uniquely identifies documents in the + scope of your operation. + +- Update only the ``recommended`` field in matched documents. + +.. literalinclude:: /includes/query-builder/QueryBuilderTest.php + :language: php + :dedent: + :start-after: begin upsert + :end-before: end upsert + +The ``upsert()`` query builder method returns the number of +documents that the operation updated, inserted, and modified. + +.. note:: + + The ``upsert()`` method does not trigger events. To trigger events + from an upsert operation, you can use the ``createOrFirst()`` method + instead. + +Update Method +^^^^^^^^^^^^^ + The following example shows how to use the ``update()`` query builder method and ``upsert`` option to update the matching document or insert one with the specified data if it does not exist. When you set the ``upsert`` option to @@ -1044,8 +1101,8 @@ and the ``title`` field and value specified in the ``where()`` query operation: .. literalinclude:: /includes/query-builder/QueryBuilderTest.php :language: php :dedent: - :start-after: begin upsert - :end-before: end upsert + :start-after: begin update upsert + :end-before: end update upsert The ``update()`` query builder method returns the number of documents that the operation updated or inserted.