[package]
name = "todo-list"
version = "0.1.0"
authors = ["qiwihui <qwh005007@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "3"
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 actix
Password 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)
use crate::models::{TodoItem, TodoList};
use deadpool_postgres::Client;
use std::io::Error;
use tokio_postgres::Row;
// 将每条记录转为 TodoList
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
}
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.toml
:在
main.rs
中,使用类似于 python flask 的语法,增加一个最简单的 service。运行并测试:
在另一个终端中
数据库设计
项目中将使用 postgres 作为数据库存储,为了方便操作和管理,我们使用 docker-compose 进行管理。
docker-compose.yml
创建数据库:
然后,我们设计整体数据库表结构,并创建一些基础数据作为测试。表结构如下:
在
database.sql
中手动创建表结构并插入数据:创建数据表并查看结果
获取 todo 列表
首先,添加我们需要的库,其中
serde
用于序列化,tokio-postgres
是一直支持异步的 PostgreSQL 客户端,deadpool-postgres
用于连接池的管理。增加
models.rs
用于管理数据模型,并支持序列化和反序列化。增加
db.rs
用于管理数据操作,例如get_todos
从数据库中获取数据并序列化为TodoList
的数组:增加
handlers.rs
用于处理服务:最后,在
main.rs
中创建连接池并添加路由:运行并测试:
两个改进
添加
.env
配置数据库连接信息和服务端口:同时,通过环境变量获取对应配置。首先增加
dotenv
和config
依赖:然后增加
config.rs
,增加从环境变量中获取配置并生成连接池方法from_env
:在
main.rs
中使用环境变量创建连接池:db.rs
中row_to_todo
函数太麻烦,使用tokio_pg_mapper
做处理,简化操作:在
models.rs
中添加PostgresMapper
,使用
from_row_ref
方法将记录进行转换:小结
参考文档和项目