CLI をインストール

Rust (Cargo)が使えるなら以下を叩くだけでインストールできます。

cargo install diesel_cli
diesel --version
# diesel 1.4.0

エラー関連

note: /usr/bin/ld: cannot find -lmysqlclient collect2: error: ld returned 1 exit status

ubuntu-18.04 で起こりました。

以下で治りました。

sudo apt install gcc default-mysql-client

またはmysql機能を使わない、例えばpostgresしか使わないのであれば以下でインストールでも回避できます。

cargo install diesel_cli --no-default-features --features postgres

failed to compile diesel_cli v1.4.0, intermediate artifacts can be found

macOS で起こりました。

以下で治りました。

cargo +stable install diesel_cli

Migration

動作を見るためにここでは PostgreSQL を使います。接続できる適当なものが無い人は Docker で以下のように建てましょう。

docker run \
  --rm \
  -p 33445:5432 \
  -e POSTGRES_HOST_AUTH_METHOD=trust \
  postgres

これにはpostgres://postgres:@0.0.0.0:33445という URI で接続できます。

準備

最初にdiesel setupという準備コマンドを叩く必要があります。その際上記の URI を指定します。
注意点としてこのコマンドはCargo.tomlを持つディレクトリで実行する必要があります。

# cargo init sandbox && cd sandbox
diesel setup --database-url postgres://postgres:@0.0.0.0:33445

実行後migrations/ディレクトリがこのような構造でできます。

migrations
└── 00000000000000_diesel_initial_setup
    ├── down.sql
    └── up.sql

migration

準備ができたので migration ファイルを作っていけます。例えば以下のようなコマンドを叩きます。

diesel migration generate create_notes_table

先程のmigration/の中身にこれ用のup.sqldown.sqlが作られたハズです。

migrations/
├── 00000000000000_diesel_initial_setup
│   ├── down.sql
│   └── up.sql
└── 2020-01-17-090405_create_notes_table
    ├── down.sql
    └── up.sql

up.sqldown.sqlの内容をそれぞれ以下のようにしてみます。

-- `up.sql`
CREATE TABLE notes (
  id SERIAL PRIMARY KEY,
  title VARCHAR(128) UNIQUE NOT NULL
)

-- `down.sql`
DROP TABLE notes

これらを適用するにはmigration runコマンドを実行します。URI も忘れずに。

diesel migration run --database-url postgres://postgres:@0.0.0.0:33445
# Running migration 2020-01-17-090405_create_notes_table

無事終わったら今度は戻すときのコマンドも問題ないかmigration redoで確認します。

diesel migration redo --database-url postgres://postgres:@0.0.0.0:33445
# Rolling back migration 2020-01-17-090405_create_notes_table
# Running migration 2020-01-17-090405_create_notes_table

どうやら大丈夫そうです。戻すだけのmigration revertコマンドもあり、連続で叩いて好きな所まで戻れます。

--database-url を省略

dotenvモジュールをサポートしているので、.envDATABASE_URLを書き込むとそっちを優先して使ってくれます。

echo 'DATABASE_URL=postgres://postgres:@0.0.0.0:33445' > .env
diesel migration redo 
# Rolling back migration 2020-01-17-090405_create_notes_table
# Running migration 2020-01-17-090405_create_notes_table

Query

データベースから情報を取る方法です。

環境は Postgres のバージョン 11 を使います。またその環境は Docker を用いて以下のコマンドで建てたサーバーを使います。

docker run \
  --name postgres \
  --detach \
  --publish 5432:5432 \
  --env POSTGRES_HOST_AUTH_METHOD=trust \
  postgres:11

準備

動作を見るためにusersというテーブルを作り、その中に適当なデータを入れておきたいと思います。

まずはdiesel_cliでマイグレーションファイルを作ります。名前はcreate_usersとし、以下のコマンドを実行します。実行後migrationsディレクトリ以下に雛形が作られます。

# セットアップがまだの場合
diesel migration setup \
  --database-url postgres://postgres:@loccalhost:5432
# diesel.toml が作られる

diesel migration generate create_users \
  --database-url postgres://postgres:@loccalhost:5432
# migrations/20xx..._create_users/{up,down}.sql
# が作られた

migrations/20xx..._create_users/up.sqlを以下のように編集します。

-- Your SQL goes here

CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(30) NOT NULL,
    nickname VARCHAR(30),
    age INTEGER NOT NULL,
    active BOOLEAN DEFAULT 't' NOT NULL,
    created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL
);

INSERT INTO users (name, nickname, age, created_at) VALUES
    ('foo', 'nickfoo', 20, '2020-04-29T01:17:16.644130000+00:00'),
    ('bar', null, 20, '2020-04-29T01:18:16.644130000+00:00');

usersテーブルの作成と、そのレコードの挿入をしてます。

一応戻せるようにdown.sqlも書いておきます。

-- This file should undo anything in `up.sql`

DROP TABLE IF EXISTS users;

できたら以下のコマンドで実行します。

diesel migration run

ちゃんとデータが入っているか以下で確認します。(jqコマンドを使用します)

psql postgres://postgres:@localhost:5432 \
  --tuples-only \
  --command \
    'select json_agg(users) from users' \
  | tr -d '\n+' \
  | jq .

以下のような出力があれば準備完了です。

[
  {
    "id": 1,
    "name": "foo",
    "nickname": "nickfoo",
    "age": 20,
    "active": true,
    "created_at": "2020-04-29T01:17:16.64413"
  },
  {
    "id": 2,
    "name": "bar",
    "nickname": null,
    "age": 20,
    "active": true,
    "created_at": "2020-04-29T01:18:16.64413"
  }
]

スキーマの確認

実は上記のdiesel migration run ...した際にスキーマモジュールが自動的に作成されているハズです。
例えば今回はこんな感じで作成されてました。

table! {
    users (id) {
        id -> Int4,
        name -> Varchar,
        nickname -> Nullable<Varchar>,
        age -> Int4,
        active -> Bool,
        created_at -> Timestamp,
    }
}

このファイルはdiesel migration setup ...した際に作られるprint_schema.fileに指定されたパスで作られます。

# デフォルト
[print_schema]
file = "src/schema.rs"

この自動作成のモジュールを使うためにsrc/lib.rsを作り以下のようにして、main.rsなどで読み込めるうようにしておきます。またその際table!マクロが解決できるようにdieselmacro_useしておきます。

#[macro_use]
extern crate diesel;

pub mod schema;

このtable!マクロでschema::users::dsl::{users,id,name,...}のようにインポートできるコードが生成されます。

アナログでスキーマモジュールを作る

diesel migrationコマンドを使わないプロジェクトなどで使いたい場合は、自分で手書きて用意しても問題なく動かすことができます。ただしそのパスはdiesel.tomlprint_schema.fileパス先でなければなりません。

スキーマで使用できる型は以下の通りです。

  • BigInt
  • Binary
  • Bool
  • Date
  • Double
  • Float
  • Integer
  • Interval
  • Nullable
  • Numeric
  • SmallInt
  • Text
  • Time
  • Timestamp
  • Tinyint

取得レコード用の入れ物(構造体)の作成

SQLのデータを入れれるような構造体を作ります。守る必要のあるルールで、その構造体はQueryableトレイトを継承してなければなりません。このトレイトはderiveで継承できます。

今回の場合構造体はこのようになります。

#[derive(Queryable, Debug)]
pub struct User {
  pub id: i32,
  pub name: String,
  pub nickname: Option<String>,
  pub age: i32,
  pub active: bool,
  pub created_at: chrono::NaiveDateTime
}

これをsrc/lib.rspub mod schema;の後に追記しておきます。

ここでTimestamp型の扱いに関してですが、chronoというライブラリを使います。これによりCargo.tomlの依存関係を以下のように修正します。
dieselfeaturesにもchronoの追加が必要です。

[dependencies]
diesel = { version = "1.4.4", features = [ "postgres", "chrono" ] }
chrono = "0.4.11"

データを取得する

src/main.rsを作り、最初のマイグレーションで挿入したデータを取得してみます。

use diesel::pg::PgConnection;
use diesel::prelude::*;
use <Cargo.toml の name (- は _ に変換される)>::{schema, User};

const DATABASE_URL: &'static str = "postgres://postgres:@localhost:5432";

fn main() {
  let connection =
    PgConnection::establish(DATABASE_URL).expect(&format!("Error connecting to {}", DATABASE_URL));

  let users = schema::users::dsl::users
    .load::<User>(&connection)
    .expect("Error loading users");

  println!("{:#?}", users);
}

cargo runで実行して以下のような結果が出力されれば完了です!

[
    User {
        id: 1,
        name: "foo",
        nickname: Some(
            "nickfoo",
        ),
        age: 20,
        active: true,
        created_at: 2020-04-29T01:17:16.644130,
    },
    User {
        id: 2,
        name: "bar",
        nickname: None,
        age: 20,
        active: true,
        created_at: 2020-04-29T01:18:16.644130,
    },
]

サンプルコード

動作確認できるコードは nju33-com/get-started-diesel-query に置かれています。

Insert

データベースにレコードを新しく登録する方法です。

環境は Postgres のバージョン 11 を使います。またその環境は Docker を用いて以下のコマンドで建てたサーバーを使います。

docker run \
  --name postgres \
  --detach \
  --publish 5432:5432 \
  --env POSTGRES_HOST_AUTH_METHOD=trust \
  postgres:11

準備

Query 時と同じようにマイグレーションファイルを作り、いくつか適当なレコードが入ってる環境を作ります。

diesel migration generate create_users

作られたup.sqldown.sqlをそれぞれ以下のように編集します。

-- Your SQL goes here

CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(30) NOT NULL
);

INSERT INTO users (name) VALUES
  ('foo'),
  ('bar');
-- This file should undo anything in `up.sql`

DROP TABLE IF EXISTS users;

以下のコマンドでデータを流し込んで完了です。

diesel migration run \
  --database-url postgres://postgres:@localhost:5432
  
psql postgres://postgres:@localhost:5432 \
  --tuples-only \
  --command \
    'select json_agg(users) from users' \
  | tr -d '\n+' \
  | jq .
[
  {
    "id": 1,
    "name": "foo"
  },
  {
    "id": 2,
    "name": "bar"
  }
]

src/schema.rsができているので、いつものsrc/lib.rsを雛形の形にしておきます。

#[macro_use]
extern crate diesel;

pub mod schema;

挿入用の構造体を作る

取得する時の構造体ではQueryableトレイトを継承しましたが、挿入用のものにはInsertableトレイトを継承させます。その構造体ではDEFAULT値が設定されているカラムなど、いらない挿入時には要らない情報を落とて定義します。

また#[table_name = schema.rs にあるテーブル名]で実際のテーブル名の指定が必要になります。その際にはschema.rsから指定したテーブル名の名前空間をスコープに持ってこなければなりません。

// #[table_name="users"] を使うため
use schema::users;

#[derive(Insertable)]
#[table_name="users"]
pub struct NewUser {
   name: String,
}

これをsrc/lib.rsの最後に追記しておきます。

データを挿入する

src/main.rsを書いていきます。

データの挿入にはdiesel::insert_into関数を使います。

use diesel::pg::PgConnection;
use diesel::prelude::*;
use get_started_diesel_query::{schema, NewUser};

const DATABASE_URL: &'static str = "postgres://postgres:@localhost:5432";

fn main() {
  let connection =
    PgConnection::establish(DATABASE_URL).expect(&format!("Error connecting to {}", DATABASE_URL));

  let new_user = NewUser { name: "baz".into() };

  diesel::insert_into(schema::users::table)
    .values(&new_user)
    .execute(&connection)
    .expect("Error saving new user");
}

これをcargo runで実行し以下の結果が得られれば完了です。

psql postgres://postgres:@localhost:5432 \
  --tuples-only \
  --command \
    'select json_agg(users) from users' \
  | tr -d '\n+' \
  | jq .
[
  {
    "id": 1,
    "name": "foo"
  },
  {
    "id": 2,
    "name": "bar"
  },
  {
    "id": 3,
    "name": "baz"
  }
]

挿入時に取得も行う

RETURNINGを含んだINSERT INTOも行えます。その場合executeメソッドではなくget_resultメソッドを呼び出します。またその際はQueryableトレイトを継承した構造体の指定が必要です。

src/lib.rsに取得用の構造体を定義します。

#[derive(Queryable, Debug)]
pub struct User {
  pub id: i32,
  pub name: String,
}

これをsrc/main.rsで読み込み、またdiesel::insert_into箇所を以下のように修正します。

fn main() {
  // ... 略
  let user = diesel::insert_into(schema::users::table)
    .values(&new_user)
    .get_result::<User>(&connection)
    .expect("Error saving new user");
    
  println!("{:#?}", user);
  // User {
  //   id: 3,
  //   name: "baz",
  // }
}

複数のデータを挿入する

.valuesメソッドに Vector で複数のNewUserを渡すことで 1 つのINSERT INTOで複数の値を挿入できます。複数時には.get_result.get_resultsと複数形に代わります。
勿論戻り値が必要ないのであれば.executeも使えます。

fn main() {
  // ... 略

  let baz_user = NewUser { name: "baz".into() };
  let qux_user = NewUser { name: "qux".into() };

  let users = diesel::insert_into(schema::users::table)
    .values(vec![baz_user, qux_user])
    .get_results::<User>(&connection)
    .expect("Error saving new users");
  println!("{:#?}", users);
  // [
  //     User {
  //         id: 6,
  //         name: "baz",
  //     },
  //     User {
  //         id: 7,
  //         name: "qux",
  //     },
  // ]
}

サンプルコード

動作確認できるコードは nju33-com/get-started-diesel-insert に置かれています。

Update

レコード更新する方法です。

環境は Postgres のバージョン 11 を使います。またその環境は Docker を用いて以下のコマンドで建てたサーバーを使います。

docker run \
  --name postgres \
  --detach \
  --publish 5432:5432 \
  --env POSTGRES_HOST_AUTH_METHOD=trust \
  postgres:11

準備

Insert 時と同じ内容です。

Query 時と同じようにマイグレーションファイルを作り、いくつか適当なレコードが入ってる環境を作ります。

diesel migration generate create_users

作られたup.sqldown.sqlをそれぞれ以下のように編集します。

-- Your SQL goes here

CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(30) NOT NULL
);

INSERT INTO users (name) VALUES
  ('foo'),
  ('bar');
-- This file should undo anything in `up.sql`

DROP TABLE IF EXISTS users;

以下のコマンドでデータを流し込んで完了です。

diesel migration run \
  --database-url postgres://postgres:@localhost:5432
  
psql postgres://postgres:@localhost:5432 \
  --tuples-only \
  --command \
    'select json_agg(users) from users' \
  | tr -d '\n+' \
  | jq .
[
  {
    "id": 1,
    "name": "foo"
  },
  {
    "id": 2,
    "name": "bar"
  }
]

src/schema.rsができているので、いつものsrc/lib.rsを雛形の形にしておきます。

#[macro_use]
extern crate diesel;

pub mod schema;

プライマリーキーからデータを更新する

src/main.rsを書いていきます。

まずはプライマリキーでレコードを直接を取得し、その値を更新する方法です。その場合diesel::update関数を使います。

diesel::updateの引数には、table!マクロで生成されるtable_name::dsl::table_name構造体の.findメソッドを対象のプライマリーキー値と共に使います。

更新後に期待する値は.setメソッドを使います。table!マクロはtable_name::dsl以下にそのテーブルのカラム名の構造体を持ちます。その中から更新したいカラム構造体の.eqメソッドを、期待する次の値と共に使い、.setメソッドの引数とします。

use diesel::pg::PgConnection;
use diesel::prelude::*;
use get_started_diesel_insert::schema;

const DATABASE_URL: &'static str = "postgres://postgres:@localhost:5432";

fn main() {
  let connection =
    PgConnection::establish(DATABASE_URL).expect(&format!("Error connecting to {}", DATABASE_URL));

  diesel::update(schema::users::dsl::users.find(1))
    .set(schema::users::dsl::name.eq("baz"))
    .execute(&connection)
    .expect("Error saving new users");
}

条件に当てはまるレコードの値を更新

diesel::update引数で使うtable_name構造体で.eqメソッドではなく.filterを使います。その引数ではカラム名の構造体の.eqを使い、「カラム値がこの値のもの」のようにします。

use diesel::pg::PgConnection;
use diesel::prelude::*;
use get_started_diesel_insert::schema;

const DATABASE_URL: &'static str = "postgres://postgres:@localhost:5432";

fn main() {
  let connection =
    PgConnection::establish(DATABASE_URL).expect(&format!("Error connecting to {}", DATABASE_URL));
    
  let target = schema::users::dsl::users.filter(schema::users::dsl::name.eq("bar"));
  diesel::update(target))
    .set(schema::users::dsl::name.eq("baz"))
    .execute(&connection)
    .expect("Error updating an users");
}

すべてのレコードを更新

単にすべてを更新したければdiesel::updateの引数にそのままtable_name構造体を渡すだけです。

diesel::update(schema::users::dsl::users))
  .set(schema::users::dsl::name.eq("baz"))
  .execute(&connection)

サンプルコード

動作確認できるコードは nju33-com/get-started-diesel-update に置かれています。

Relation

テーブルを関連付けて使う方法です。

環境は Postgres のバージョン 11 を使います。またその環境は Docker を用いて以下のコマンドで建てたサーバーを使います。

docker run \
  --name postgres \
  --detach \
  --publish 5432:5432 \
  --env POSTGRES_HOST_AUTH_METHOD=trust \
  postgres:11

準備

テーブルを関連付けたいのでuserspostsという 2 つのテーブルを作ります。ユーザー 1 人辺り0-nの投稿を持つようなものとして作ります。

ユーザーテーブル

ユーザーから作ります。

diesel migration generate create-users

up.sqlは以下のようにします。foobarという名前のユーザー名も一緒に作ります。

CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(30) NOT NULL
);

INSERT INTO users (name) VALUES
  ('foo'),
  ('bar');

投稿テーブル

同じように投稿テーブルも作ります。

diesel migration generate create-posts

up.sqlはこのようにしました。本来はスネークケースが多いと思いますが、キャメルケースに対応した例の為にuserIdと名付けています。

CREATE TABLE IF NOT EXISTS posts (
    id SERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    "userId" INTEGER NOT NULL REFERENCES users(id)
);

INSERT INTO posts (title, "userId") VALUES
    ('hoge', 1),
    ('fuga', 2);

流します。

diesel migration run \
  --database-url postgres://postgres:@localhost:5432

うまく処理が終わればsrc/schema.rsが以下のように生成されます。

table! {
    posts (id) {
        id -> Int4,
        title -> Varchar,
        userId -> Int4,
    }
}

table! {
    users (id) {
        id -> Int4,
        name -> Varchar,
    }
}

joinable!(posts -> users (userId));

allow_tables_to_appear_in_same_query!(
    posts,
    users,
);

また、こんな感じで投稿が取得できるようになります。

psql postgres://postgres:@localhost:5432 \
  --tuples-only \
  --command \
    '
      select json_agg(posts) from posts 
        where "userId" = (
          select id from users where id = 1
        )
    ' \
  | jq .

この結果は JSON でこんな感じです。

[
  {
    "id": 1,
    "title": "hoge",
    "userId": 1
  }
]

最後にsrc/lib.rsschemaを取り込めるようにしておきます。

#[macro_use]
extern crate diesel;

pub mod schema;

構造体作成

src/lib.rsの最後から以下の 2 つの構造体を追記します。

use schema::{posts, users};

#[derive(Debug, Queryable, Identifiable)]
pub struct User {
  pub id: i32,
  pub name: String,
}

#[derive(Debug, Queryable, Associations, Identifiable)]
#[belongs_to(User, foreign_key = "userId")]
pub struct Post {
  pub id: i32,
  pub title: String,
  #[column_name = "userId"]
  pub user_id: i32,
}

気にする点は 4 つです。

  1. Queryableトレイトを継承
  2. Identifiableトレイトを継承
  3. 関連付けを行う方のテーブルはAssociationsトレイトを継承
  4. belongs_toで属す先の構造体名を指定

またカラム名にキャメルケースを使っているような場合にはさらに 2 つあります。

  1. 構造体のプロパティはスネークケースで定義し、column_nameで実際の名前を指定
  2. belongs_toでさらにforeigh_keyとしてcolumn_nameを指定

これは Diesel がbelongs_to(User)のような時の場合、user_idが参照キーだろうと見てしまうので、これを訂正しなければなりません。

ユーザーから投稿を取得する

src/main.rsを書きます。

紐付けには対象の構造体から::belonging_to関数を呼び出します。その際引数には属す先のインスタンスを指定します。

その後.load.get_resultsなどのメソッドを呼ぶことで、うまく行けば関連する投稿一覧が取得できます。

use diesel::pg::PgConnection;
use diesel::prelude::*;
use get_started_diesel_relation::{Post, User};

const DATABASE_URL: &'static str = "postgres://postgres:@localhost:5432";

fn main() {
  let connection =
    PgConnection::establish(DATABASE_URL).expect(&format!("Error connecting to {}", DATABASE_URL));

  // 検索して取ってきた体
  let user = User {
    id: 1,
    name: "foo".into(),
  };

  let post = Post::belonging_to(&user)
    .load::<Post>(&connection)
    .expect("Error in getting posts");
  println!("{:#?}", post);
}

サンプルコード

動作確認できるコードは nju33-com/get-started-diesel-relation に置かれています。