April 15, 2019
This blog is part of our Rails 6 series.
Rails 6 has added support for bulk inserts similar to how bulk update is
supported using update_all and bulk delete is supported using delete_all.
Bulk inserts can be performed using newly added methods: insert_all,
insert_all! and upsert_all.
All of these new methods allow the insertion of multiple records of the same
model into the database. A single INSERT SQL query is prepared by these
methods and a single sql statement is sent to the database, without
instantiating the model or invoking Active Record callbacks or validations.
During bulk insertion, violation of primary key, violation of unique indexes, and violation of unique constraints is possible. Rails leverages database-specific features to either skip, or upsert the duplicates, depending on the case.
Let's discuss insert_all, insert_all! and upsert_all methods in detail,
which are all used to perform bulk insert.
We will create an articles table with two unique indexes.
create_table :articles do |t|
t.string :title, null: false
t.string :slug, null: false
t.string :author, null: false
t.text :description
t.index :slug, unique: true
t.index [:title, :author], unique: true
end
Note that we do not allow duplicate slug columns. We also prevent records from
having duplicate title and author columns together.
To try out the examples provided in this blog post, please ensure to always
clean up the articles table before running any example.
Let's say we want to insert multiple articles at once into the database. It is possible that certain records may violate the unique constraint(s) of the table. Such records are considered duplicates.
In other words, rows or records are determined to be unique by every unique index on the table by default.
To skip the duplicate rows or records, and insert the rest of the records at
once, we can use ActiveRecord::Persistence#insert_all method.
Let's run the following example on a PostgreSQL database.
result = Article.insert_all(
[
{ id: 1,
title: 'Handling 1M Requests Per Second',
author: 'John',
slug: '1m-req-per-second' },
{ id: 1, # duplicate 'id' here
title: 'Type Safety in Elm',
author: 'George',
slug: 'elm-type-safety' },
{ id: 2,
title: 'Authentication with Devise - Part 1',
author: 'Laura',
slug: 'devise-auth-1' },
{ id: 3,
title: 'Authentication with Devise - Part 1',
author: 'Laura', # duplicate 'title' & 'author' here
slug: 'devise-auth-2' },
{ id: 4,
title: 'Dockerizing and Deploying Rails App to Kubernetes',
author: 'Paul',
slug: 'rails-on-k8s' },
{ id: 5,
title: 'Elm on Rails',
author: 'Amanda',
slug: '1m-req-per-second' }, # duplicate 'slug' here
{ id: 6,
title: 'Working Remotely',
author: 'Greg',
slug: 'working-remotely' }
]
)
# Bulk Insert (2.3ms) INSERT INTO "articles"("id","title","author","slug") VALUES (1, 'Handling 1M Requests [...snip...] 'working-remotely') ON CONFLICT DO NOTHING RETURNING "id"
puts result.inspect
#<ActiveRecord::Result:0x00007fb6612a1ad8 @columns=["id"], @rows=[[1], [2], [4], [6]], @hash_rows=nil, @column_types={"id"=>#<ActiveModel::Type::Integer:0x00007fb65f420078 @precision=nil, @scale=nil, @limit=8, @range=-9223372036854775808...9223372036854775808>}>
puts Article.count
# 4
The insert_all method accepts a mandatory argument which should be an array of
hashes with the attributes of the same model. The keys in all hashes should be
same.
Notice the ON CONFLICT DO NOTHING clause in the INSERT query. This clause is
supported by PostgreSQL and SQLite databases. This instructs the database that
when there is a conflict or a unique key constraint violation during bulk insert
operation, to skip the conflicting record silently and proceed with the
insertion of the next record.
In the above example, we have exactly 3 records which violate various unique
constraints defined on the articles table.
One of the records being inserted has a duplicate id: 1 attribute, which
violates unique primary key constraint. Another record that has duplicate
title: 'Authentication with Devise - Part 1', author: 'Laura' attributes
violates the multi-column unique index defined on title and author columns.
Another record has duplicate slug: '1m-req-per-second' attributes violates the
unique index defined on the slug column.
All of these records that violate any unique constraint or unique index are skipped and are not inserted into the database.
If successful, ActiveRecord::Persistence#insert_all returns an instance of
ActiveRecord::Result. The contents of the result vary per database. In case of
PostgreSQL database, this result instance holds information about the
successfully inserted records such as the chosen column names, values of the
those columns in each successfully inserted row, etc.
For PostgreSQL, by default, insert_all method appends RETURNING "id" clause
to the SQL query where id is the primary key(s). This clause instructs the
database to return the id of every successfully inserted record. By inspecting
the result, especially the @columns=["id"], @rows=[[1], [2], [4], [6]]
attributes of the result instance, we can see that the records having id
attribute with values 1, 2, 4 and 6 were successfully inserted.
What if we want to see more attributes and not just the id attribute of the
successfully inserted records in the result?
We should use the optional returning option, which accepts an array of
attribute names, which should be returned for all successfully inserted records!
result = Article.insert_all(
[
{ id: 1,
title: 'Handling 1M Requests Per Second',
author: 'John',
slug: '1m-req-per-second' },
#...snip...
],
returning: %w[ id title ]
)
# Bulk Insert (2.3ms) INSERT INTO "articles"("id","title","author","slug") VALUES (1, 'Handling 1M Requests [...snip...] 'working-remotely') ON CONFLICT DO NOTHING RETURNING "id","title"
puts result.inspect
#<ActiveRecord::Result:0x00007f902a1196f0 @columns=["id", "title"], @rows=[[1, "Handling 1M Requests Per Second"], [2, "Authentication with Devise - Part 1"], [4, "Dockerizing and Deploying Rails App to Kubernetes"], [6, "Working Remotely"]], @hash_rows=nil, @column_types={"id"=>#<ActiveModel::Type::Integer:0x00007f90290ca8d0 @precision=nil, @scale=nil, @limit=8, @range=-9223372036854775808...9223372036854775808>, "title"=>#<ActiveModel::Type::String:0x00007f9029978298 @precision=nil, @scale=nil, @limit=nil>}>
puts result.pluck("id", "title").inspect
#[[1, "Handling 1M Requests Per Second"], [2, "Authentication with Devise - Part 1"], [4, "Dockerizing and Deploying Rails App to Kubernetes"], [6, "Working Remotely"]]
Notice how the INSERT query appends RETURNING "id","title" clause and the
result now holds the id and title attributes of the successfully inserted
records.
Similar to PostgreSQL, the violating records are skipped during the bulk insert
operation performed using insert_all when we run our example on a SQLite
database.
result = Article.insert_all(
[
{ id: 1, title: 'Handling 1M Requests Per Second', author: 'John', slug: '1m-req-per-second' },
{ id: 1, title: 'Type Safety in Elm', author: 'George', slug: 'elm-type-safety' }, # duplicate 'id' here
#...snip...
]
)
# Bulk Insert (1.6ms) INSERT INTO "articles"("id","title","author","slug") VALUES (1, 'Handling 1M Requests [...snip...] 'working-remotely') ON CONFLICT DO NOTHING
puts result.inspect
#<ActiveRecord::Result:0x00007fa9df448ff0 @columns=[], @rows=[], @hash_rows=nil, @column_types={}>
puts Article.pluck(:id, :title)
#[[1, "Handling 1M Requests Per Second"], [2, "Authentication with Devise - Part 1"], [4, "Dockerizing and Deploying Rails App to Kubernetes"], [6, "Working Remotely"]]
puts Article.count
# 4
Note that since SQLite does not support RETURING clause, it is not being added
to the SQL query. Therefore, the returned ActiveRecord::Result instance does
not contain any useful information.
If we try to explicitly use the returning option when the database being used
is SQLite, then the insert_all method throws an error.
Article.insert_all(
[
{ id: 1, title: 'Handling 1M Requests Per Second', author: 'John', slug: '1m-req-per-second' },
#...snip...
],
returning: %w[ id title ]
)
# ActiveRecord::ConnectionAdapters::SQLite3Adapter does not support :returning (ArgumentError)
The records that violate primary key, unique key constraints, or unique indexes
are skipped during bulk insert operation performed using insert_all on a MySQL
database.
result = Article.insert_all(
[
{ id: 1, title: 'Handling 1M Requests Per Second', author: 'John', slug: '1m-req-per-second' },
{ id: 1, title: 'Type Safety in Elm', author: 'George', slug: 'elm-type-safety' }, # duplicate 'id' here
#...snip...
]
)
# Bulk Insert (20.3ms) INSERT INTO `articles`(`id`,`title`,`author`,`slug`) VALUES (1, 'Handling 1M Requests [...snip...] 'working-remotely') ON DUPLICATE KEY UPDATE `id`=`id`
puts result.inspect
#<ActiveRecord::Result:0x000055d6cfea7580 @columns=[], @rows=[], @hash_rows=nil, @column_types={}>
puts Article.pluck(:id, :title)
#[[1, "Handling 1M Requests Per Second"], [2, "Authentication with Devise - Part 1"], [4, "Dockerizing and Deploying Rails App to Kubernetes"], [6, "Working Remotely"]]
puts Article.count
# 4
Here, the ON DUPLICATE KEY UPDATE 'id'='id' clause in the INSERT query is
essentially doing the same thing as the ON CONFLICT DO NOTHING clause
supported by PostgreSQL and SQLite.
Since MySQL does not support RETURING clause, it is not being included in the
SQL query and therefore, the result doesn't contain any useful information.
Explicitly trying to use returning option with insert_all method on a MySQL
database throws
ActiveRecord::ConnectionAdapters::Mysql2Adapter does not support :returning
error.
In the previous case, we were skipping the records that were violating any unique constraints. In some case, we may want to skip duplicates caused by only a specific unique index but abort transaction if the other records violate any other unique constraints.
The optional unique_by option of the insert_all method allows to define such
a unique constraint.
Let's see an example to skip duplicate records that violate only the specified
unique index :index_articles_on_title_and_author using unique_by option. The
duplicate records that do not violate index_articles_on_title_and_author index
are not skipped, and therefore throw an error.
result = Article.insert_all(
[
{ .... },
{ .... }, # duplicate 'id' here
{ .... },
{ .... }, # duplicate 'title' and 'author' here
{ .... },
{ .... }, # duplicate 'slug' here
{ .... }
],
unique_by: :index_articles_on_title_and_author
)
# PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "articles_pkey" (ActiveRecord::RecordNotUnique)
# DETAIL: Key (id)=(1) already exists.
In case of SQLite, the error appears as shown below.
# SQLite3::ConstraintException: UNIQUE constraint failed: articles.id (ActiveRecord::RecordNotUnique)
In this case we get ActiveRecord::RecordNotUnique error which is caused by the
violation of primary key constraint on the id column.
It didn't skip the second record in the example above which violated the unique
index on primary key id since the unique_by option was specified with a
different unique index.
When an exception occurs, no record persists to the database since insert_all
executes just a single SQL query.
The unique_by option can be identified by columns or a unique index name.
~~~ruby unique_by: :index_articles_on_title_and_author
unique_by: %i[ title author ]
unique_by: :slug
unique_by: %i[ :slug ]
unique_by: :index_articles_on_slug
Let's remove (or fix) the record that has duplicate primary key
and
re-run the above example.
~~~ruby
result = Article.insert_all(
[
{ .... },
{ .... },
{ .... },
{ .... }, # duplicate 'title' and 'author' here
{ .... },
{ .... }, # duplicate 'slug' here
{ .... }
],
unique_by: :index_articles_on_title_and_author
)
# PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_articles_on_slug" (ActiveRecord::RecordNotUnique)
# DETAIL: Key (slug)=(1m-req-per-second) already exists.
In case of SQLite, the error looks appears as shown below.
# SQLite3::ConstraintException: UNIQUE constraint failed: articles.slug (ActiveRecord::RecordNotUnique)
The ActiveRecord::RecordNotUnique error in the example above now says the
index_articles_on_slug unique constraint is violated. Note how it
intentionally didn't raise an error for the unique constraint violated on the
title and author columns by the fourth record in the examples above.
Now we will remove (or fix) the record that has same slug.
result = Article.insert_all(
[
{ .... },
{ .... },
{ .... },
{ .... }, # duplicate 'title' and 'author' here
{ .... },
{ .... },
{ .... }
],
unique_by: :index_articles_on_title_and_author
)
# Bulk Insert (2.5ms) INSERT INTO "articles"("id","title","author","slug") VALUES (1, 'Handling 1M Requests Per Second', [...snip...] 'working-remotely') ON CONFLICT ("title","author") DO NOTHING RETURNING "id"
puts result.inspect
#<ActiveRecord::Result:0x00007fada2069828 @columns=["id"], @rows=[[1], [7], [2], [4], [5], [6]], @hash_rows=nil, @column_types={"id"=>#<ActiveModel::Type::Integer:0x00007fad9fdb9df0 @precision=nil, @scale=nil, @limit=8, @range=-9223372036854775808...9223372036854775808>}>
Here, the fourth record was skipped since that record violates the unique index
index_articles_on_title_and_author specified by the unique_by option.
Similarly, we can specify a different unique index using the unique_by option.
For example, if we specify unique_by: :slug option then the records containing
duplicate slug columns will be skipped, but would raise
ActiveRecord::RecordNotUnique exception if any record violates other unique
constraints.
The unique_by option is not supported when the database is MySQL.
The insert_all! method (with bang version) never skips a duplicate record. If
a record violates any unique constraints, then insert_all! method would simply
throw an ActiveRecord::RecordNotUnique error.
When database is PostgreSQL, insert_all! method can accept optional
returning option, which we discussed in depth in 1.1 section above.
The unique_by option is not supported by the insert_all! method.
So far, in the sections 1, 2 and 3 above, we discussed either skipping the duplicates or raising an exception if a duplicate is encountered during bulk inserts. Sometimes, we want to update the existing record when a duplicate occurs otherwise insert a new record. This operation is called upsert because either it tries to update the record, or if there is no record to update, then it tries to insert.
The upsert_all method in Rails 6 allows performing bulk upserts.
Let's see it's usage and behavior with different database systems.
upsert_all in MySQLLet's try to bulk upsert multiple articles containing some duplicates.
result = Article.upsert_all(
[
{ id: 1, title: 'Handling 1M Requests Per Second', author: 'John', slug: '1m-req-per-second' },
{ id: 1, .... }, # duplicate 'id' here
{ id: 2, .... },
{ id: 3, .... }, # duplicate 'title' and 'author' here
{ id: 4, .... },
{ id: 5, .... }, # duplicate 'slug' here
{ id: 6, .... }
]
)
# Bulk Insert (26.3ms) INSERT INTO `articles`(`id`,`title`,`author`,`slug`) VALUES (1, 'Handling 1M Requests Per Second', 'John', [...snip...] 'working-remotely') ON DUPLICATE KEY UPDATE `title`=VALUES(`title`),`author`=VALUES(`author`),`slug`=VALUES(`slug`)
puts result.inspect
#<ActiveRecord::Result:0x000055a43c1fae10 @columns=[], @rows=[], @hash_rows=nil, @column_types={}>
puts Article.count
# 5
puts Article.all
#<ActiveRecord::Relation [#<Article id: 1, title: "Type Safety in Elm", slug: "elm-type-safety", author: "George", description: nil>, #<Article id: 2, title: "Authentication with Devise - Part 1", slug: "devise-auth-2", author: "Laura", description: nil>, #<Article id: 4, title: "Dockerizing and Deploying Rails App to Kubernetes", slug: "rails-on-k8s", author: "Paul", description: nil>, #<Article id: 5, title: "Elm on Rails", slug: "1m-req-per-second", author: "Amanda", description: nil>, #<Article id: 6, title: "Working Remotely", slug: "working-remotely", author: "Greg", description: nil>]>
The persisted records in the database look exactly as intended. Let's discuss it in detail.
The second row in the input array that has the id: 1 attribute replaced the
first row, which also had the duplicate id: 1 attribute.
The fourth row that has id: 3 replaced the attributes of the third row since
both had duplicate "title" and "author" attributes.
The rest of the rows were not duplicates or no longer became duplicates, and therefore were inserted without any issues.
Note that the returning and unique_by options are not supported in the
upsert_all method when the database is MySQL.
upsert_all in SQLiteLet's try to execute the same example from the above section 4.1 when database is SQLite.
result = Article.upsert_all(
[
{ id: 1, title: 'Handling 1M Requests Per Second', author: 'John', slug: '1m-req-per-second' },
{ id: 1, title: 'Type Safety in Elm', author: 'George', slug: 'elm-type-safety' }, # duplicate 'id' here
{ id: 2, title: 'Authentication with Devise - Part 1', author: 'Laura', slug: 'devise-auth-1' },
{ id: 3, title: 'Authentication with Devise - Part 1', author: 'Laura', slug: 'devise-auth-2' }, # duplicate 'title' and 'author' here
{ id: 4, title: 'Dockerizing and Deploying Rails App to Kubernetes', author: 'Paul', slug: 'rails-on-k8s' },
{ id: 5, title: 'Elm on Rails', author: 'Amanda', slug: '1m-req-per-second' }, # duplicate 'slug' here
{ id: 6, title: 'Working Remotely', author: 'Greg', slug: 'working-remotely' }
]
)
# Bulk Insert (4.0ms) INSERT INTO "articles"("id","title","author","slug") VALUES (1, 'Handling 1M Requests Per Second', [...snip...] 'working-remotely') ON CONFLICT ("id") DO UPDATE SET "title"=excluded."title","author"=excluded."author","slug"=excluded."slug"
# SQLite3::ConstraintException: UNIQUE constraint failed: articles.title, articles.author (ActiveRecord::RecordNotUnique)
The bulk upsert operation failed in the above example due to
ActiveRecord::RecordNotUnique exception.
Why it didn't work similar to MySQL?
As per the documentation of MySQL, an upsert operation takes place if a new record violates any unique constraint.
Whereas, in case of SQLite, by default, new record replaces existing record when
both the existing and new record have the same primary key. If a record violates
any other unique constraints other than the primary key, it then raises
ActiveRecord::RecordNotUnique exception.
The ON CONFLICT ("id") DO UPDATE clause in the SQL query above conveys the
same intent.
Therefore, upsert_all in SQLite doesn't behave exactly same as in MySQL.
As a workaround, we will need to upsert records with the help of multiple
upsert_all calls with the usage of unique_by option.
If a duplicate record is encountered during the upsert operation, which violates
the unique index specified using unique_by option then it will replace the
attributes of the existing matching record.
Let's try to understand this workaround with another example.
Article.upsert_all(
[
{ id: 1, title: 'Handling 1M Requests Per Second', author: 'John', slug: '1m-req-per-second' },
{ id: 1, title: 'Type Safety in Elm', author: 'George', slug: 'elm-type-safety' }, # duplicate 'id' here
],
unique_by: :id
)
Article.upsert_all(
[
{ id: 2, title: 'Authentication with Devise - Part 1', author: 'Laura', slug: 'devise-auth-1' },
{ id: 3, title: 'Authentication with Devise - Part 1', author: 'Laura', slug: 'devise-auth-2' }, # duplicate 'title' and 'author' here
{ id: 4, title: 'Dockerizing and Deploying Rails App to Kubernetes', author: 'Paul', slug: 'rails-on-k8s' },
{ id: 5, title: 'Elm on Rails', author: 'Amanda', slug: '1m-req-per-second' }, # duplicate 'slug' here
{ id: 6, title: 'Working Remotely', author: 'Greg', slug: 'working-remotely' }
],
unique_by: %i[ title author ]
)
puts Article.count
# 5
puts Article.all
#<ActiveRecord::Relation [#<Article id: 1, title: "Type Safety in Elm", slug: "elm-type-safety", author: "George", description: nil>, #<Article id: 2, title: "Authentication with Devise - Part 1", slug: "devise-auth-2", author: "Laura", description: nil>, #<Article id: 4, title: "Dockerizing and Deploying Rails App to Kubernetes", slug: "rails-on-k8s", author: "Paul", description: nil>, #<Article id: 5, title: "Elm on Rails", slug: "1m-req-per-second", author: "Amanda", description: nil>, #<Article id: 6, title: "Working Remotely", slug: "working-remotely", author: "Greg", description: nil>]>
Here, we first tried to upsert all the records which violated the unique primary
key index on id column. Later, we upsert successfully all the remaining
records, which violated the unique index on the title and author columns.
Note that since the first record's slug attribute was already replaced with
the second record's slug attribute; the last second record having id: 5
didn't raise an exception because of duplicate slug column.
upsert_all in PostgreSQLWe will run the same example in the 4.1 section above with PostgreSQL database.
result = Article.upsert_all(
[
{ id: 1, title: 'Handling 1M Requests Per Second', author: 'John', slug: '1m-req-per-second' },
{ id: 1, title: 'Type Safety in Elm', author: 'George', slug: 'elm-type-safety' }, # duplicate 'id' here
{ id: 2, title: 'Authentication with Devise - Part 1', author: 'Laura', slug: 'devise-auth-1' },
{ id: 3, title: 'Authentication with Devise - Part 1', author: 'Laura', slug: 'devise-auth-2' }, # duplicate 'title' and 'author' here
{ id: 4, title: 'Dockerizing and Deploying Rails App to Kubernetes', author: 'Paul', slug: 'rails-on-k8s' },
{ id: 5, title: 'Elm on Rails', author: 'Amanda', slug: '1m-req-per-second' }, # duplicate 'slug' here
{ id: 6, title: 'Working Remotely', author: 'Greg', slug: 'working-remotely' }
]
)
# PG::CardinalityViolation: ERROR: ON CONFLICT DO UPDATE command cannot affect row a second time (ActiveRecord::StatementInvalid)
# HINT: Ensure that no rows proposed for insertion within the same command have duplicate constrained values.
The bulk upsert operation failed in the above example due to
ActiveRecord::StatementInvalid exception which was caused by another
PG::CardinalityViolation exception.
The PG::CardinalityViolation exception originates from
here.
The PG::CardinalityViolation exception occurs when a row cannot be updated a
second time in the same ON CONFLICT DO UPDATE SQL query. PostgreSQL assumes
this behavior would lead the same row to updated a second time in the same SQL
query, in unspecified order, non-deterministically.
PostgreSQL recommends it is the developer's responsibility to prevent this situation from occurring.
Here's more discussion about this issue - rails/rails#35519.
Therefore, the upsert_all method doesn't work as intended due to the above
limitation in PostgreSQL.
As a workaround, we can divide the single upsert_all query into multiple
upsert_all queries with the use of unique_by option similar to how we did in
case of SQLite in the 4.2 section above.
Article.insert_all(
[
{ id: 1, title: 'Handling 1M requests per second', author: 'John', slug: '1m-req-per-second' },
{ id: 2, title: 'Authentication with Devise - Part 1', author: 'Laura', slug: 'devise-auth-1' },
{ id: 4, title: 'Dockerizing and deploy Rails app to Kubernetes', author: 'Paul', slug: 'rails-on-k8s' },
{ id: 6, title: 'Working Remotely', author: 'Greg', slug: 'working-remotely' }
]
)
Article.upsert_all(
[
{ id: 1, title: 'Type Safety in Elm', author: 'George', slug: 'elm-type-safety' }, # duplicate 'id' here
]
)
Article.upsert_all(
[
{ id: 3, title: 'Authentication with Devise - Part 1', author: 'Laura', slug: 'devise-auth-2' }, # duplicate 'title' and 'author' here
],
unique_by: :index_articles_on_title_and_author
)
Article.upsert_all(
[
{ id: 5, title: 'Elm on Rails', author: 'Amanda', slug: '1m-req-per-second' }, # duplicate 'slug' here
]
)
puts Article.count
# 5
puts Article.all
#<ActiveRecord::Relation [#<Article id: 1, title: "Type Safety in Elm", slug: "elm-type-safety", author: "George", description: nil>, #<Article id: 2, title: "Authentication with Devise - Part 1", slug: "devise-auth-2", author: "Laura", description: nil>, #<Article id: 4, title: "Dockerizing and deploy Rails app to Kubernetes", slug: "rails-on-k8s", author: "Paul", description: nil>, #<Article id: 5, title: "Elm on Rails", slug: "1m-req-per-second", author: "Amanda", description: nil>, #<Article id: 6, title: "Working Remotely", slug: "working-remotely", author: "Greg", description: nil>]>
For reference, note that the upsert_all method also accepts returning option
for PostgreSQL which we have already discussed in the 1.1 section above.
insert, insert! and upsertRails 6 has also introduced three more additional methods namely insert,
insert! and upsert for convenience.
The insert method inserts a single record into the database. If that record
violates a uniqueness constrain, then the insert method will skip inserting
record into the database without raising an exception.
Similarly, the insert! method also inserts a single record into the database,
but will raise ActiveRecord::RecordNotUnique exception if that record violates
a uniqueness constraint.
The upsert method inserts or updates a single record into the database similar
to how upsert_all does.
The methods insert, insert! and upsert are wrappers around insert_all,
insert_all! and upsert_all respectively.
Let's see some examples to understand the usage of these methods.
Article.insert({ id: 5, title: 'Elm on Rails', author: 'Amanda', slug: '1m-req-per-second' }, unique_by: :slug)
# is same as
Article.insert_all([{ id: 5, title: 'Elm on Rails', author: 'Amanda', slug: '1m-req-per-second' }], unique_by: :slug)
Article.insert!({ id: 5, title: 'Elm on Rails', author: 'Amanda', slug: '1m-req-per-second' }, returning: %w[ id title ])
# is same as
Article.insert_all!([{ id: 5, title: 'Elm on Rails', author: 'Amanda', slug: '1m-req-per-second' }], returning: %w[ id title ])
Article.upsert({ id: 5, title: 'Elm on Rails', author: 'Amanda', slug: '1m-req-per-second' }, unique_by: :slug, returning: %w[ id title ])
# is same as
Article.upsert_all([{ id: 5, title: 'Elm on Rails', author: 'Amanda', slug: '1m-req-per-second' }], unique_by: :slug, returning: %w[ id title ])
To learn more about the bulk insert feature and its implementation, please check rails/rails#35077, rails/rails#35546 and rails/rails#35854.
If this blog was helpful, check out our full blog archive.