Actix
actix 是 Rust 生态中的 Actor 系统。actix-web 是在 actix actor 框架和 Tokio 异步 IO 系统之上构建的高级 Web 框架。
本篇博客实践使用 actix-web 实现一个简单的 todo 应用。基本要求:了解 rust 基本语法,了解一定的 sql 和 docker 知识。
创建一个 Hello world 程序
首先,新建一个 todo-list
项目,并在其中增加 actix-web
依赖,我们使用最新的 actix 3.0。
cargo new todo-list cd todo-list
Cargo.toml
:
[package] name = "todo-list" version = "0.1.0" authors = ["qiwihui <qwh005007@gmail.com>" ]edition = "2018" [dependencies] actix-web = "3"
在 main.rs
中,使用类似于 python flask 的语法,增加一个最简单的 service。
use actix_web::{get, App, HttpServer, Responder};use std::io::Result ;#[get("/" )] async fn hello () -> impl Responder { format! ("Hello world!" ) } #[actix_web::main] async fn main () -> Result <()> { println! ("Starting server at http://127.0.0.1:8000" ); HttpServer::new(|| App::new().service(hello)) .bind("127.0.0.1:8000" )? .run() .await }
运行并测试:
在另一个终端中
$ curl 127.0.0.1:8000Hello world!
数据库设计
项目中将使用 postgres 作为数据库存储,为了方便操作和管理,我们使用 docker-compose 进行管理。
docker-compose.yml
version: "3" services: postgres: image: postgres:11-alpine container_name: postgres restart: always environment: POSTGRES_PASSWORD: actix POSTGRES_USER: actix POSTGRES_DB: actix ports: - 5432 :5432
创建数据库:
然后,我们设计整体数据库表结构,并创建一些基础数据作为测试。表结构如下:
TodoList TodoItem +---------+ | id | +-------+ +---------+ | id + <-- FK --+ list_id | +-------+ +---------+ | title | | title | +-------+ +---------+ | checked | +---------+
在 database.sql
中手动创建表结构并插入数据:
drop table if exists todo_list;drop table if exists todo_item;create table todo_list ( id serial primary key , title varchar (150 ) not null ); create table todo_item ( id serial primary key , title varchar (150 ) not null , checked boolean not null default false , list_id integer not null , foreign key (list_id) references todo_list(id ) ); insert into todo_list (title) values ('List 1' ), ('List 2' ); insert into todo_item (title, list_id) values ('item 1' , 1 ), ('item 2' , 1 );
创建数据表并查看结果
$ psql -h 127.0.0.1 -p 5432 -U actix actix < database.sql Password for user actix: NOTICE: table "todo_list" does not exist, skipping DROP TABLE NOTICE: table "todo_item" does not exist, skipping DROP TABLE CREATE TABLE CREATE TABLE INSERT 0 2 INSERT 0 2
$ psql -h 127.0.0.1 -p 5432 -U actix actixPassword for user actix: psql (12.4, server 11.9) Type "help" for help. actix=# \d List of relations Schema | Name | Type | Owner --------+------------------+----------+------- public | todo_item | table | actix public | todo_item_id_seq | sequence | actix public | todo_list | table | actix public | todo_list_id_seq | sequence | actix (4 rows) actix=# select * from todo_list; id | title ----+-------- 1 | List 1 2 | List 2 (2 rows)
获取 todo 列表
首先,添加我们需要的库,其中 serde
用于序列化,tokio-postgres
是一直支持异步的 PostgreSQL 客户端,deadpool-postgres
用于连接池的管理。
[dependencies] actix-web = "3" serde ="1.0.117" deadpool-postgres = "0.5.0" tokio-postgres = "0.5.1"
增加 models.rs
用于管理数据模型,并支持序列化和反序列化。
use serde::{Deserialize, Serialize};#[derive(Serialize, Deserialize)] pub struct TodoList { pub id: i32 , pub title: String , } #[derive(Serialize, Deserialize)] pub struct TodoItem { pub id: i32 , pub title: String , pub checked: bool , pub list_id: i32 , }
增加 db.rs
用于管理数据操作,例如 get_todos
从数据库中获取数据并序列化为 TodoList
的数组:
use crate::models::{TodoItem, TodoList};use deadpool_postgres::Client;use std::io::Error;use tokio_postgres::Row;fn row_to_todo (row: &Row) -> TodoList { let id: i32 = row.get(0 ); let title: String = row.get(1 ); TodoList { id, title } } pub async fn get_todos (client: &Client) -> Result <Vec <TodoList>, Error> { let statement = client .prepare("select * from todo_list order by id desc" ) .await .unwrap(); let todos = client .query(&statement, &[]) .await .expect("Error getting todo lists" ) .iter() .map(|row| row_to_todo(row)) .collect::<Vec <TodoList>>(); Ok (todos) }
增加 handlers.rs
用于处理服务:
use crate::db::get_todos;use actix_web::{web, HttpResponse, Responder};use deadpool_postgres::{Client, Pool};pub async fn todos (db_pool: web::Data<Pool>) -> impl Responder { let client: Client = db_pool .get() .await .expect("Error connecting to the database" ); let result = get_todos(&client).await ; match result { Ok (todos) => HttpResponse::Ok ().json(todos), Err (_) => HttpResponse::InternalServerError().into(), } }
最后,在 main.rs
中创建连接池并添加路由:
mod db;mod handlers;mod models;use actix_web::{get, web, App, HttpServer, Responder};use deadpool_postgres;use handlers::todos;use std::io;use tokio_postgres::{self , NoTls};#[get("/" )] async fn hello () -> impl Responder { format! ("Hello world!" ) } #[actix_web::main] async fn main () -> io::Result <()> { println! ("Starting server at http://127.0.0.1:8000" ); let mut cfg = tokio_postgres::Config::new(); cfg.host("localhost" ); cfg.port(5432 ); cfg.user("actix" ); cfg.password("actix" ); cfg.dbname("actix" ); let mgr = deadpool_postgres::Manager::new(cfg, NoTls); let pool = deadpool_postgres::Pool::new(mgr, 100 ); HttpServer::new(move || { App::new() .data(pool.clone()) .service(hello) .route("/todos{_:/?}" , web::get().to(todos)) }) .bind("127.0.0.1:8000" )? .run() .await }
运行并测试:
$ cargo run# 另一个总端,jq 用于格式化返回的 json$ curl 127.0.0.1:8000/todos | jq[ { "id": 2, "title": "List 2" }, { "id": 1, "title": "List 1" } ]
两个改进
数据库的连接信息硬编码在代码中,在实际使用中会使用环境变量进行设置
添加 .env
配置数据库连接信息和服务端口:
SERVER.HOST=127.0.0.1 SERVER.PORT=8000 PG.USER=actix PG.PASSWORD=actix PG.HOST=127.0.0.1 PG.PORT=5432 PG.DBNAME=actix PG.POOL.MAX_SIZE=30
同时,通过环境变量获取对应配置。首先增加 dotenv
和 config
依赖:
[dependencies] dotenv = "0.15.0" config = "0.10.1"
然后增加 config.rs
,增加从环境变量中获取配置并生成连接池方法 from_env
:
use config::{self , ConfigError};use serde::Deserialize;#[derive(Deserialize, Debug)] pub struct ServerConfig { pub host: String , pub port: i32 , } #[derive(Deserialize, Debug)] pub struct Config { pub server: ServerConfig, pub pg: deadpool_postgres::Config, } impl Config { pub fn from_env () -> Result <Self , ConfigError> { let mut cfg = config::Config::new(); cfg.merge(config::Environment::new())?; cfg.try_into() } }
在 main.rs
中使用环境变量创建连接池:
mod config;use dotenv::dotenv;#[actix_web::main] async fn main () -> io::Result <()> { dotenv().ok(); let cfg = crate::config::Config::from_env().unwrap(); let pool = cfg.pg.create_pool(NoTls).unwrap(); println! ( "Starting server at http://{}:{}" , cfg.server.host, cfg.server.port ); HttpServer::new(move || { App::new() .data(pool.clone()) .service(hello) .route("/todos{_:/?}" , web::get().to(todos)) }) .bind(format! ("{}:{}" , cfg.server.host, cfg.server.port))? .run() .await }
db.rs
中 row_to_todo
函数太麻烦,使用 tokio_pg_mapper
做处理,简化操作:
[dependencies] tokio-pg-mapper = "0.1" tokio-pg-mapper-derive = "0.1"
在 models.rs
中添加 PostgresMapper
,
use serde::{Deserialize, Serialize};use tokio_pg_mapper_derive::PostgresMapper;#[derive(Serialize, Deserialize, PostgresMapper)] #[pg_mapper(table = "todo_list" )] pub struct TodoList { pub id: i32 , pub title: String , } #[derive(Serialize, Deserialize, PostgresMapper)] #[pg_mapper(table = "todo_item" )] pub struct TodoItem { pub id: i32 , pub title: String , pub checked: bool , pub list_id: i32 , }
使用 from_row_ref
方法将记录进行转换:
use tokio_pg_mapper::FromTokioPostgresRow;pub async fn get_todos (client: &Client) -> Result <Vec <TodoList>, Error> { let todos = client .query(&statement, &[]) .await .expect("Error getting todo lists" ) .iter() .map(|row| TodoList::from_row_ref(row).unwrap()) .collect::<Vec <TodoList>>(); Ok (todos) }
小结
创建 hello world 程序;
创建数据库连接和获取数据;
使用环境变量;
参考文档和项目
Creating a simple TODO service with Actix
actix-web 官方文档
actix/example
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
from: qiwihui on: 10/20/2020
cool