0utputab1e

[ Rust ] DieselでPostgreSQLをマイグレーション~crud操作までしてみた

 2020-12-13
 

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公式

 

あわせて読みたい記事

>> Homeに戻る