1
0
mirror of https://github.com/actix/examples synced 2025-04-22 08:34:52 +02:00

Merge pull request #992 from alexted/feature/diesel-async-example

Diesel-async usage example
This commit is contained in:
Rob Ede 2025-04-09 21:45:35 +00:00 committed by GitHub
commit c5ce3c118e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 547 additions and 5 deletions

View File

@ -21,16 +21,21 @@ jobs:
with: with:
toolchain: ${{ matrix.version }} toolchain: ${{ matrix.version }}
- name: Install system packages
run: |
sudo apt-get update
sudo apt-get -y install sqlite3
sudo apt-get -y install libpq-dev
- name: Install DB CLI tools - name: Install DB CLI tools
run: | run: |
cargo install --force sqlx-cli --no-default-features --features=sqlite,rustls cargo install --force sqlx-cli --no-default-features --features=sqlite,rustls
cargo install --force diesel_cli --no-default-features --features sqlite cargo install --force diesel_cli --no-default-features --features=sqlite,postgres
- name: Create Test DBs - name: Create Test DBs
env: env:
DATABASE_URL: sqlite://./todo.db DATABASE_URL: sqlite://./todo.db
run: | run: |
sudo apt-get update && sudo apt-get install sqlite3
sqlx database create sqlx database create
chmod a+rwx ./todo.db chmod a+rwx ./todo.db
sqlx migrate run --source=./basics/todo/migrations sqlx migrate run --source=./basics/todo/migrations

View File

@ -26,16 +26,21 @@ jobs:
with: with:
toolchain: ${{ matrix.version }} toolchain: ${{ matrix.version }}
- name: Install system packages
run: |
sudo apt-get update
sudo apt-get -y install sqlite3
sudo apt-get -y install libpq-dev
- name: Install DB CLI tools - name: Install DB CLI tools
run: | run: |
cargo install --force sqlx-cli --no-default-features --features=sqlite,rustls cargo install --force sqlx-cli --no-default-features --features=sqlite,rustls
cargo install --force diesel_cli --no-default-features --features sqlite cargo install --force diesel_cli --no-default-features --features=sqlite,postgres
- name: Create Test DBs - name: Create Test DBs
env: env:
DATABASE_URL: sqlite://./todo.db DATABASE_URL: sqlite://./todo.db
run: | run: |
sudo apt-get update && sudo apt-get install sqlite3
sqlx database create sqlx database create
chmod a+rwx ./todo.db chmod a+rwx ./todo.db
sqlx migrate run --source=./basics/todo/migrations sqlx migrate run --source=./basics/todo/migrations

75
Cargo.lock generated
View File

@ -1730,6 +1730,18 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "bb8"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89aabfae550a5c44b43ab941844ffcd2e993cb6900b342debf59e9ea74acdb8"
dependencies = [
"async-trait",
"futures-util",
"parking_lot",
"tokio",
]
[[package]] [[package]]
name = "bigdecimal" name = "bigdecimal"
version = "0.3.1" version = "0.3.1"
@ -2633,6 +2645,20 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "db-diesel-async"
version = "1.0.0"
dependencies = [
"actix-web",
"diesel",
"diesel-async",
"dotenvy",
"env_logger",
"log",
"serde",
"uuid",
]
[[package]] [[package]]
name = "db-mongo" name = "db-mongo"
version = "0.0.0" version = "0.0.0"
@ -2663,7 +2689,7 @@ version = "0.0.0"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"env_logger", "env_logger",
"redis 0.27.6", "redis 0.28.2",
"serde", "serde",
"tracing", "tracing",
] ]
@ -2860,6 +2886,21 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "diesel-async"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51a307ac00f7c23f526a04a77761a0519b9f0eb2838ebf5b905a58580095bdcb"
dependencies = [
"async-trait",
"bb8",
"diesel",
"futures-util",
"scoped-futures",
"tokio",
"tokio-postgres",
]
[[package]] [[package]]
name = "diesel_derives" name = "diesel_derives"
version = "2.2.4" version = "2.2.4"
@ -6538,6 +6579,29 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"ryu", "ryu",
"sha1_smol", "sha1_smol",
"tokio",
"tokio-util",
"url",
]
[[package]]
name = "redis"
version = "0.28.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e37ec3fd44bea2ec947ba6cc7634d7999a6590aca7c35827c250bc0de502bda6"
dependencies = [
"arc-swap",
"backon",
"bytes",
"combine",
"futures-channel",
"futures-util",
"itoa",
"num-bigint",
"percent-encoding",
"pin-project-lite",
"ryu",
"sha1_smol",
"socket2", "socket2",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -7154,6 +7218,15 @@ dependencies = [
"parking_lot", "parking_lot",
] ]
[[package]]
name = "scoped-futures"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b24aae2d0636530f359e9d5ef0c04669d11c5e756699b27a6a6d845d8329091"
dependencies = [
"pin-project-lite",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"

View File

@ -17,6 +17,7 @@ members = [
"cors/backend", "cors/backend",
"data-factory", "data-factory",
"databases/diesel", "databases/diesel",
"databases/diesel-async",
"databases/mongodb", "databases/mongodb",
"databases/mysql", "databases/mysql",
"databases/postgres", "databases/postgres",

View File

@ -0,0 +1,17 @@
[package]
name = "db-diesel-async"
version = "1.0.0"
edition = "2021"
[features]
postgres_tests = []
[dependencies]
actix-web.workspace = true
diesel = { version = "2", default-features = false, features = ["uuid"] }
diesel-async = { version = "0.5", features = ["postgres", "bb8", "async-connection-wrapper"] }
serde.workspace = true
uuid.workspace = true
dotenvy.workspace = true
env_logger.workspace = true
log.workspace = true

View File

@ -0,0 +1,113 @@
# diesel
Basic integration of [Diesel-async](https://github.com/weiznich/diesel_async) using PostgreSQL for Actix Web.
## Usage
### Install PostgreSQL
```sh
# on any OS
docker run -d --restart unless-stopped --name postgresql -e POSTGRES_USER=test-user -e POSTGRES_PASSWORD=password -p 5432:5432 -v postgres_data:/var/lib/postgresql/data postgres:alpine
```
make sure it has successfully started up and is running
```sh
# on any OS
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}"
```
### Initialize PostgreSQL Database
```sh
cd databases/diesel-async
cargo install diesel_cli --no-default-features --features postgres
echo DATABASE_URL=postgres://test-user:password@localhost:5432/test_db > .env
diesel setup
diesel migration run
```
The database will now be created in your PostgreSQL instance.
```sh
docker exec -i postgresql psql -U test-user -c "\l"
```
### Running Server
```sh
cd databases/diesel-async
cargo run
# Started http server: 127.0.0.1:8080
```
### Available Routes
#### `POST /items`
Inserts a new item into the PostgreSQL DB.
Provide a JSON payload with a name. Eg:
```json
{ "name": "thingamajig" }
```
On success, a response like the following is returned:
```json
{
"id": "01948982-67d0-7a55-b4b1-8b8b962d8c6b",
"name": "thingamajig"
}
```
<details>
<summary>Client Examples</summary>
Using [HTTPie]:
```sh
http POST localhost:8080/items name=thingamajig
```
Using cURL:
```sh
curl -S -X POST --header "Content-Type: application/json" --data '{"name":"thingamajig"}' http://localhost:8080/items
```
</details>
#### `GET /items/{item_uid}`
Gets an item from the DB using its UID (returned from the insert request or taken from the DB directly). Returns a 404 when no item exists with that UID.
<details>
<summary>Client Examples</summary>
Using [HTTPie]:
```sh
http localhost:8080/items/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97
```
Using cURL:
```sh
curl -S http://localhost:8080/items/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97
```
</details>
### Explore The PostgreSQL DB
```sh
docker exec -i postgresql psql -U test-user -d test_db -c "select * from public.items"
```
## Using Other Databases
You can find a complete example of Diesel + PostgreSQL at: [https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/Rust/actix](https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/Rust/actix)
[httpie]: https://httpie.io/cli

View File

@ -0,0 +1,9 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
[migrations_directory]
dir = "/home/alex/CLionProjects/actix-with-async-diesel/migrations"

View File

View File

@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS
$$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS
$$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS items;

View File

@ -0,0 +1,6 @@
-- Your SQL goes here
CREATE TABLE IF NOT EXISTS items
(
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
name VARCHAR NOT NULL
);

View File

@ -0,0 +1,52 @@
use diesel::prelude::*;
use uuid::{NoContext, Timestamp, Uuid};
use crate::models;
use diesel_async::AsyncPgConnection;
use diesel_async::RunQueryDsl;
type DbError = Box<dyn std::error::Error + Send + Sync>;
// /// Run query using Diesel to find item by uid and return it.
pub async fn find_item_by_id(
conn: &mut AsyncPgConnection,
uid: Uuid,
) -> Result<Option<models::Item>, DbError> {
use super::schema::items::dsl::*;
let item = items
.filter(id.eq(uid))
.select(models::Item::as_select())
// execute the query via the provided async `diesel_async::RunQueryDsl`
.first::<models::Item>(conn)
.await
.optional()?;
Ok(item)
}
/// Run query using Diesel to insert a new database row and return the result.
pub async fn insert_new_item(
conn: &mut AsyncPgConnection,
nm: &str, // prevent collision with `name` column imported inside the function
) -> Result<models::Item, DbError> {
// It is common when using Diesel with Actix Web to import schema-related
// modules inside a function's scope (rather than the normal module's scope)
// to prevent import collisions and namespace pollution.
use crate::schema::items::dsl::*;
let new_item = models::Item {
id: Uuid::new_v7(Timestamp::now(NoContext)),
name: nm.to_owned(),
};
let item = diesel::insert_into(items)
.values(&new_item)
.returning(models::Item::as_returning())
.get_result(conn)
.await
.expect("Error inserting person");
Ok(item)
}

View File

@ -0,0 +1,183 @@
#[macro_use]
extern crate diesel;
use actix_web::{error, get, middleware, post, web, App, HttpResponse, HttpServer, Responder};
use diesel_async::{
pooled_connection::{bb8::Pool, AsyncDieselConnectionManager},
AsyncPgConnection,
};
use dotenvy::dotenv;
use std::{env, io};
use uuid::Uuid;
mod actions;
mod models;
mod schema;
type DbPool = Pool<AsyncPgConnection>;
/// Finds item by UID.
///
/// Extracts:
/// - the database pool handle from application data
/// - an item UID from the request path
#[get("/items/{item_id}")]
async fn get_item(
pool: web::Data<DbPool>,
item_id: web::Path<Uuid>,
) -> actix_web::Result<impl Responder> {
let item_id = item_id.into_inner();
let mut conn = pool
.get()
.await
.expect("Couldn't get db connection from the pool");
let item = actions::find_item_by_id(&mut conn, item_id)
.await
// map diesel query errors to a 500 error response
.map_err(error::ErrorInternalServerError)?;
Ok(match item {
// item was found; return 200 response with JSON formatted item object
Some(item) => HttpResponse::Ok().json(item),
// item was not found; return 404 response with error message
None => HttpResponse::NotFound().body(format!("No item found with UID: {item_id}")),
})
}
/// Creates new item.
///
/// Extracts:
/// - the database pool handle from application data
/// - a JSON form containing new item info from the request body
#[post("/items")]
async fn add_item(
pool: web::Data<DbPool>,
form: web::Json<models::NewItem>,
) -> actix_web::Result<impl Responder> {
let mut conn = pool
.get()
.await
.expect("Couldn't get db connection from the pool");
let item = actions::insert_new_item(&mut conn, &form.name)
.await
// map diesel query errors to a 500 error response
.map_err(error::ErrorInternalServerError)?;
// item was added successfully; return 201 response with new item info
Ok(HttpResponse::Created().json(item))
}
#[actix_web::main]
async fn main() -> io::Result<()> {
dotenv().ok();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
// initialize DB pool outside `HttpServer::new` so that it is shared across all workers
let pool = initialize_db_pool().await;
log::info!("starting HTTP server at http://localhost:8080");
HttpServer::new(move || {
App::new()
// add DB pool handle to app data; enables use of `web::Data<DbPool>` extractor
.app_data(web::Data::new(pool.clone()))
// add request logger middleware
.wrap(middleware::Logger::default())
// add route handlers
.service(add_item)
.service(get_item)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
/// Initialize database connection pool based on `DATABASE_URL` environment variable.
///
/// See more: <https://docs.rs/diesel-async/latest/diesel_async/pooled_connection/index.html#modules>.
async fn initialize_db_pool() -> DbPool {
let db_url = env::var("DATABASE_URL").expect("Env var `DATABASE_URL` not set");
let connection_manager = AsyncDieselConnectionManager::<AsyncPgConnection>::new(db_url);
Pool::builder().build(connection_manager).await.unwrap()
}
#[cfg(not(feature = "postgres_tests"))]
#[allow(unused_imports)]
mod tests {
use actix_web::{http::StatusCode, test};
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use super::*;
#[actix_web::test]
async fn item_routes() {
dotenv().ok();
env_logger::try_init_from_env(env_logger::Env::new().default_filter_or("info")).ok();
let pool = initialize_db_pool().await;
let app = test::init_service(
App::new()
.app_data(web::Data::new(pool.clone()))
.wrap(middleware::Logger::default())
.service(get_item)
.service(add_item),
)
.await;
// send something that isn't a UUID to `get_item`
let req = test::TestRequest::get().uri("/items/123").to_request();
let res = test::call_service(&app, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let body = test::read_body(res).await;
assert!(
body.starts_with(b"UUID parsing failed"),
"unexpected body: {body:?}",
);
// try to find a non-existent item
let req = test::TestRequest::get()
.uri(&format!("/items/{}", Uuid::nil()))
.to_request();
let res = test::call_service(&app, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let body = test::read_body(res).await;
assert!(
body.starts_with(b"No item found"),
"unexpected body: {body:?}",
);
// create new item
let req = test::TestRequest::post()
.uri("/items")
.set_json(models::NewItem::new("Test item"))
.to_request();
let res: models::Item = test::call_and_read_body_json(&app, req).await;
assert_eq!(res.name, "Test item");
// get an item
let req = test::TestRequest::get()
.uri(&format!("/items/{}", res.id))
.to_request();
let res: models::Item = test::call_and_read_body_json(&app, req).await;
assert_eq!(res.name, "Test item");
// delete new item from table
use crate::schema::items::dsl::*;
diesel::delete(items.filter(id.eq(res.id)))
.execute(
&mut pool
.get()
.await
.expect("couldn't get db connection from pool"),
)
.await
.expect("couldn't delete test item from table");
}
}

View File

@ -0,0 +1,26 @@
use super::schema::items;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Item details.
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Selectable, Insertable)]
#[diesel(table_name = items)]
pub struct Item {
pub id: Uuid,
pub name: String,
}
/// New item details.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NewItem {
pub name: String,
}
#[cfg(not(feature = "postgres_tests"))]
impl NewItem {
/// Constructs new item details from name.
#[cfg(test)] // only needed in tests
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
}

View File

@ -0,0 +1,8 @@
// @generated automatically by Diesel CLI.
diesel::table! {
items (id) {
id -> Uuid,
name -> Varchar,
}
}