Most applications fall into a category called “CRUD” apps. CRUD
stands for “Create, Read, Update, Delete”. Diesel provides support for
all four pieces, but in this guide we’re going to look at the different
ways to go about creating INSERT
statements.
The examples for this guide are going to be shown for PostgreSQL, but you can follow along with any backend. The full code examples for all backends are linked at the bottom of this guide.
An insert statement always starts with insert_into
.
The first argument to this function is the table you’re inserting
into.
For this guide, our schema will look like this:
diesel::table! {
{
users -> Integer,
id -> Text,
name -> Nullable<Text>,
hair_color -> Timestamp,
created_at -> Timestamp,
updated_at }
}
Since our functions are going to only operate on the
users
table, we can put
use schema::users::dsl::*;
at the top of our function,
which will let us write insert_into(users)
instead of
insert_into(users::table)
. If you’re importing
table::dsl::*
, make sure it’s always inside a function, not
the top of your module.
If all of the columns on a table have a default, the simplest thing
we can do is call .default_values
.
We could write a function that ran that query like this:
use schema::users::dsl::*;
.default_values().execute(conn) insert_into(users)
It’s worth noting that this code will still compile, even if you don’t have default values on all of your columns. Diesel will ensure that the value you’re assigning has the right type, but it can’t validate whether the column has a default, any constraints that could fail, or any triggers that could fire.
We can use debug_query
to inspect the generated SQL. The exact SQL that is generated may differ
depending on the backend you’re using. If we run
println!("{}", debug_query::<Pg, _>(&our_query));
,
we’ll see the following:
INSERT INTO "users" DEFAULT VALUES -- binds: []
If we want to actually provide values, we can call .values
instead. There are a lot of different arguments we can provide here. The
simplest is a single column/value pair using .eq
.
use schema::users::dsl::*;
.values(name.eq("Sean")).execute(conn) insert_into(users)
This will generate the following SQL:
INSERT INTO "users" ("name") VALUES ($1)
-- binds ["Sean"]
If we want to provide values for more than one column, we can pass a tuple.
insert_into(users).values((name.eq("Tess"), hair_color.eq("Brown")))
.execute(conn)
This will generate the following SQL:
INSERT INTO "users" ("name", "hair_color") VALUES ($1, $2)
-- binds: ["Tess", "Brown"]
Insertable
Working with tuples is the typical way to do an insert if you just
have some values that you want to stick in the database. But what if
your data is coming from another source, like a web form deserialized by
Serde? It’d be annoying to have to write
(name.eq(user.name), hair_color.eq(user.hair_color))
.
Diesel provides the Insertable
trait for this case. Insertable
maps your struct to columns
in the database. We can derive this automatically by adding #[derive(Insertable)]
to our type.
use schema::users;
#[derive(Deserialize, Insertable)]
#[diesel(table_name = users)]
pub struct UserForm<'a> {
: &'a str,
name: Option<&'a str>,
hair_color}
use schema::users::dsl::*;
let json = r#"{ "name": "Sean", "hair_color": "Black" }"#;
let user_form = serde_json::from_str::<UserForm>(json)?;
.values(&user_form).execute(conn)?;
insert_into(users)
Ok(())
This will generate the same SQL as if we had used a tuple.
INSERT INTO "users" ("name", "hair_color") VALUES ($1, $2)
-- binds: ["Sean", "Black"]
If one of the fields is None
, the default value will be
inserted for that field.
use schema::users::dsl::*;
let json = r#"{ "name": "Ruby", "hair_color": null }"#;
let user_form = serde_json::from_str::<UserForm>(json)?;
.values(&user_form).execute(conn)?;
insert_into(users)
Ok(())
That will generate the following SQL:
INSERT INTO "users" ("name", "hair_color") VALUES ($1, DEFAULT)
-- binds: ["Ruby"]
Batch Insert
If we want to insert more than one row at a time, we can do that by
passing a &Vec
or slice of any of the forms used above.
Keep in mind that you’re always passing a reference here.
On backends that support the DEFAULT
keyword (all
backends except SQLite), the data will be inserted in a single query. On
SQLite, one query will be performed per row.
For example, if we wanted to insert two rows with a single value, we
can just use a Vec
.
use schema::users::dsl::*;
insert_into(users).values(&vec![name.eq("Sean"), name.eq("Tess")])
.execute(conn)
Which generates the following SQL:
INSERT INTO "users" ("name") VALUES ($1), ($2)
-- binds ["Sean", "Tess"]
If we wanted to use DEFAULT
for some of our rows, we can
use an option here.
use schema::users::dsl::*;
insert_into(users).values(&vec![Some(name.eq("Sean")), None])
.execute(conn)
Note that the type here is
Option<Eq<Column, Value>>
not
Eq<Column, Option<Value>>
. Doing
column.eq(None)
would insert NULL
not
DEFAULT
. This generates the following SQL:
INSERT INTO "users" ("name") VALUES ($1), (DEFAULT)
-- binds ["Sean"]
We can do the same thing with tuples.
use schema::users::dsl::*;
insert_into(users).values(&vec![
.eq("Sean"), hair_color.eq("Black")),
(name.eq("Tess"), hair_color.eq("Brown")),
(name
]).execute(conn)
Which generates the following SQL:
INSERT INTO "users" ("name", "hair_color")
VALUES ($1, $2), ($3, $4)
-- binds: ["Sean", "Black", "Tess", "Brown"]
Once again, we can use an Option
for any of the fields
to insert DEFAULT
.
use schema::users::dsl::*;
insert_into(users).values(&vec![
.eq("Sean"), Some(hair_color.eq("Black"))),
(name.eq("Ruby"), None),
(name
]).execute(conn)
Which generates the following SQL:
INSERT INTO "users" ("name", "hair_color")
VALUES ($1, $2), ($3, DEFAULT)
-- binds: ["Sean", "Black", "Ruby"]
Finally, Insertable
structs can be used for batch insert
as well.
use schema::users::dsl::*;
let json = r#"[
{ "name": "Sean", "hair_color": "Black" },
{ "name": "Tess", "hair_color": "Brown" }
]"#;
let user_form = serde_json::from_str::<Vec<UserForm>>(json)?;
.values(&user_form).execute(conn)?;
insert_into(users)
Ok(())
This generates the same SQL as if we had used a tuple:
INSERT INTO "users" ("name", "hair_color")
VALUES ($1, $2), ($3, $4)
-- binds: ["Sean", "Black", "Tess", "Brown"]
RETURNING
Clause
The On backends that support the RETURNING
clause (such as
PostgreSQL and SQLite), we can get data back from our insert as well. On
the SQLite backend, support for the RETURNING
clause can be
enabled with a feature flag,
returning_clauses_for_sqlite_3_35
. MySQL does not support
RETURNING
clauses. To get back all of the inserted rows, we
can call .get_results
instead of .execute
.
Given this struct:
#[derive(Queryable, PartialEq, Debug)]
struct User {
: i32,
id: String,
name: Option<String>,
hair_color: SystemTime,
created_at: SystemTime,
updated_at}
We can use get_results
with this test:
use diesel::select;
use schema::users::dsl::*;
let now = select(diesel::dsl::now).get_result::<SystemTime>(conn)?;
let inserted_users = insert_into(users)
.values(&vec![
.eq(1), name.eq("Sean")),
(id.eq(2), name.eq("Tess")),
(id
]).get_results(conn)?;
let expected_users = vec![
{
User : 1,
id: "Sean".into(),
name: None,
hair_color: now,
created_at: now,
updated_at},
{
User : 2,
id: "Tess".into(),
name: None,
hair_color: now,
created_at: now,
updated_at},
;
]assert_eq!(expected_users, inserted_users);
To inspect the SQL generated by .get_results
or
.get_result
, we will need to call .as_query
before passing it to debug_query
. The query in the last
test generates the following SQL:
INSERT INTO "users" ("id", "name") VALUES ($1, $2), ($3, $4)
RETURNING "users"."id", "users"."name", "users"."hair_color",
"users"."created_at", "users"."updated_at"
-- binds: [1, "Sean", 2, "Tess"]
You’ll notice that we’ve never given an explicit value for
created_at
and updated_at
in any of our
examples. With Diesel, you typically won’t set those values in Rust.
Typically these columns get set with
DEFAULT CURRENT_TIMESTAMP
, and a trigger is used to change
updated_at
on updates. If you’re using PostgreSQL, you can
use a built-in trigger by running
SELECT diesel_manage_updated_at('users');
in a
migration.
If we expect one row instead of multiple, we can call
.get_result
instead of .get_results
.
use diesel::select;
use schema::users::dsl::*;
let now = select(diesel::dsl::now).get_result::<SystemTime>(conn)?;
let inserted_user = insert_into(users)
.values((id.eq(3), name.eq("Ruby")))
.get_result(conn)?;
let expected_user = User {
: 3,
id: "Ruby".into(),
name: None,
hair_color: now,
created_at: now,
updated_at};
assert_eq!(expected_user, inserted_user);
This generates the same SQL as get_results
:
INSERT INTO "users" ("id", "name") VALUES ($1, $2)
RETURNING "users"."id", "users"."name", "users"."hair_color",
"users"."created_at", "users"."updated_at"
-- binds: [3, "Ruby"]
Finally, if we only want a single column back, we can call
.returning()
explicitly. This code would return the
inserted ID:
use schema::users::dsl::*;
insert_into(users).values(name.eq("Ruby"))
.returning(id)
.get_result(conn)
Which generates the following SQL:
INSERT INTO "users" ("name") VALUES ($1)
RETURNING "users"."id"
-- binds: ["Ruby"]
“Upsert”
Every type of insert statement covered in this guide can also be used for “insert or update” queries, also known as “upsert”. The specifics of upsert are covered extensively in the API documentation.
For PostgreSQL and SQLite, see the diesel::upsert
module. For MySQL, upsert is done via REPLACE
. See replace_into
for details.
Conclusion
While there are a lot of examples in this guide, ultimately the only
difference between various kinds of insert statements is the argument
passed to .values
.
All examples in this guide are run as part of Diesel’s test suite. You can find the full code examples for each backend at these links: