RustのORM SeaORMを使ってみる

RustのSeaORMというORMを使ってSQLiteにレコード追加したりしてみる
軽く調べてみたところdieselが安パイ的な記事を見かけたものの若干情報が古い&非同期処理対応してないっぽいので今回は見送った

クレートのインストール

Cargo.tomlを編集する
SQLiteを使う場合は↓のようになる(詳細はこちらのドキュメントを参照)

[dependencies]
sea-orm = { version = "^0", features = [ "sqlx-sqlite", "runtime-tokio-native-tls", "macros" ] }
tokio = {version = "1.27.0", features = ["full"]} # 必須じゃないけど非同期処理使いたいので入れておく

DBにコネクションを貼る

SeaORMを使ってSQLiteのDBにコネクションを貼ってみる

use sea_orm::Database;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::connect("sqlite://sample.db?mode=rwc").await?;

    Ok(())
}

sqlite://sample.db?mode=rwcSQLiteのDBファイルのパスとモードを指定している
mode=rwcというクエリパラメータでレコードの読み書きを許可し、さらにsample.dbファイルが存在しない場合は新規作成することを表している
※このURLの仕様についてはこちらを参照

Entityとマッピング

ORM特有のDBのレコードとプログラムのオブジェクトを結びつける儀式を行う

Entityの実装

src/entities/users.rsusersテーブルの定義を書いてみよう

use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "users")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub name: String,
    pub age: i32,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}

そうしたら、main関数でDBにusersテーブルがなければsrc/entities/users.rsの定義に従って作成するようにする

use sea_orm::*;
mod entities;
use entities::users::Entity as Users;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::connect("sqlite://sample.db?mode=rwc").await?;
    let backend = db.get_database_backend();
    let schema = Schema::new(backend);
    let statement = backend.build(schema.create_table_from_entity(Users).if_not_exists());
    db.execute(statement).await?;

    Ok(())
}

実装し終わったらcargo runで実行しsample.dbにusersテーブルができているか確認
SQLiteのファイルはVSCodeSQLite Viewerという拡張機能で閲覧している

CRUD操作

Create

レコードの新規作成処理を実装する

use sea_orm::*;
mod entities;
use entities::users::ActiveModel as UserModel;
use entities::users::Entity as Users;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::connect("sqlite://sample.db?mode=rwc").await?;
    let backend = db.get_database_backend();
    let schema = Schema::new(backend);
    let statement = backend.build(schema.create_table_from_entity(Users).if_not_exists());
    db.execute(statement).await?;

    create(&db).await?;

    Ok(())
}

async fn create(db: &DatabaseConnection) -> Result<(), Box<dyn std::error::Error>> {
    UserModel {
        name: Set("hoge".to_owned()),
        age: Set(20),
        ..Default::default()
    }
    .save(db)
    .await?;

    Ok(())
}

EntityのActiveModelsave()メソッドが生えているのでこれを使う
上記のコードを実行すると期待通りのレコードが追加されている

Read

今度はレコードを一覧取得する

use entities::users::Model;
use sea_orm::*;
mod entities;
use entities::users::ActiveModel as UserModel;
use entities::users::Entity as Users;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::connect("sqlite://sample.db?mode=rwc").await?;
    let backend = db.get_database_backend();
    let schema = Schema::new(backend);
    let statement = backend.build(schema.create_table_from_entity(Users).if_not_exists());
    db.execute(statement).await?;

    create(&db).await?;

    let users = find_all(&db).await?;
    println!("users: {}", users.len());

    Ok(())
}

async fn create(db: &DatabaseConnection) -> Result<(), Box<dyn std::error::Error>> {
    UserModel {
        name: Set("hoge".to_owned()),
        age: Set(20),
        ..Default::default()
    }
    .save(db)
    .await?;

    Ok(())
}

async fn find_all(db: &DatabaseConnection) -> Result<Vec<Model>, Box<dyn std::error::Error>> {
    let users = Users::find().all(db).await?;

    Ok(users)
}

Entityfind()でSELECT文を発行し、さらにall()を実行することでSELECT * FROM...というSQLが実行されて一覧取得するようになっている
子のコードを実行するとレコード数が標準出力される

WHERE句を使う

何らかの条件付きで検索してみる
今回は↓のようなテーブルからname = hogeというユーザーだけ取得する

use entities::users::Model;
use sea_orm::*;
mod entities;
use entities::users::ActiveModel as UserModel;
use entities::users::Column as UserColumn;
use entities::users::Entity as Users;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::connect("sqlite://sample.db?mode=rwc").await?;
    let backend = db.get_database_backend();
    let schema = Schema::new(backend);
    let statement = backend.build(schema.create_table_from_entity(Users).if_not_exists());
    db.execute(statement).await?;

    let user = find_by_name("hoge".to_owned(), &db).await?;
    println!("found user name: {}", user.unwrap().name);

    Ok(())
}

async fn find_by_name(
    name: String,
    db: &DatabaseConnection,
) -> Result<Option<Model>, Box<dyn std::error::Error>> {
    let user = Users::find()
        .filter(UserColumn::Name.eq(name))
        .one(db)
        .await?;

    Ok(user)
}

Columnにattributeと各種条件(eqとか)があるのでそれを使ってfilterを作成する
今回はnameが一致したものを一つだけ取り出す条件

Update

name=hogeのレコードのageを30に更新する処理

async fn update(
    name: String,
    age: i32,
    db: &DatabaseConnection,
) -> Result<(), Box<dyn std::error::Error>> {
    let user = find_by_name(name, db).await?;
    let mut user: UserModel = user.unwrap().into();
    user.age = Set(age);
    user.update(db).await?;

    Ok(())
}

実行するとage=30に更新される

Delete

name=hogeのレコードを削除する

async fn delete_by_name(
    name: String,
    db: &DatabaseConnection,
) -> Result<(), Box<dyn std::error::Error>> {
    let user = find_by_name(name, db).await?.unwrap();
    user.delete(db).await?;

    Ok(())
}

実行するとname=hogeのレコードが消えているはず

RustでWebスクレイピング

RustでWebスクレイピングするCLIツールを作ってみた
相変わらず基礎文法もわからないままChatGPTに質問しまくって実装する

クレートを見繕う

HTMLパーサ

Scraperというクレートが記事もたくさんあってスターも多いのでよさげ
github.com

HTTPリクエス

スクレイピングするには対象のページからHTMLを取得する必要があるのでそれ用のクレートも用意する
こちらはreqwestが有力っぽい

github.com

非同期処理

上二つだけで十分だと思っていたら、Rustはmain関数を非同期処理にできないらしい
今回は1ファイルに手続き的に書く手抜き実装を想定していたのでそれができるようtokioを採用

github.com

クレートをプロジェクトに追加

今までは律儀にtmolファイル編集してたけど普通にcargoコマンドで追加できるっぽい(NuGetとかnpmみたいなノリ)
行き当たりばったり学習だとこういう基礎的な所が抜け落ちるのできちんと体系的に学ぼう

cargo add scraper reqwest tokio

なお、tokioに関しては以下のようにCargo.tmolを編集しておかないと一部機能が有効にならないので注意

[dependencies]
tokio = {version = "1.27.0", features = ["full"]}

実装

スクレイピング対象のHTMLを取得

今回はQiitaのトップページから記事タイトルを一覧取得するのを目標にした
なのでhttps://qiita.com/にアクセスしてHTMLを取得する

適当にURL渡したらreqwestでHTTPリクエスト→レスポンスを文字列化→それをHTMLオブジェクトに変換して返す
という感じの関数を作った

async fn get_html(url: &str) -> Result<Html, Box<dyn std::error::Error>> {
    let res = get(url).await?.text().await?;
    let html = Html::parse_document(&res);
    Ok(html)
}

HTMLをパース

これでHTMLの取得はできるようになったのでパースして記事タイトルの一覧取得をしてみる
QiitaのHTMLを確認すると各記事のタイトルはarticleh2aとタグを辿っていけば取得できそうなのでまずはarticleタグの一覧取得をし、
さらにそれをループ処理してaタグを抽出してその文字列を配列に挿入する関数を作る

fn list_article_title(html: &Html) -> Result<Vec<String>, Box<dyn std::error::Error>> {
    let mut titles: Vec<String> = Vec::new(); // Stringの配列を宣言
    let article_selector = Selector::parse("article").unwrap(); // document.querySelectorAll("article")
    for article in html.select(&article_selector) {
        let anchor_selector = Selector::parse("h2 > a").unwrap().into(); // document.querySelector("h2 > a")
        let anchor = article.select(&anchor_selector).next().unwrap();
        let title = anchor.text().collect::<String>();
        titles.push(title);
    }
    Ok(titles)
}

javascriptdocument.querySelectorAll("article")となるようなSelectorオブジェクトを生成している

動作確認

コード全体

use reqwest::get;
use scraper::{Html, Selector};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "https://qiita.com/";
    let html = get_html(url).await?;
    let titles = list_article_title(&html)?;
    for title in titles {
        println!("{}", title)
    }
    Ok(())
}

async fn get_html(url: &str) -> Result<Html, Box<dyn std::error::Error>> {
    let res = get(url).await?.text().await?;
    let html = Html::parse_document(&res);
    Ok(html)
}

fn list_article_title(html: &Html) -> Result<Vec<String>, Box<dyn std::error::Error>> {
    let mut titles: Vec<String> = Vec::new();
    let article_selector = Selector::parse("article").unwrap();
    for article in html.select(&article_selector) {
        let anchor_selector = Selector::parse("h2 > a").unwrap().into();
        let anchor = article.select(&anchor_selector).next().unwrap();
        let title = anchor.text().collect::<String>();
        titles.push(title);
    }
    Ok(titles)
}

実行してみる

cargo runでこのコードを動かしてみよう


こんな感じでそれっぽいのが表示される


実際にQiitaへアクセスすると期待通りの記事タイトルが取得できているっぽい

ChatGPTに介護されながらRustでSQLiteを操作

ChatGPTにあれこれ質問しながらRustでSQLiteのデータベース作ってレコード作成→読み取りまでやってみた

まずは質問してみる

Q RustでSQLiteCRUD操作をしたい

A rusqliteを使って実装する

rusqliteとは

GitHub - rusqlite/rusqlite: Ergonomic bindings to SQLite for Rust

RustでSQLiteへ接続する時のあれこれをやってくれるクレート
ORMではなく単なるドライバ

Cargo.tomlのdependenciesに追加しておこう

[dependencies]
rusqlite = { version = "0.29.0" }

サンプルコード

ChatGPTが↓のようなコードを書いてくれた

use rusqlite::{params, Connection};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let conn = Connection::open("example.db")?;
    conn.execute(
        "CREATE TABLE IF NOT EXISTS person (
                  id              INTEGER PRIMARY KEY,
                  name            TEXT NOT NULL,
                  age             INTEGER NOT NULL
                  )",
        [],
    )?;

    let name = "Alice";
    let age = 42;
    conn.execute(
        "INSERT INTO person (name, age) VALUES (?1, ?2)",
        params![name, age],
    )?;

    let mut stmt = conn.prepare("SELECT name, age FROM person WHERE id = 1")?;
    let person: (String, i32) = stmt.query_row([], |row| Ok((row.get(0)?, row.get(1)?)))?;

    println!("Found person: {:?}", person);

    Ok(())
}

わからないところを聞く

↑のコードを読んだときに浮かんだのはdynって何だっけ?mutは再代入禁止?...という初歩的な疑問ばかりで全然読めなかった
なのでRustのドキュメント使って調べる、ではなくAIに質問(脳死)

Q Result<(), Box>のdynって何?

A dynはトレイトオブジェクトを宣言する識別子
トレイトオブジェクトはトレイトを実装したインスタンスを表す
トレイトはインターフェースのようにある構造体で実装すべきメソッド/フィールドを定義する

要するにOOPポリモーフィズムを実現するための機能で、今回の場合エラーの戻り値にstd:error:Errorと同じメソッドが実装されている何かしらのオブジェクトが来るということを表している(と理解した)

Q 関数の戻り値の後ろについてる"?"の意味は?

A Result型のハンドリングを簡便化するシンタックスシュガー

fn foo() -> Result<i32, String> {
    let x = bar()?; // bar()がErrを返す場合、foo()はそのErrを返す
    Ok(x)
}

bar()がOKを返す場合は変数xにその値が代入され、Errを返す場合はその場でfoo()もそのErrを返すようになる

Q 引数を|で囲い|row|ってしてるけど何?

A その引数がクロージャとして扱われることを表している

クロージャとは無名関数(jsでいうアロー関数とか)のことであり、クロージャのスコープ内の変数はキャプチャ(そのスコープ内で固定)される

fn main() {
    let x = 10;
    let y = 20;

    let add = || {
        x + y
    };

    println!("The result is {}", add());
}

例えばこのようなコードだと、変数addにx + yを返す無名関数が代入される
そして、xとyはキャプチャされているのでaddをいつどこで呼び出しても30が返ってくる

サンプルコードでは以下のようにstmt.query_row()の第二引数クロージャを使っている

let person: (String, i32) = stmt.query_row([], |row| Ok((row.get(0)?, row.get(1)?)))?;

大分わかりにくい形になっているが、やっていること自体はrowを引数にとりrowのインデックス0と1の値(=personテーブルのnameとage)をタプルとして返すだけの関数

実行する

cargo runで実行してみよう

※環境によっては= note: /usr/bin/ld: cannot find -lsqlite3: No such file or directoryというエラーが発生するかもしれない
これは以下のようにしてsqlite3のライブラリをインストールしてやればOK

sudo apt-get install libsqlite3-dev

実行してみてFound person: ("Alice", 42)と表示されれば成功
また、SQLiteなのでデータベースがファイル出力される(サンプルコードそのままだとexample.dbという名前)

Rustの勉強を始めたくなったのでChatGPTに教えてもらう

巷で噂になってからそこそこ経つプログラミング言語Rustについて本格的に勉強したくなったのでChatGPTを使って学ぼうと思う
AIをツールとして従えるための訓練という面もある(というかそっちがメイン)

Rustの特徴とかを知る

Q. Rustってどんなプログラミング言語

A.コンパイル言語で、実行速度が速く、メモリ使用量を制御でき、高い並列性を持ち、安全性が強化されている

事前にざっと眺めた公式ドキュメントで推されていたようなことが書いてある
こんな文章がパッと出てくるのはやっぱりすごい

Q. Rustはどんな場面で使われている?

A. OS, Webブラウザ, ゲームエンジンなど

OS: GoogleがKataOSなる組み込み向けOSを開発しているらしい
Webブラウザ: ご存じの通りFirefoxの現場で使われている
ゲームエンジン: BevyエンジンというのがRustで実装されているようだ

Rustを勉強するには何から始めたらいい?

A. 公式ドキュメントを読み、コードを書き、ライブラリを触って、コミュニティに参加し、プロジェクトに参加

思っていたよりスコープの広い話で驚いたが、やはり公式ドキュメントのチュートリアルを読むのが手っ取り早そうだ

Rustのチュートリアルを読む

言われた通りにチュートリアルを読んでみる
有志による公式ドキュメントの日本語訳版があったのでありがたく利用させてもらおう

doc.rust-jp.rs

このチュートリアルを10章までやってみて、ある程度Rustの文法等は理解できた

ChatGPTにコードを書いてもらう

まだ無からコードを書くのはしんどい程度の知識量なので一旦AIにお手本を書いてもらう
最終的にRustでスクレイピングしてみたいので、手始めにHTTPリクエストするコードを質問してみた

Q. RustでHTTP リクエストを送信するコードを書いてほしい

use std::collections::HashMap;
use std::io;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let resp = reqwest::get("https://httpbin.org/ip")
        .await?
        .json::<HashMap<String, String>>()
        .await?;
    println!("{:#?}", resp);
    Ok(())
}

reqwesttokioというクレートを使っているのでCargo.tomlを編集しておこう

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }

このコードをコピペしてcargo runで実行すると確かにhttpbin.orgからのレスポンスを受け取れているようだ

おまけ


こんなポンコツに教わってたのかよ!

マスターオブニンジャは救済可能か

使っているだけでキックされると噂の産廃武器マスターオブニンジャ
先日、大辞典を眺めているとフロンティアの面白いスキルを発見したのでそれがサンブレイクに実装されたらどうなるか...という妄想記事

余剰会心を活かすスキル閃転

wikiwiki.jp

会心率が100%を超えた分だけ攻撃力がアップするスキル
MHFで実装されていたスキルで、メインシリーズにはまだ輸入されていない

スキル詳細

  • 会心率+30%
  • 会心率100%を超えた分だけ攻撃力アップ

攻撃力アップの計算式

上昇武器倍率 = √(余剰会心率) * 7

小数点以下切り捨て...っぽいけど詳細不明らしい
この記事では切り捨てとして計算する

計算してみる

マスターオブニンジャのスペック

何もなし...攻撃力300、会心100%、切れ味青、百竜スロ1
錬成MAX...攻撃力350、会心100%、切れ味青、百竜スロ1

今回は錬成でスロ拡張6、攻撃Lv4の時のデータを使う

搭載するスキル

シンプルにするために誰でもつけるであろう7733だけで計算する
攻撃7、達人7、弱点特攻3、超会心3

モンスターと技

原種レイアの頭に旋刈りを当てた時のダメージを計算する

計算結果

↓のシミュレータを利用

macarongamemo.com

ニンジャでレイアの頭に旋刈り当てた時のダメージは...
閃転なし 301.938(攻撃力350)
閃転あり 365.383(攻撃力426)

閃転ありだとダメージ約60もアップ!
せっかくなのでアクルスでも計算してみた

アクルスとの比較

攻撃力375、会心0%、切れ味白、百竜スロ2(百竜装飾品は竜種特攻にする)
アクルスでのダメージ 361.930

(まさかの)龍属性武器が圧倒的に有利なレイアですらニンジャの方が上回るという結果に

結論

閃転ひとつでニンジャが一線級の武器とも渡り合えるほど伸びることが判明
もちろん青だと心眼なしでは危なすぎるしチクチクも属性強化も乗らない以上火力はすぐ伸び悩みそうだけど、少なくとも産廃とは言われなくなるのではなかろうか

...まあ、そう都合よく閃転が追加されるとは思わないし、追加されてもこんなのよりルナロやアスタロフィ使うかな...

MonsterHunter Sunbreakのプレイ日記

ずっとマイクラの工業MOD解説的な記事ばかりで飽きてきたのでたまには日記っぽいのを書く

片手剣使いがアプデで臨むこと

4月予定のアプデで諸々調整が入るそうなのでこうなったらいいのになっていうあれこれ

属性武器との格差是正

狂化奮闘や激昂変換で属性重視の武器(特に弓と双剣)の火力がグンと上がったが、逆に物理偏重な片手剣はすっかりインフレに置いてかれてしまった
同じく物理重視の大剣は冰気で底上げされたが片手剣の場合ジャストラッシュが微妙なのもあって恩恵にあずかれず

冰気に代わる物理武器救済スキルを実装してもらいたい

穿ち斬り救済

異様に短いリーチと無駄に長いモーションのせいで全く使われていない穿ちに利用価値を与えてほしい
このままじゃ盾コンや溜め斬りと何ら変わらんクソ技のまま終わってしまう

風車の属性補正撤廃

P3片手の属性7割補正もそうだけど絶対要らなかっただろこれ!
手数は双剣に比べたら大したことないのになぜか双剣と同じ属性偏重武器と捉えられているのだろうか

チャアク渡ってみた

ひたすら回転ループと昇竜と旋風車をこすり続けるのも飽きたので超出チャアクに手を出してみた
面倒と聞いていた儀式も開幕盾強化隙を見て剣強化して大技にCFC超出決めるだけならそんなでもなかったし、何よりも必殺技みたいなのは新鮮で面白い
ミドロ武器ないのもちょっと親近感

金冠集め


まだまだ出る気配のないバルファルク最大金冠
ちょっと精神的に苦しいのでたまに救難出しているけどさすがに誰も来ない
無駄に色んなフィールドに出るから参加する側からしたら面倒そうだし

初めてヒーラーと遭遇


220ランダムに潜っていたらでまさかの広域笛と出会ってしまった
噂には聞いていたけど総火力オトモ以下って一体どういうことなんだ...

導管MOD XNetの基本的な使い方

アイテム、液体、エネルギー等を搬入出できるMOD XNetの解説

必要なアイテムを用意する

MachineFrame


中間素材

Controller


XNetのネットワークを構成するブロック
最低1つ必要

Red Network Cable


Controller、Connectorをつなげるのに使うケーブル
Red、Green、Blue、Yellowの4色あり、違う色のケーブルとはつながらない

Red Connector


チェスト等をネットワークに組み込むのに使う

Advanced Red Connector


Connectorより輸送効率がいい

ネットワークの構築

配線する


適当な所にControllerを設置し、エネルギーを供給する


ケーブルを使ってControllerとチェストを接続する
この時、ブロックと隣接させるのはConnectorでなければならない

画像の配置を一応文字で書いておくと

Controller、Connector、Cable、Connector、チェスト


同じ要領でもう一つチェストを置いておく

ネットワークの設定


Controllerを右クリックして設定を開く


左上の適当な数字(今回は1)を選び右上からxnet.itemを選んでCreate


するとそのチャンネルがアイテム運搬用のものになる


そうしたらチェストをそれぞれInsertExtractに設定する


Extractにしたチェストにアイテムを入れるともう一方へ搬出されていくのが確認できたらOK