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

feat: diesel-async usage example

This commit is contained in:
Alex Ted 2025-01-21 20:27:58 +03:00
parent df0dc549a5
commit 631c5f9472
13 changed files with 360 additions and 0 deletions

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,12 @@
[package]
name = "db-diesel-async"
version = "1.0.0"
edition = "2021"
[dependencies]
actix-web.workspace = true
diesel = { version = "*", default-features = false, features = ["uuid"] }
diesel-async = { version = "*", features = ["postgres", "bb8", "async-connection-wrapper"] }
serde.workspace = true
uuid.workspace = true
dotenvy.workspace = true

View File

@ -0,0 +1,108 @@
# 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
```
### 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 /item`
Inserts a new item into the PostgreSQL DB.
Provide a JSON payload with a name. Eg:
```json
{ "name": "bill" }
```
On success, a response like the following is returned:
```json
{
"id": "01948982-67d0-7a55-b4b1-8b8b962d8c6b",
"name": "bill"
}
```
<details>
<summary>Client Examples</summary>
Using [HTTPie]:
```sh
http POST localhost:8080/item name=bill
```
Using cURL:
```sh
curl -S -X POST --header "Content-Type: application/json" --data '{"name":"bill"}' http://localhost:8080/item
```
</details>
#### `GET /item/{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/item/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97
```
Using cURL:
```sh
curl -S http://localhost:8080/item/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,53 @@
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_uid(
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,94 @@
#[macro_use]
extern crate diesel;
use std::env::VarError;
use actix_web::{error, get, post, web, App, HttpResponse, HttpServer, Responder};
use diesel_async::pooled_connection::{bb8::Pool, AsyncDieselConnectionManager, PoolError};
use diesel_async::AsyncPgConnection;
use dotenvy::dotenv;
use std::{env, io};
use thiserror::Error as ThisError;
use uuid::Uuid;
pub mod actions;
pub mod models;
pub 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_uid: web::Path<Uuid>,
) -> actix_web::Result<impl Responder> {
let item_uid = item_uid.into_inner();
let mut conn = pool
.get()
.await
.expect("Couldn't get db connection from the pool");
let item = actions::find_item_by_uid(&mut conn, item_uid)
.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_uid}")),
})
}
/// 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();
let db_url = env::var("DATABASE_URL").expect("Env var `DATABASE_URL` not set");
let mgr = AsyncDieselConnectionManager::<AsyncPgConnection>::new(db_url);
let pool = Pool::builder().build(mgr).await.unwrap();
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(pool.clone()))
.service(add_item)
.service(get_item)
})
.bind(("127.0.0.1", 5000))?
.run()
.await
}

View File

@ -0,0 +1,25 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::schema::items;
/// 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,
}
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(), ..Default::default() }
}
}

View File

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