1
0
mirror of https://github.com/actix/examples synced 2024-11-23 22:41:07 +01:00

Updated basics/to-do to v4 using sqlx.

This commit is contained in:
Chris Gubbin 2022-02-05 17:34:43 +00:00
parent ae9a9a0382
commit 2aa8b15ce2
11 changed files with 110 additions and 132 deletions

View File

@ -4,11 +4,10 @@ version = "1.0.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
actix-web = "4.0.0-rc.1" actix-web = "4.0.0-rc.2"
actix-files = "0.6.0-beta.15" actix-files = "0.6.0-beta.16"
actix-session = "0.5.0-beta.7" actix-session = "0.5.0-beta.7"
diesel = { version = "1.3.2", features = ["postgres", "r2d2"] }
dotenv = "0.15" dotenv = "0.15"
env_logger = "0.9" env_logger = "0.9"
futures = "0.3.7" futures = "0.3.7"
@ -16,3 +15,10 @@ log = "0.4"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tera = "1.5" tera = "1.5"
[dependencies.sqlx]
features = [
"runtime-actix-rustls",
"postgres"
]
version = "0.5.10"

View File

@ -19,22 +19,23 @@ cd basics/todo
## Set up the database ## Set up the database
Install the [diesel](http://diesel.rs) command-line tool including the `postgres` feature: Install the [sqlx](https://github.com/launchbadge/sqlx) command-line tool including the `postgres` feature:
```bash ```bash
cargo install diesel_cli --no-default-features --features postgres cargo install sqlx-cli --no-default-features --features postgres
``` ```
Check the contents of the `.env` file. If your database requires a password, update `DATABASE_URL` to be of the form: Check the contents of the `.env` file. If your database requires a password, update `DATABASE_URL` to be of the form:
```.env ```.env
DATABASE_URL=postgres://username:password@localhost/actix_todo DATABASE_URL=postgres://username:password@localhost:5432/actix_todo
``` ```
Then to create and set-up the database run: Then to create and set-up the database run:
```bash ```bash
diesel database setup sqlx database create
sqlx migrate run
``` ```
## Run the application ## Run the application
@ -45,4 +46,4 @@ To run the application execute:
cargo run cargo run
``` ```
Then to view it in your browser navigate to: [http://localhost:8080/](http://localhost:8080/) Then to view it in your browser navigate to: [http://localhost:8088/](http://localhost:8088/)

View File

@ -1,6 +0,0 @@
-- 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

@ -1,36 +0,0 @@
-- 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

@ -1,9 +1,9 @@
use actix_files::NamedFile; use actix_files::NamedFile;
use actix_session::Session; use actix_session::Session;
use actix_web::{ use actix_web::middleware::ErrorHandlerResponse;
dev, error, http, middleware::ErrorHandlerResponse, web, Error, HttpResponse, Result, use actix_web::{dev, error, http, web, Error, HttpResponse, Result};
};
use serde::Deserialize; use serde::Deserialize;
use sqlx::postgres::PgPool;
use tera::{Context, Tera}; use tera::{Context, Tera};
use crate::{ use crate::{
@ -12,11 +12,13 @@ use crate::{
}; };
pub async fn index( pub async fn index(
pool: web::Data<db::PgPool>, pool: web::Data<PgPool>,
tmpl: web::Data<Tera>, tmpl: web::Data<Tera>,
session: Session, session: Session,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let tasks = web::block(move || db::get_all_tasks(&pool)).await?; let tasks = db::get_all_tasks(&pool)
.await
.map_err(error::ErrorInternalServerError)?;
let mut context = Context::new(); let mut context = Context::new();
context.insert("tasks", &tasks); context.insert("tasks", &tasks);
@ -42,7 +44,7 @@ pub struct CreateForm {
pub async fn create( pub async fn create(
params: web::Form<CreateForm>, params: web::Form<CreateForm>,
pool: web::Data<db::PgPool>, pool: web::Data<PgPool>,
session: Session, session: Session,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
if params.description.is_empty() { if params.description.is_empty() {
@ -52,8 +54,8 @@ pub async fn create(
)?; )?;
Ok(redirect_to("/")) Ok(redirect_to("/"))
} else { } else {
web::block(move || db::create_task(params.into_inner().description, &pool)) db::create_task(params.into_inner().description, &pool)
.await? .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
session::set_flash(&session, FlashMessage::success("Task successfully added"))?; session::set_flash(&session, FlashMessage::success("Task successfully added"))?;
Ok(redirect_to("/")) Ok(redirect_to("/"))
@ -71,7 +73,7 @@ pub struct UpdateForm {
} }
pub async fn update( pub async fn update(
db: web::Data<db::PgPool>, db: web::Data<PgPool>,
params: web::Path<UpdateParams>, params: web::Path<UpdateParams>,
form: web::Form<UpdateForm>, form: web::Form<UpdateForm>,
session: Session, session: Session,
@ -87,22 +89,22 @@ pub async fn update(
} }
async fn toggle( async fn toggle(
pool: web::Data<db::PgPool>, pool: web::Data<PgPool>,
params: web::Path<UpdateParams>, params: web::Path<UpdateParams>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
web::block(move || db::toggle_task(params.id, &pool)) db::toggle_task(params.id, &pool)
.await? .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
Ok(redirect_to("/")) Ok(redirect_to("/"))
} }
async fn delete( async fn delete(
pool: web::Data<db::PgPool>, pool: web::Data<PgPool>,
params: web::Path<UpdateParams>, params: web::Path<UpdateParams>,
session: Session, session: Session,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
web::block(move || db::delete_task(params.id, &pool)) db::delete_task(params.id, &pool)
.await? .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
session::set_flash(&session, FlashMessage::success("Task was deleted."))?; session::set_flash(&session, FlashMessage::success("Task was deleted."))?;
Ok(redirect_to("/")) Ok(redirect_to("/"))

View File

@ -1,43 +1,36 @@
use std::ops::Deref as _; use sqlx::postgres::{PgPool, PgPoolOptions};
use diesel::{
pg::PgConnection,
r2d2::{ConnectionManager, Pool, PoolError, PooledConnection},
};
use crate::model::{NewTask, Task}; use crate::model::{NewTask, Task};
pub type PgPool = Pool<ConnectionManager<PgConnection>>; pub async fn init_pool(database_url: &str) -> Result<PgPool, sqlx::Error> {
type PgPooledConnection = PooledConnection<ConnectionManager<PgConnection>>; PgPoolOptions::new()
.connect_timeout(std::time::Duration::from_secs(2))
pub fn init_pool(database_url: &str) -> Result<PgPool, PoolError> { .connect(database_url)
let manager = ConnectionManager::<PgConnection>::new(database_url); .await
Pool::builder().build(manager)
} }
fn get_conn(pool: &PgPool) -> Result<PgPooledConnection, &'static str> { pub async fn get_all_tasks(pool: &PgPool) -> Result<Vec<Task>, &'static str> {
pool.get().map_err(|_| "Can't get connection") Task::all(pool).await.map_err(|_| "Error retrieving tasks")
} }
pub fn get_all_tasks(pool: &PgPool) -> Result<Vec<Task>, &'static str> { pub async fn create_task(todo: String, pool: &PgPool) -> Result<(), &'static str> {
Task::all(get_conn(pool)?.deref()).map_err(|_| "Error retrieving tasks")
}
pub fn create_task(todo: String, pool: &PgPool) -> Result<(), &'static str> {
let new_task = NewTask { description: todo }; let new_task = NewTask { description: todo };
Task::insert(new_task, get_conn(pool)?.deref()) Task::insert(new_task, pool)
.await
.map(|_| ()) .map(|_| ())
.map_err(|_| "Error inserting task") .map_err(|_| "Error inserting task")
} }
pub fn toggle_task(id: i32, pool: &PgPool) -> Result<(), &'static str> { pub async fn toggle_task(id: i32, pool: &PgPool) -> Result<(), &'static str> {
Task::toggle_with_id(id, get_conn(pool)?.deref()) Task::toggle_with_id(id, pool)
.await
.map(|_| ()) .map(|_| ())
.map_err(|_| "Error toggling task completion") .map_err(|_| "Error toggling task completion")
} }
pub fn delete_task(id: i32, pool: &PgPool) -> Result<(), &'static str> { pub async fn delete_task(id: i32, pool: &PgPool) -> Result<(), &'static str> {
Task::delete_with_id(id, get_conn(pool)?.deref()) Task::delete_with_id(id, pool)
.await
.map(|_| ()) .map(|_| ())
.map_err(|_| "Error deleting task") .map_err(|_| "Error deleting task")
} }

View File

@ -1,6 +1,3 @@
#[macro_use]
extern crate diesel;
use std::{env, io}; use std::{env, io};
use actix_files::Files; use actix_files::Files;
@ -16,7 +13,6 @@ use tera::Tera;
mod api; mod api;
mod db; mod db;
mod model; mod model;
mod schema;
mod session; mod session;
static SESSION_SIGNING_KEY: &[u8] = &[0; 32]; static SESSION_SIGNING_KEY: &[u8] = &[0; 32];
@ -28,9 +24,12 @@ async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let pool = db::init_pool(&database_url).expect("Failed to create pool"); println!("{}", database_url);
let pool = db::init_pool(&database_url)
.await
.expect("Failed to create pool");
log::info!("starting HTTP serer at http://localhost:8080"); log::info!("starting HTTP serer at http://localhost:8088");
HttpServer::new(move || { HttpServer::new(move || {
log::debug!("Constructing the App"); log::debug!("Constructing the App");
@ -50,8 +49,8 @@ async fn main() -> io::Result<()> {
.handler(http::StatusCode::NOT_FOUND, api::not_found); .handler(http::StatusCode::NOT_FOUND, api::not_found);
App::new() App::new()
.app_data(templates) .app_data(web::Data::new(templates))
.app_data(pool.clone()) .app_data(web::Data::new(pool.clone()))
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(session_store) .wrap(session_store)
.wrap(error_handlers) .wrap(error_handlers)

View File

@ -1,19 +1,11 @@
use diesel::pg::PgConnection; use sqlx::PgPool;
use diesel::prelude::*;
use serde::Serialize;
use crate::schema::{ #[derive(Debug)]
tasks,
tasks::dsl::{completed as task_completed, tasks as all_tasks},
};
#[derive(Debug, Insertable)]
#[table_name = "tasks"]
pub struct NewTask { pub struct NewTask {
pub description: String, pub description: String,
} }
#[derive(Debug, Queryable, Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Task { pub struct Task {
pub id: i32, pub id: i32,
pub description: String, pub description: String,
@ -21,27 +13,62 @@ pub struct Task {
} }
impl Task { impl Task {
pub fn all(conn: &PgConnection) -> QueryResult<Vec<Task>> { pub async fn all(connection: &PgPool) -> Result<Vec<Task>, sqlx::Error> {
all_tasks.order(tasks::id.desc()).load::<Task>(conn) let tasks = sqlx::query_as!(
Task,
r#"
SELECT *
FROM tasks
"#
)
.fetch_all(connection)
.await?;
Ok(tasks)
} }
pub fn insert(todo: NewTask, conn: &PgConnection) -> QueryResult<usize> { pub async fn insert(todo: NewTask, connection: &PgPool) -> Result<(), sqlx::Error> {
diesel::insert_into(tasks::table) sqlx::query!(
.values(&todo) r#"
.execute(conn) INSERT INTO tasks (description)
VALUES ($1)
"#,
todo.description,
)
.execute(connection)
.await?;
Ok(())
} }
pub fn toggle_with_id(id: i32, conn: &PgConnection) -> QueryResult<usize> { pub async fn toggle_with_id(
let task = all_tasks.find(id).get_result::<Task>(conn)?; id: i32,
connection: &PgPool,
let new_status = !task.completed; ) -> Result<(), sqlx::Error> {
let updated_task = diesel::update(all_tasks.find(id)); sqlx::query!(
updated_task r#"
.set(task_completed.eq(new_status)) UPDATE tasks
.execute(conn) SET completed = NOT completed
WHERE id = $1
"#,
id
)
.execute(connection)
.await?;
Ok(())
} }
pub fn delete_with_id(id: i32, conn: &PgConnection) -> QueryResult<usize> { pub async fn delete_with_id(
diesel::delete(all_tasks.find(id)).execute(conn) id: i32,
connection: &PgPool,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
DELETE FROM tasks
WHERE id = $1
"#,
id
)
.execute(connection)
.await?;
Ok(())
} }
} }

View File

@ -1,7 +0,0 @@
table! {
tasks (id) {
id -> Int4,
description -> Varchar,
completed -> Bool,
}
}