まさひlog

趣味で行っていることをのんびりとログとして残していきます

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レコードほどデータを入れています f:id:mshr_t:20210604230841p:plain

実装

ライブラリのリファレンスを参考に進めていこうと思います

必要ライブラリの記入

[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のコードを見ていて時間がかかってしまいました。。

他の機能も色々あるのでこれから試してみようと思います!

参考

github.com

github.com

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")

参考

cloud.google.com

Rubyに静的型付けSorbetを触ってみる

ついにRubyでも静的型付けができるようになりましたね。

Typescriptなど少し前から、静的型付けが熱いですがやっとRubyでも!
僕自身、業務上ではRubyを使っていて、動的型付けに悩まされることもあるので嬉しいです!

とはいえ、触ってみないとわからないため、今回はそのメモです。

今回はSorbetのPlaygroundで簡単に触ってみます。

sorbet.run

Sorbetとは?

  • Stripeが作成
  • Ruby用に設計された強力な型チェッカーライブラリ
  • IDEに対応
  • Gradual Type Checking

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を使って、どんな感じなのかを触ってみました。
まだ触り始めたばかりですが、型があることで変数に何が入ってくるのかわかりいいですね。
あと段階的に型に対応していけるのは嬉しいですね!

参考

Adopting Sorbet in an Existing Codebase · Sorbet

RustでMySQLを操作してみる(書き込み)

前回はRustでMySQLの読み込み操作について書きました。

mshr-t.hatenablog.jp

引き続き、書き込み操作について書いていきます。
今回も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を使用します。

今回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によるセットアップ

初めにdieselMySQLに接続するために.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

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;
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読み込みができました。