RustでBigQueryにあるデータを取得してみる
最近BigQueryには慣れてきたが、何らかのプログラムから操作する機会がなかったため、Rustで操作してみました
環境
Rust: 1.51
ライブラリ
https://github.com/Byron/google-apis-rs/tree/main/gen/bigquery2
サービスアカウント
今回RustからBigQueryに接続するための認証としてサービスアカウントを使用します。
お試しのため、ロールは「BigQuery 管理者」を付与しています。
作成したサービスアカウントからJSONで鍵を作成、ファイル名をauth.json
に変更し、Rustプロジェクト直下におきます。
BigQuery
sample_1という名前のテーブルを作成し、
適当に2レコードほどデータを入れています
実装
ライブラリのリファレンスを参考に進めていこうと思います
必要ライブラリの記入
[package] name = "app" version = "0.1.0" authors = [] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] google-bigquery2 = "*" hyper = "^0.14" hyper-rustls = "^0.22" serde = "^1.0" serde_json = "^1.0" yup-oauth2 = "^5.0" tokio = { version = "1.6.1", features = ["full"] } # #[tokio::main]を使うために入れている
コード
projectId
,datasetId
やテーブルは必要に応じて変更してください。
use bigquery2::Error; use hyper; use hyper_rustls; use yup_oauth2 as oauth2; use google_bigquery2 as bigquery2; #[tokio::main] async fn main() { let secret = oauth2::read_service_account_key(&"auth.json".to_string()).await.unwrap(); // 認証ファイルの読み込み let key = oauth2::ServiceAccountAuthenticator::builder(secret).build().await.unwrap(); let hub = bigquery2::Bigquery::new(hyper::Client::builder().build(hyper_rustls::HttpsConnector::with_native_roots()), key); let mut req = bigquery2::api::QueryRequest::default(); req.query = Some("SELECT * FROM `projectId.datasetId.sample_1`".to_string()); // 実行したいSQL req.use_legacy_sql = Some(false); // standardSQLを使うためfalseに let result = hub.jobs().query(req, "projectId") .doit().await; match result { Err(e) => match e { Error::HttpError(_) |Error::Io(_) |Error::MissingAPIKey |Error::MissingToken(_) |Error::Cancelled |Error::UploadSizeLimitExceeded(_, _) |Error::Failure(_) |Error::BadRequest(_) |Error::FieldClash(_) |Error::JsonDecodeError(_, _) => println!("{}", e), }, Ok(res) => { let query_res: bigquery2::api::QueryResponse = res.1; println!("Success: {:?}", query_res.rows.unwrap()) }, } }
結果
以下のようにテーブルの中身が取れました!
TableCell
はString型で定義されているため、実際に使うとなると少し工夫は必要そうです。
Success: [TableRow { f: Some([TableCell { v: Some("2") }, TableCell { v: Some("fuga") }]) }, TableRow { f: Some([TableCell { v: Some("1") }, TableCell { v: Some("hoge") }]) }]
まとめ
今回はRustでBigQueryにあるデータを取得してみました。
個人的にRustでGCPサービスに接続すること自体初めてだったため、
サービスアカウントによる認証方法を探すのにyup-oauth2
のコードを見ていて時間がかかってしまいました。。
他の機能も色々あるのでこれから試してみようと思います!
参考
BigQuery ワイルドカードテーブルで正規表現を使って特定テーブルだけ読み込む
BigQueryで特定文字が含まれるテーブルだけのデータを対象にしたかったのでメモ
結論
SELECT * FROM `project.dataset.table*` WHERE REGEXP_CONTAINS(_TABLE_SUFFIX, "ここに文字")
こんな使い方をするかはわからないが
「table_201004、table_201005...table_202104」のように年月毎に分かれたテーブルがあったとき、4月だけを指定したい場合は以下のようになる
SELECT * FROM `project.dataset.table*` WHERE REGEXP_CONTAINS(_TABLE_SUFFIX, "[0-9]{4}04")
参考
Rubyに静的型付けSorbetを触ってみる
ついにRubyでも静的型付けができるようになりましたね。
Typescriptなど少し前から、静的型付けが熱いですがやっとRubyでも!
僕自身、業務上ではRubyを使っていて、動的型付けに悩まされることもあるので嬉しいです!
とはいえ、触ってみないとわからないため、今回はそのメモです。
今回はSorbetのPlaygroundで簡単に触ってみます。
Sorbetとは?
Gradual Type Checkingは段階的な型チェック
つまり、ファイル単位やメソッド単位など細かい粒度で採用していけるということ。
「Sorbetを採用したいけどシステムが大規模...」
「どれだけの時間がかかるか...」
といったことがなく、1つ1つのファイル対応や重要な処理を優先的にできる!
もちろん全て対応できた方がいいと思います...
メソッドシグネチャ
コード内で型チェックを可能にするための主な方法
# typed: true require 'sorbet-runtime' class Sample # シグネチャを使用するため、クラス・モジュールのトップに必要 extend T::Sig sig {params(x: Integer, y: Integer).returns(Integer)} def self.sum(x, y) x + y end end Sample.sum(1, 2) # => 3 Sample.sum('aa', 'bb') # => error
sig {params(x: Integer, y: Integer).returns(Integer)}
これは、引数xとyがInteger型、戻り値がIntegerという意味です。
以下のように複数行でも可能なため、長くなりそうな時でも可読性は問題なさそうです。
sig do params( x: Integer, y: Integer ) .returns(Integer) end
戻り値がない場合は
sig {void} def self.print_bar puts 'bar' end
とかけます
型アサーション
種類としては4つあります。
T.let(expo, Type)
T.cast(expo, Type)
T.must(expo)
T.assert_type!(express, Type)
T.let
変数代入で型指定ができる
x = T.let(10, Integer) y = T.let(10, String) => error
T.cast
# typed: true require 'sorbet-runtime' extend T::Sig class A; def foo; end; end class B; def bar; end; end sig {params(label: String, a_or_b: T.any(A, B)).void} def foo(label, a_or_b) case label when 'a' a_or_b.foo # Not enough arguments provided for method Object#foo on B component of T.any(A, B). when 'b' a_or_b.bar # Method bar does not exist on A component of T.any(A, B) end end
これは、引数にそんなメソッドはないと言われています。 そのため以下のようにcastしてclassを明示してあげる必要があります。
case label when 'a' T.cast(a_or_b, A).foo when 'b' T.cast(a_or_b, B).bar end
T.must
変数の中身がnilだとエラーを出す。
使われていない変数の検知やnilが入って欲しくない変数に使えそう?
class A extend T::Sig sig {void} def foo x = T.let(nil, T.nilable(String)) y = T.must(nil) puts y # error: This code is unreachable end sig {void} def bar vals = T.let([], T::Array[Integer]) x = vals.find {|a| a > 0} T.reveal_type(x) # Revealed type: T.nilable(Integer) y = T.must(x) puts y # no static error end end
T.assert_type!
以下では、assert_type
でxはString型と断言しているが、xは型がないためエラーになっています。
class A extend T::Sig sig {params(x: T.untyped).void} def foo(x) T.assert_type!(x, String) # error here end end
まとめ
今回はPlaygroundを使って、どんな感じなのかを触ってみました。
まだ触り始めたばかりですが、型があることで変数に何が入ってくるのかわかりいいですね。
あと段階的に型に対応していけるのは嬉しいですね!
参考
RustでMySQLを操作してみる(書き込み)
前回はRustでMySQLの読み込み操作について書きました。
引き続き、書き込み操作について書いていきます。
今回もdieselのチュートリアル参考にしていきます。
http://diesel.rs/guides/getting-started/
読み込みではチュートリアルとの違いはそこまでなかったのですが、
書き込みに関しては、異なる部分がほんの少し増えました。
書き込み処理追加
- src/models.rs
#[derive(Queryable)] pub struct Post { pub id: i32, pub title: String, pub body: String, pub published: bool, } // 以下、追加部分 use super::schema::posts; #[derive(Insertable)] #[table_name="posts"] pub struct NewPost<'a> { pub title: &'a str, pub body: &'a str, }
modelsには、postsテーブルを使うことを宣言しています。
そのため、schema.rsからposts
を読み込んでいます。
NewPostの構造体には、titleとbody定義しています。
これにより、DBに入力する際はtitleとbobyに対して値を代入することができます。
- src/lib.rs
#[macro_use] extern crate diesel; extern crate dotenv; pub mod schema; pub mod models; use diesel::prelude::*; use dotenv::dotenv; use diesel::mysql::MysqlConnection; use std::env; pub fn establish_connection() -> MysqlConnection { dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); MysqlConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url)) } // 以下、追加部分 use self::models::{NewPost, Post}; pub fn create_post(conn: &MysqlConnection, title: &str, body: &str) -> Post { use self::schema::posts::dsl::{id, posts}; let new_post = NewPost { title: title, body: body, }; diesel::insert_into(posts) .values(&new_post) .execute(conn) .expect("Error saving new post"); posts.order(id.desc()).first(conn).unwrap() }
postsテーブルに書き込みを行うために必要な処理を書いています。
読み込み同様にPgConnection
の部分をMysqlConnection
に置き換えます。
チュートリアルではdiesel::insert_into
以下の処理は次のようになっています。
diesel::insert_into(posts::table) .values(&new_post) .get_result(conn) .expect("Error saving new post")
PostgreSQLでは.get_result(conn)
となっていますが、
MySQLでは execute(conn)
を使います。
dieselのドキュメントには、PostgreSQLなどのReturning句をサポートするものはexecute
の代わりにget_results
を使うと書いてあります。
https://docs.diesel.rs/diesel/fn.insert_into.html#examples
下記に関しては、自分で付け加えた部分になります。
戻り値をPostにしているため、行っていることとしてはidで降順にし、Postが返るようにしています。
ここでid
を使用しているため、use self::schema::posts::dsl::{id, posts};
でidを宣言している感じですね。
posts.order(id.desc()).first(conn).unwrap()
以下のwrite_post.rs
に関してはチュートリアルと変更はありません。
- src/bin/write_post.rs
extern crate diesel_demo; extern crate diesel; use self::diesel_demo::*; 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)]; // Drop the newline character 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: &str = "CTRL+D"; #[cfg(windows)] const EOF: &str = "CTRL+Z";
ここまでできたら
docker-compose run --rm rust cargo run --bin write_post
で実行します。
What would you like your title to be? write_test Ok! Let's write write_test (Press CTRL+D when finished) hello Saved draft write_test with id 1
sqlに登録されているか確認すると
mysql> select * from posts; +----+------------+-------+-----------+ | id | title | body | published | +----+------------+-------+-----------+ | 1 | write_test | hello | 0 | +----+------------+-------+-----------+ 1 row in set (0.00 sec)
ちゃんと書き込みできていますね!
まとめ
前回に引き続き、dieselを用いたMySQL操作を行いました。 チュートリアルには、更新と削除処理もあるので試してみてください。
チュートリアルを一通り試すと、簡単なTODOリスト的なものはできそうですね!
RustでMySQLを操作してみる(読み込み)
RustでAPIサーバを作成するにあたり、MySQLを使いたいと思ったため どんな感じで使うことができるのか試してみました。
個人的には今までRailsで開発を行っていたこともあり、 ActiveRecordのようなORマッパーがよかったため、dieselを使用します。
http://diesel.rs/guides/getting-started/
環境
プロジェクト作成
ディレクトリ作成
mkdir diesel_demo cd diesel_demo
この中にdockerの準備していきます
docker
- docker/rust/Dockerfile
FROM rust:1.31 WORKDIR /diesel_demo RUN apt-get update -y && apt-get upgrade -y RUN apt-get install -y mysql-client RUN cargo install diesel_cli --no-default-features --features mysql
imageは公式が配布しているもの
https://hub.docker.com/_/rust/
docker内からMySQLを操作するため、mysql-clientをインストール
diesel_cliをインストールすることでスキーマ管理できます。
- docker-compose.yml
version: '2' services: db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: password ports: - 3306:3306 volumes: - ./docker/mysql:/var/lib/mysql container_name: demo_sql rust: build: context: . dockerfile: ./docker/rust/Dockerfile environment: USER: root volumes: - ./:/diesel_demo depends_on: - db container_name: demo_rust
ここまで準備できたら以下を実行しプロジェクトを作成していきます。
docker-compose build docker-compose run --rm rust cargo init
生成されたCargo.tomlに以下を追記し、docker-compose run --rm rust cargo install
[dependencies] diesel = { version = "1.4.0", features = ["mysql"] } dotenv = "0.9.0
diesel_cliによるセットアップ
初めにdieselでMySQLに接続するために.env
を用意
echo DATABASE_URL=mysql://root:password@db/diesel_demo > .env
次に以下コマンドを実行します
docker-compose run --rm rust diesel setup
これでデータベースにdiesel_demoが作成されます
docker-compose run --rm rust diesel migration generate create_posts
上記コマンドで以下のファイルが生成されるのでそれぞれ記述していきます。
migrations/20160815133237_create_posts/up.sql
migrations/20160815133237_create_posts/down.sql
- up.sql
CREATE TABLE posts ( id INTEGER AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, body TEXT NOT NULL, published BOOLEAN NOT NULL DEFAULT 0 ) DEFAULT CHARACTER SET= utf8mb4;
- down.sql
DROP TABLE posts
sqlファイルが書けたあと実行し、テーブル作成
docker-compose run --rm rust diesel migration run
作成したテーブルを削除するときはdiesel migration revert
でできる
DBの読み込み
以下のファイルを用意していきます 基本的にチュートリアルと変わらないため説明は省きます。 異なる部分としては、チュートリアルはPostgreSQLのため、 部分的にMySQLに書き換える必要があります。
- src/models.rs
#[derive(Queryable)] pub struct Post { pub id: i32, pub title: String, pub body: String, pub published: bool, }
- src/lib.rs
#[macro_use] extern crate diesel; extern crate dotenv; pub mod schema; pub mod models; use diesel::prelude::*; use dotenv::dotenv; use diesel::mysql::MysqlConnection; use std::env; pub fn establish_connection() -> MysqlConnection { dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); MysqlConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url)) }
- src/bin/show_posts.rs
extern crate diesel_demo; extern crate diesel; use self::diesel_demo::*; use self::models::*; use self::diesel::prelude::*; fn main() { use diesel_demo::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); } }
実行する
サンプルデータをpostsテーブルに用意し、確認します。
insert into posts(title, body, published) values("test","おはよう",1); insert into posts(title, body, published) values("test2","こんにちは",0); insert into posts(title, body, published) values("test3","こんばんは",1); mysql> select * from posts; +----+-------+-----------------+-----------+ | id | title | body | published | +----+-------+-----------------+-----------+ | 1 | test | おはよう | 1 | | 2 | test2 | こんにちは | 0 | | 3 | test3 | こんばんは | 1 | +----+-------+-----------------+-----------+ 3 rows in set (0.01 sec)
docker-compose run --rm rust cargo run --bin show_posts => Displaying 2 posts test ----------- おはよう test3 ----------- こんばんは
実行結果を見るとpublished
がtrue(1)のものが表示されましたね。
これはposts.filter(published.eq(true))
でpublished
がtrueのものだけに絞り込んでいるからですね
この部分をfalseに置き換えるとpublished
が0のレコードが表示されます。
Displaying 1 posts test2 ----------- こんにちは
まとめ
diesel_cliでスキーマ管理が簡単にできますね!
今回は読み込みだけですがチュートリアルもあるので割と簡単にDB読み込みができました。