From bfede4c1bba886afbed528160eddb8d04f8cf856 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 7 Oct 2021 03:00:13 +0100 Subject: [PATCH] improve sqlx example inspired by #455 --- .gitignore | 3 + Cargo.lock | 12 -- database_interactions/sqlx_todo/.env.example | 4 +- database_interactions/sqlx_todo/Cargo.toml | 1 - database_interactions/sqlx_todo/README.md | 13 +- database_interactions/sqlx_todo/src/main.rs | 47 ++--- .../sqlx_todo/src/todo/model.rs | 174 +++++++++++------- .../sqlx_todo/src/todo/routes.rs | 73 +++++--- 8 files changed, 198 insertions(+), 129 deletions(-) diff --git a/.gitignore b/.gitignore index 54d7e28..5538bde 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ # For multipart example upload.png + +# any dotenv files +.env diff --git a/Cargo.lock b/Cargo.lock index 8875413..9125591 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3623,17 +3623,6 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" -[[package]] -name = "listenfd" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e514e2cb8a9624701346ea3e694c1766d76778e343e537d873c1c366e79a7" -dependencies = [ - "libc", - "uuid", - "winapi 0.3.9", -] - [[package]] name = "local-channel" version = "0.1.2" @@ -5780,7 +5769,6 @@ dependencies = [ "dotenv", "env_logger 0.7.1", "futures", - "listenfd", "log", "serde 1.0.130", "serde_json", diff --git a/database_interactions/sqlx_todo/.env.example b/database_interactions/sqlx_todo/.env.example index 0715908..442b38c 100644 --- a/database_interactions/sqlx_todo/.env.example +++ b/database_interactions/sqlx_todo/.env.example @@ -1,4 +1,4 @@ HOST=127.0.0.1 -PORT=5000 -DATABASE_URL=sqlite://database_interactions/sqlx_todo/test.db +PORT=8080 +DATABASE_URL=sqlite://${CARGO_MANIFEST_DIR}/test.db RUST_LOG=sqlx_todo=info,actix=info diff --git a/database_interactions/sqlx_todo/Cargo.toml b/database_interactions/sqlx_todo/Cargo.toml index d5baefb..bee058a 100644 --- a/database_interactions/sqlx_todo/Cargo.toml +++ b/database_interactions/sqlx_todo/Cargo.toml @@ -8,7 +8,6 @@ edition = "2018" [dependencies] actix-web = "3" -listenfd = "0.3.3" serde = "1.0.106" serde_json = "1.0.51" sqlx = { version = "0.3", features = ["sqlite"] } diff --git a/database_interactions/sqlx_todo/README.md b/database_interactions/sqlx_todo/README.md index 4210931..3b8d21e 100644 --- a/database_interactions/sqlx_todo/README.md +++ b/database_interactions/sqlx_todo/README.md @@ -11,16 +11,21 @@ Example Todo application using Actix-web and [SQLx](https://github.com/launchbad ## Change into the project sub-directory -All instructions assume you have changed into this folder: +All instructions assume you have changed into this directory: ```bash -cd examples/sqlx_todo +$ cd database_interactions/sqlx_todo ``` ## Set up the database * Create new database using `schema.sql` -* Copy `.env-example` into `.env` and adjust DATABASE_URL to match your SQLite address, username and password +* Copy `.env.example` into `.env` and adjust `DATABASE_URL` to match your SQLite address, if needed + +```sh +cat schema.sql | sqlite3 test.db +cp .env.example .env +``` ## Run the application @@ -30,4 +35,4 @@ To run the application execute: cargo run ``` -By default application will be available on `http://localhost:5000`. If you wish to change address or port you can do it inside `.env` file +By default application will be available on `http://localhost:8080`. If you wish to change address or port you can do it inside the `.env` file diff --git a/database_interactions/sqlx_todo/src/main.rs b/database_interactions/sqlx_todo/src/main.rs index 845e3d7..3e3fcad 100644 --- a/database_interactions/sqlx_todo/src/main.rs +++ b/database_interactions/sqlx_todo/src/main.rs @@ -1,19 +1,20 @@ #[macro_use] extern crate log; -use actix_web::{web, App, HttpResponse, HttpServer, Responder}; +use std::env; + +use actix_web::{middleware, web, App, HttpResponse, HttpServer, Responder}; use anyhow::Result; use dotenv::dotenv; -use listenfd::ListenFd; use sqlx::SqlitePool; -use std::env; // import todo module (routes and model) mod todo; -// default / handler +// root (/) handler async fn index() -> impl Responder { - HttpResponse::Ok().body(r#" + HttpResponse::Ok().body( + r#" Welcome to Actix-web with SQLx Todos example. Available routes: GET /todos -> list of all todos @@ -21,7 +22,7 @@ async fn index() -> impl Responder { GET /todo/{id} -> show one todo with requested id PUT /todo/{id} -> update todo with requested id, example: { "description": "learn actix and sqlx", "done": true } DELETE /todo/{id} -> delete todo with requested id - "# + "# ) } @@ -30,28 +31,32 @@ async fn main() -> Result<()> { dotenv().ok(); env_logger::init(); - // this will enable us to keep application running during recompile: systemfd --no-pid -s http::5000 -- cargo watch -x run - let mut listenfd = ListenFd::from_env(); - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); + let host = env::var("HOST").expect("HOST is not set in .env file"); + let port = env::var("PORT") + .expect("PORT is not set in .env file") + .parse::() + .expect("PORT should be a u16"); + + info!("using sqlite database at: {}", &database_url); let db_pool = SqlitePool::new(&database_url).await?; - let mut server = HttpServer::new(move || { + // startup connection+schema check + sqlx::query!("SELECT * FROM todos") + .fetch_one(&db_pool) + .await + .expect("no connection to database"); + + let server = HttpServer::new(move || { App::new() - .data(db_pool.clone()) // pass database pool to application so we can access it inside handlers + // pass database pool to application so we can access it inside handlers + .data(db_pool.clone()) + .wrap(middleware::Logger::default()) .route("/", web::get().to(index)) .configure(todo::init) // init todo routes - }); - - server = match listenfd.take_tcp_listener(0)? { - Some(listener) => server.listen(listener)?, - None => { - let host = env::var("HOST").expect("HOST is not set in .env file"); - let port = env::var("PORT").expect("PORT is not set in .env file"); - server.bind(format!("{}:{}", host, port))? - } - }; + }) + .bind((host, port))?; info!("Starting server"); server.run().await?; diff --git a/database_interactions/sqlx_todo/src/todo/model.rs b/database_interactions/sqlx_todo/src/todo/model.rs index 1cf24f1..a0796b2 100644 --- a/database_interactions/sqlx_todo/src/todo/model.rs +++ b/database_interactions/sqlx_todo/src/todo/model.rs @@ -1,6 +1,5 @@ use actix_web::{Error, HttpRequest, HttpResponse, Responder}; use anyhow::Result; -use futures::future::{ready, Ready}; use serde::{Deserialize, Serialize}; use sqlx::sqlite::SqliteRow; use sqlx::{FromRow, Row, SqlitePool}; @@ -23,52 +22,89 @@ pub struct Todo { // implementation of Actix Responder for Todo struct so we can return Todo from action handler impl Responder for Todo { type Error = Error; - type Future = Ready>; + type Future = HttpResponse; fn respond_to(self, _req: &HttpRequest) -> Self::Future { - let body = serde_json::to_string(&self).unwrap(); // create response and set content type - ready(Ok(HttpResponse::Ok() - .content_type("application/json") - .body(body))) + HttpResponse::Ok().json(&self) } } // Implementation for Todo struct, functions for read/write/update and delete todo from database impl Todo { pub async fn find_all(pool: &SqlitePool) -> Result> { - let mut todos = vec![]; - let recs = sqlx::query!( + let todos = sqlx::query!( r#" - SELECT id, description, done - FROM todos - ORDER BY id + SELECT id, description, done + FROM todos + ORDER BY id "# ) .fetch_all(pool) - .await?; - - for rec in recs { - todos.push(Todo { - id: rec.id, - description: rec.description, - done: rec.done, - }); - } + .await? + .into_iter() + .map(|rec| Todo { + id: rec.id, + description: rec.description, + done: rec.done, + }) + .collect(); Ok(todos) } - pub async fn find_by_id(id: i32, pool: &SqlitePool) -> Result { + pub async fn find_by_id(id: i32, pool: &SqlitePool) -> Result> { let rec = sqlx::query!( r#" - SELECT * FROM todos WHERE id = $1 - "#, + SELECT id, description, done + FROM todos + WHERE id = $1 + "#, id ) - .fetch_one(&*pool) + .fetch_optional(&*pool) .await?; + Ok(rec.map(|rec| Todo { + id: rec.id, + description: rec.description, + done: rec.done, + })) + } + + pub async fn create(todo: TodoRequest, pool: &SqlitePool) -> Result { + let mut tx = pool.begin().await?; + + sqlx::query!( + r#" + INSERT INTO todos (description, done) + VALUES ($1, $2) + "#, + todo.description, + todo.done, + ) + .execute(&mut tx) + .await?; + + // TODO: this can be replaced with RETURNING with sqlite v3.35+ and/or sqlx v0.5+ + let row_id: i32 = sqlx::query("SELECT last_insert_rowid()") + .map(|row: SqliteRow| row.get(0)) + .fetch_one(&mut tx) + .await?; + + let rec = sqlx::query!( + r#" + SELECT id, description, done + FROM todos + WHERE id = $1 + "#, + row_id, + ) + .fetch_one(&mut tx) + .await?; + + tx.commit().await?; + Ok(Todo { id: rec.id, description: rec.description, @@ -76,53 +112,65 @@ impl Todo { }) } - pub async fn create(todo: TodoRequest, pool: &SqlitePool) -> Result { - let mut tx = pool.begin().await?; - let todo = sqlx::query("INSERT INTO todos (description, done) VALUES ($1, $2) RETURNING id, description, done") - .bind(&todo.description) - .bind(todo.done) - .map(|row: SqliteRow| { - Todo { - id: row.get(0), - description: row.get(1), - done: row.get(2) - } - }) - .fetch_one(&mut tx) - .await?; - - tx.commit().await?; - Ok(todo) - } - - pub async fn update(id: i32, todo: TodoRequest, pool: &SqlitePool) -> Result { + pub async fn update( + id: i32, + todo: TodoRequest, + pool: &SqlitePool, + ) -> Result> { let mut tx = pool.begin().await.unwrap(); - let todo = sqlx::query("UPDATE todos SET description = $1, done = $2 WHERE id = $3 RETURNING id, description, done") - .bind(&todo.description) - .bind(todo.done) - .bind(id) - .map(|row: SqliteRow| { - Todo { - id: row.get(0), - description: row.get(1), - done: row.get(2) - } - }) - .fetch_one(&mut tx) - .await?; + + let n = sqlx::query!( + r#" + UPDATE todos + SET description = $1, done = $2 + WHERE id = $3 + "#, + todo.description, + todo.done, + id, + ) + .execute(&mut tx) + .await?; + + if n == 0 { + return Ok(None); + } + + // TODO: this can be replaced with RETURNING with sqlite v3.35+ and/or sqlx v0.5+ + let todo = sqlx::query!( + r#" + SELECT id, description, done + FROM todos + WHERE id = $1 + "#, + id, + ) + .fetch_one(&mut tx) + .await + .map(|rec| Todo { + id: rec.id, + description: rec.description, + done: rec.done, + })?; tx.commit().await.unwrap(); - Ok(todo) + Ok(Some(todo)) } pub async fn delete(id: i32, pool: &SqlitePool) -> Result { let mut tx = pool.begin().await?; - let deleted = sqlx::query("DELETE FROM todos WHERE id = $1") - .bind(id) - .execute(&mut tx) - .await?; + + let n_deleted = sqlx::query!( + r#" + DELETE FROM todos + WHERE id = $1 + "#, + id, + ) + .execute(&mut tx) + .await?; tx.commit().await?; - Ok(deleted) + Ok(n_deleted) } } diff --git a/database_interactions/sqlx_todo/src/todo/routes.rs b/database_interactions/sqlx_todo/src/todo/routes.rs index a857de0..be0eab2 100644 --- a/database_interactions/sqlx_todo/src/todo/routes.rs +++ b/database_interactions/sqlx_todo/src/todo/routes.rs @@ -1,23 +1,42 @@ -use crate::todo::{Todo, TodoRequest}; use actix_web::{delete, get, post, put, web, HttpResponse, Responder}; use sqlx::SqlitePool; +use crate::todo::{Todo, TodoRequest}; + +// function that will be called on new Application to configure routes for this module +pub fn init(cfg: &mut web::ServiceConfig) { + cfg.service(find_all) + .service(find) + .service(create) + .service(update) + .service(delete); +} + #[get("/todos")] async fn find_all(db_pool: web::Data) -> impl Responder { let result = Todo::find_all(db_pool.get_ref()).await; match result { Ok(todos) => HttpResponse::Ok().json(todos), - _ => HttpResponse::BadRequest() - .body("Error trying to read all todos from database"), + Err(err) => { + error!("error fetching todos: {}", err); + HttpResponse::InternalServerError() + .body("Error trying to read all todos from database") + } } } #[get("/todo/{id}")] async fn find(id: web::Path, db_pool: web::Data) -> impl Responder { let result = Todo::find_by_id(id.into_inner(), db_pool.get_ref()).await; + match result { - Ok(todo) => HttpResponse::Ok().json(todo), - _ => HttpResponse::BadRequest().body("Todo not found"), + Ok(Some(todo)) => HttpResponse::Ok().json(todo), + Ok(None) => HttpResponse::NotFound().body("Todo not found"), + Err(err) => { + error!("error fetching todo: {}", err); + HttpResponse::InternalServerError() + .body("Error trying to read todo from database") + } } } @@ -29,7 +48,10 @@ async fn create( let result = Todo::create(todo.into_inner(), db_pool.get_ref()).await; match result { Ok(todo) => HttpResponse::Ok().json(todo), - _ => HttpResponse::BadRequest().body("Error trying to create new todo"), + Err(err) => { + error!("error creating todo: {}", err); + HttpResponse::InternalServerError().body("Error trying to create new todo") + } } } @@ -39,35 +61,34 @@ async fn update( todo: web::Json, db_pool: web::Data, ) -> impl Responder { - let result = - Todo::update(id.into_inner(), todo.into_inner(), db_pool.get_ref()).await; + let result = Todo::update(*id, todo.into_inner(), &db_pool).await; + match result { - Ok(todo) => HttpResponse::Ok().json(todo), - _ => HttpResponse::BadRequest().body("Todo not found"), + Ok(Some(todo)) => HttpResponse::Ok().json(todo), + Ok(None) => HttpResponse::NotFound().body("Todo not found"), + Err(err) => { + error!("error updating todo: {}", err); + HttpResponse::InternalServerError().body("Error trying to update todo") + } } } #[delete("/todo/{id}")] async fn delete(id: web::Path, db_pool: web::Data) -> impl Responder { - let result = Todo::delete(id.into_inner(), db_pool.get_ref()).await; + let result = Todo::delete(*id, db_pool.get_ref()).await; + match result { - Ok(rows) => { - if rows > 0 { - HttpResponse::Ok() - .body(format!("Successfully deleted {} record(s)", rows)) + Ok(rows_deleted) => { + if rows_deleted > 0 { + let msg = format!("Successfully deleted {} record(s)", rows_deleted); + HttpResponse::Ok().body(msg) } else { - HttpResponse::BadRequest().body("Todo not found") + HttpResponse::NotFound().body("Todo not found") } } - _ => HttpResponse::BadRequest().body("Todo not found"), + Err(err) => { + error!("error deleting todo: {}", err); + HttpResponse::InternalServerError().body("Todo not found") + } } } - -// function that will be called on new Application to configure routes for this module -pub fn init(cfg: &mut web::ServiceConfig) { - cfg.service(find_all); - cfg.service(find); - cfg.service(create); - cfg.service(update); - cfg.service(delete); -}