[ Rust ] DieselでPostgreSQLをマイグレーション~crud操作までしてみた
Diesel(Rust)でPostgreSQLをマイグレーション~操作までしてみたので、その時の手順についてまとめておく。
実施環境
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.5 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
cargoプロジェクトの作成
ライブラリクレートをベースに作成していく。
$ cargo new --lib psql1
$ cd psql1
$ mkdir -p src/bin
PostgreSQLのインストール
公式に沿ってインストールしていく。
$ sudo apt-get install curl ca-certificates gnupg
$ curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
$ sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
$ sudo apt-get update
$ sudo apt-get install postgresql-12
無事に入った。
$ sudo apt list --installed
postgresql-12/bionic-pgdg,now 12.5-1.pgdg18.04+1 amd64 [インストール済み]
postgresql-client-12/bionic-pgdg,now 12.5-1.pgdg18.04+1 amd64 [インストール済み、自動]
postgresql-client-common/bionic-pgdg,bionic-pgdg,now 223.pgdg18.04+1 all [インストール済み、自動]
postgresql-common/bionic-pgdg,bionic-pgdg,now 223.pgdg18.04+1 all [インストール済み、自動]
PostgreSQLデータベースの作成
以下の手順で作成してみた。まずpostgresユーザーでpsqlにログイン。
PostgreSQLのユーザー認証はデフォルトでpeer認証のようなので、DB側のユーザーに合わせてubuntu側のユーザーもこしらえる。
$ sudo adduser guest //ユーザーの追加
$ sudo -u postgres psql
postgres=# create database diesel_demo;
postgres=# \l
postgres=# \c diesel_demo
diesel_demo=# grant insert, update, delete, references on posts to guest; //ユーザーのテーブルに対する必要な権限を入れる
diesel_demo=# grant usage on all sequences in schema public to guest; //nextvalで自動採番するシーケンスへのアクセスも許可しないとエラーになったので
間違えて権限あげすぎたらrevoke
(これは例です)
postgres=# revoke select on all sequences in schema public from guest;
※ (diesel migration runのあとで)一応guestユーザーでもinsertできるか確認
$ su - guest
$ psql -U guest diesel_demo
diesel_demo=# insert into (title, body, published) values ('test', 'test', 'f');
INSERT 0 1
大丈夫そう。
dieselを入れる
Cargo.tomlに追記していく。
(抜粋)
[dependencies]
diesel = { version = "1.4.4", features = ["postgres"] }
dotenv = "0.15.0"
$ cargo install diesel_cli --no-default-features --features postgres
これでdieselコマンドが使えるようになる。
ちなみにこのオプションは、デフォルトでmysqlなど他のDBの機能まで持ってくるものなので、MySQLなどがローカルマシンに入ってないとエラーになる。
そのため、必要なPostgreSQLの機能のみ要求するためのオプションを付けている。
.envにデータベース接続情報を定義、dieselのセットアップ
DATABASE_URL=postgres://[ユーザー名]:[DBパスワード]@[ホスト名]:[ポート番号]/[データベース名]
のフォーマットで定義していく。
DATABASE_URL=postgres://guest:password@localhost:5433/diesel_demo
ここまでできたら、dieselをセットアップする。
$ diesel setup
このとき、自動でスキーマ定義も作成される。
src/schema.rs
table! {
posts (id) {
id -> Int4,
title -> Varchar,
body -> Text,
published -> Bool,
}
}
dieselでマイグレーションファイルを生成
diesel migration generate create_posts
上記のコマンドで、
migrations/[日付]/_create_posts/up.sql(追加用)
migrations/[日付]/_create_posts/up.sql(redo用)
のようなマイグレーションファイルが生成される。up.sql
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
body TEXT NOT NULL,
published BOOLEAN NOT NULL DEFAULT 'f'
)
- down.sql
DROP TABLE posts
それぞれの挙動を定義したら、下記を実行。
diesel migration run
データベース「diesel_demo」に「posts」テーブルが作成される。
この操作は下記で巻き戻し可能。
diesel migration redo
プログラムの記述
以下の構成で実施する。
$ tree src
src
├── bin
│ ├── delete-post.rs
│ ├── publish-post.rs
│ ├── show-posts.rs
│ └── write-post.rs
├── lib.rs
├── models.rs
└── schema.rs
- src/show-posts.rs(投稿したデータを閲覧する用)
extern crate psql1;
extern crate diesel;
use self::psql1::*;
use self::models::*;
use self::diesel::prelude::*;
fn main() {
use psql1::schema::posts::dsl::*;
let connection = establish_connection();
let results = posts.filter(published.eq(true))
.limit(5)
.load::<Post>(&connection)
.expect("Error loading posts");
println!("Displaying {} posts", results.len());
for post in results {
println!("{}", post.title);
println!("----------\n");
println!("{}", post.body);
}
}
- src/publish-post.rs(投稿したデータを公開する用)
extern crate psql1;
extern crate diesel;
use self::diesel::prelude::*;
use self::psql1::*;
use self::models::Post;
use std::env::args;
fn main() {
use psql1::schema::posts::dsl::{posts, published};
let id = args().nth(1).expect("publish_post requires a post id")
.parse::<i32>().expect("Invalid ID");
let connection = establish_connection();
let post = diesel::update(posts.find(id))
.set(published.eq(true))
.get_result::<Post>(&connection)
.expect(&format!("Unable to find post {}", id));
println!("Published post {}", post.title);
}
- src/write-post.rs(記事データを投稿する用)
extern crate psql1;
extern crate diesel;
use self::psql1::*;
use std::io::{stdin, Read};
fn main() {
let connection = establish_connection();
println!("What would you like your title to be?");
let mut title = String::new();
stdin().read_line(&mut title).unwrap();
let title = &title[..(title.len() - 1)];
println!("\nOk! Let's write {} (Press {} when finished)\n", title, EOF);
let mut body = String::new();
stdin().read_to_string(&mut body).unwrap();
let post = create_post(&connection, title, &body);
println!("\nSaved draft {} with id {}", title, post.id);
}
#[cfg(not(windows))]
const EOF: &'static str = "CTRL+D";
#[cfg(windows)]
const EOF: &'static str = "CTRL+Z";
- src/bin/delete-post.rs(投稿データを削除する用)
extern crate psql1;
extern crate diesel;
use self::diesel::prelude::*;
use self::psql1::*;
use std::env::args;
fn main() {
use psql1::schema::posts::dsl::*;
let target = args().nth(1).expect("Expected a target to match against");
let pattern = format!("%{}%", target);
let connection = establish_connection();
let number_deleted = diesel::delete(posts.filter(title.like(pattern)))
.execute(&connection)
.expect("Error deleting posts");
println!("Deleted {} posts", number_deleted);
}
- src/lib.rs(DBコネクションとinsertクエリの関数用)
#[macro_use]
extern crate diesel;
extern crate dotenv;
use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;
use std::env;
pub mod schema;
pub mod models;
use self::models::{Post, NewPost};
//postレコードをinsertするクエリを実行
pub fn create_post<'a>(conn: &PgConnection, title: &'a str, body: &'a str) -> Post {
use schema::posts;
let new_post = NewPost {
title: title,
body: body,
};
diesel::insert_into(posts::table)
.values(&new_post)
.get_result(conn)
.expect("Error saving new post")
}
//コネクションの基本部分
pub fn establish_connection() -> PgConnection {
dotenv().ok();
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
PgConnection::establish(&database_url)
.expect(&format!("Error connecting to {}", database_url))
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
- model.rs(DBのための構造を定義)
use super::schema::posts;
/*
postレコード一件の基本単位
*/
#[derive(Insertable)]
#[table_name="posts"]
pub struct NewPost<'a> {
pub title: &'a str,
pub body: &'a str,
}
#[derive(Queryable)]
pub struct Post {
pub id: i32,
pub title: String,
pub body: String,
pub published: bool,
}
各命令を呼び出す準備
以下のようにすることで
$ cargo run --bin [name]
のように実行バイナリの指定が可能。
- Cargo.toml
(末尾に追記)
[[bin]]
name = "show_posts"
path = "src/bin/show-posts.rs"
[[bin]]
name = "write_post"
path = "src/bin/write-post.rs"
[[bin]]
name = "publish_post"
path = "src/bin/publish-post.rs"
[[bin]]
name = "delete_post"
path = "src/bin/delete-post.rs"
さあ実行
- [ 一覧表示 ]
$ cargo run --bin show_post
投稿済みの記事(タイトル、本文)を表示する。
はじめは記事がないので「Displaying 0 posts」の表示だが、以下のwrite_publish(公開)後には見えるようになる。
- [ 作成 ]
$ cargo run --bin write_post
記事を投稿する。
コンソールからの入力(タイトルは一行、本文はreturnキー改行にて複数行入力可)をString::new()で作った受け皿で受けてsrc/lib.rsのcreate_post関数でテーブルに書き込む。
対話コンソールが立ち上がるので、記事のタイトルを入れてreturn、続けて記事本文をを入力し「ctrl + D」で完了する。
正常に完了すると、
Ok! Let's write [入力した記事タイトル] (Press CTRL+D when finished)
Saved draft [入力した記事タイトル] with id [id]
のようにidが払い出されるので、更新時に利用する。
- [ 更新 ]
$ cargo run --bin publish_post [記事id]
前述の記事でidが払い出されるので、そのidを指定することにより、記事を公開ステータスに変化させる。
これでshow_posts時に閲覧可能になる。
- [ 削除 ]
$ cargo run --bin delete_post [記事タイトル]
write_post時に入力した記事タイトルを指定することで、記事の削除ができる。
この後show_postsすると一覧からなくなっている。
最後に
今回は英文の公式サイトを見ながら実際にDBのやりとりを実装してみた。
ターミナルからの入力をwebアプリのフォームなどに置き換えてみるのもおもしろいかも?
他にもいろいろな操作ができるようなので、MySQLなど他のDBや、サービスに組み込んで触っていきたいと思う。
今回はここでおしまい。
参考: diesel公式