別のディレクトリにある Gemfile をsystemメソッドで "bundle check" する

タイトルの通りなのですが、普通にやると意図した動きしなかったのでメモ。

具体的な例

factory_bot が分かりやすいです。

具体的なコマンドは以下の通り。

$ git clone --depth 1 https://github.com/thoughtbot/factory_bot.git
$ cd factory_bot
$ bundle install
$ bundle exec ruby -e 'system({ "BUNDLE_GEMFILE" => "6.0.gemfile" }, "bundle check", chdir: "gemfiles/")'
Could not find zeitwerk-2.1.9 in any of the sources
Run `bundle install` to install missing gems.

動くコマンド

bundler経由で実行すると RUBYOPT="-r/usr/local/lib/ruby/2.7.0/bundler/setup" がセットされるので、これを上書きすると動く。

$ bundle exec ruby -e 'system({ "BUNDLE_GEMFILE" => "6.0.gemfile", "RUBYOPT" => ""}, "bundle check", chdir: "gemfiles/")'
The following gems are missing
 * zeitwerk (2.1.9)
 * childprocess (2.0.0)
 * aruba (0.14.11)
 * simplecov (0.17.0)
Install missing gems with `bundle install`

bundle install する前なので、正しく 6.0.gemfile を参照してる挙動になった。

追記

Bundler.with_clean_system を使う方法を Twitter で教えてもらった。

こっちの方がシンプルで、良いですね!

PostgreSQL 12でpg_hint_planをインストールする方法

brew install postgresql でインストールした postgres で pg_hint を使えるようにする方法が分からなかったのでメモ。

URL一覧

インストール方法

OSDN の v1.3.4 の rpm に含まれているのは Postgres 11 用らしく、 Postgres 12 で動くか不安だったので使うのは避けた。

そして、 v1.3.4 のソースも Postgres 11 用なのか、ビルドはエラーになった。

$ make
clang -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -Wno-unused-command-line-argument -O2  -I. -I./ -I/usr/local/include/postgresql/server -I/usr/local/include/postgresql/internal -I/usr/local/Cellar/icu4c/64.2/include -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -I/usr/local/opt/openssl@1.1/include -I/usr/local/opt/readline/include -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/libxml2   -c -o pg_hint_plan.o pg_hint_plan.c
pg_hint_plan.c:20:10: fatal error: 'nodes/relation.h' file not found
#include "nodes/relation.h"
         ^~~~~~~~~~~~~~~~~~
1 error generated.
make: *** [pg_hint_plan.o] Error 1

GitHubのmasterブランチ

GitHub からソースを取得し、 22a770c で make すると、今度は別のエラーが出た。

$ make
clang -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -Wno-unused-command-line-argument -O2  pg_hint_plan.o -L/usr/local/lib -L/usr/local/opt/openssl@1.1/lib -L/usr/local/opt/readline/lib  -Wl,-dead_strip_dylibs  -Wl,--build-id  -bundle -bundle_loader /usr/local/Cellar/postgresql/12.1/bin/postgres -o pg_hint_plan.so
ld: unknown option: --build-id
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [pg_hint_plan.so] Error 1

よく分からないけど --build-id のオプションがダメらしいので、消してみたら make と make install が実行できた。

$ git diff
diff --git a/Makefile b/Makefile
index de9f5c5..bde9cdc 100644
--- a/Makefile
+++ b/Makefile
@@ -31,7 +31,7 @@ TARSOURCES = Makefile *.c  *.h COPYRIGHT* \
        doc/* expected/*.out sql/*.sql sql/maskout.sh \
        data/data.csv input/*.source output/*.source SPECS/*.spec

-LDFLAGS+=-Wl,--build-id
+LDFLAGS+=-Wl

 installcheck: $(REGRESSION_EXPECTED)

これで pg_hint_plan は使えるようになったけど、これが正式なやり方かは謎。

銀座Railsで「Railsアプリの設計」というタイトルで発表しました #ginzarails

12/13(金)にあった銀座Rails#16で登壇しました。

スライド

speakerdeck.com

話したかったこと

  • 1〜3年目のRailsエンジニアに参考になる話がしたい
    • 7年やって私が覚えたことは、たぶん何かしら参考になるだろう
    • 来週の仕事ですぐ参考になる話がしたかった
  • 懇親会で他のシニアエンジニアの考えを聞きたかった

補足: なぜクラス内クラスを使うのか?

スライドの最後の方が説明不足だったので、ブログで補足を書いておきます。

👮‍♀️RuboCop の取締りは厳しい

RuboCop の標準ルールを守ろうとすると、以下を行うことになる。

  1. 行数が長くなると、変数を抽出する
  2. 変数代入によって AbcSize が増えるので、プライベートメソッドを抽出する
  3. メソッド追加によって ClassLength が増える

対策方法は2つある

  • Module を抽出する
  • Class を抽出する

しかし、Module はプライベートメソッドを使えない。(スライドを参照)

Class を抽出する

  • 普通にクラスを作る
  • 内部クラス(クラス内クラス)を作る

内部クラスだと僅かにですが使用箇所を制限できる。

例えば、スライド例で挙げた User::Ranking を他のクラス(例えば Blog )で使うのは避けるはず。

これにより User::Ranking を消すときに調べる範囲を減らして、消しやすくしている。

結論

このような思考過程で内部クラス(クラス内クラス)を利用したクラス設計を好んで使っています。

ActiveRecord::Base.unscope の呼び出しでネストが深くなっているのを浅くする

仕事で見かけたコードを汎用化した内容にして、個人ブログにメモっておく。*1

多段ネストを reverse して Proc でラップしたり、それを RSpec で検証する方法はブログ書いてないと忘れる自信がある。

モデルの定義

class User < ApplicationRecord
  default_scope { where(deleted_at: nil) }
end

class Article < ApplicationRecord
  default_scope { where.not(published_at: nil) }
end

元コード

User.unscoped do
  Article.unscoped do
      assert_equal 'SELECT "users".* FROM "users"', User.all.to_sql
      assert_equal 'SELECT "articles".* FROM "articles"', Article.all.to_sql
  end
end

書き換えたあと

module ScopeHelper
  def self.unscoped(models:, &block)
    models.reverse
      .inject(block) { |result, model| -> { model.unscoped(&result) } }
      .call
  end
end

ScopeHelper.unscoped(models: [User, Article]) do
  assert_equal 'SELECT "users".* FROM "users"', User.all.to_sql
  assert_equal 'SELECT "articles".* FROM "articles"', Article.all.to_sql
end

RSpecで書いたテスト

RSpec.describe ScopeHelper do
  describe '.unscoped' do
    it do
      expect(User).to receive(:unscoped).and_call_original
      block = -> {}
      expect(Article).to receive(:unscoped) do |&args|
        expect(args).to eq block
      end

      ScopeHelper.unscoped(models: [User, Article], &block)
    end
  end
end

*1:default_scopeとか絶対使わないけど、他に汎用化した例は思い浮かばなかった

2019年11月現在のRailsのissueに関するメモ #heiseirb

平成.rb #10Railsのissueに取り組むきっかけが分からない人向けのメモです。

個人的に気になっているissueについて簡単にまとめました。

注意)特に初心者向けとかではありません。

STIhas_secure_password の組合せで、親クラスでvalidationsを切り替えたい?

Trouble using has_secure_password validations attribute with STI models https://github.com/rails/rails/issues/37755

  • 再現コードなし
  • 登録者のコードは意図通りじゃなさそう
    • self.classClass を返すから常に true

scope + new でエラーが出る

raise_on_type_mismatch! in ActiveRecord::Associations::BelongsToAssociation https://github.com/rails/rails/issues/37752

  • 再現コードなし
  • コミッターの反応あり

ignored_columns + from で意図しないカラムが無視される

Ignored columns is ignoring virtual columns selected by .from https://github.com/rails/rails/issues/37745

  • 再現コードなし

through と polymorphic の組合せで外部キーがnilになる

Assigning with a through association and a self-ref polymorphic association leaves a nil foreign key https://github.com/rails/rails/issues/37758

  • 再現コードなし

request.variant の挙動が5.2と6.0で違う

Rails 6.0.0 chooses variants differently from 5.2 when variants are using different template engines https://github.com/rails/rails/issues/37021

  • 再現コードなし
  • リグレッション
    • 5.2 の挙動がバグっていた可能性もある
  • stale ラベル

enum で文字列を代入したらエラーになる

Revisiting the inconsistency in enums: raises ArgumentError when making an assignment with invalid value, but returns wrong results when querying https://github.com/rails/rails/issues/37630

  • 再現コードなし

accepts_nested_attributes_for でモデルを削除してもメモリ上に残る

Removing association with dependent: :destroy, through nested attributes leaves entity from intermediate table in memory https://github.com/rails/rails/issues/37649

  • 再現コードあり

DateTime#advance の挙動が5.2と6.0で違う?

DateTime#advance() expats integer params https://github.com/rails/rails/issues/37425

  • 再現コードなし

joins と pluck をあわせて使うと型が変わる

Unexpected Type Casting in ActiveRecord::Calculations#pluck https://github.com/rails/rails/issues/28044

  • 再現コードあり
  • 直すのは難しそう

テーブルない状態で order を呼ぶと5.2からエラー?

Calling "order" crashes when the model isn't represented by a database table https://github.com/rails/rails/issues/37741

  • 再現コードあり
  • 仕様かバグかの判断から必要そう

belongs_to と scope の組合せでsaveに失敗する

belongs_to with scope cause racord save fails https://github.com/rails/rails/issues/36990

  • 再現コードなし
  • stale ラベル

Rustでwebアプリを実装して勉強 その5 - テンプレートを使ってhtmlを返す

Rust初心者が勉強したことを記録する備忘録。

今日やった事

今日はRustやる時間が少ないので、handlebarsを使ってhtmlを返すところを少しだけ試した。

github.com

handlebars の使い方

actix-webのexamplesを参考にして、そのままコピペした。

// Handlebars uses a repository for the compiled templates. This object must be
// shared between the application threads, and is therefore passed to the
// Application Builder as an atomic reference-counted pointer.

サンプルコードのコメントに書いてあるように、Handlebars のインスタンスは各スレッドで共有するために web::Data を使う。

html を検証する

html を assert_eq! で検証するのは大変なので、部分一致で確認するように修正した。

- assert_eq!(test::read_body(resp), Bytes::from_static(b"Hello world!"));
+ let html = str::from_utf8(&test::read_body(resp))
+    .expect("Could not convert response to UTF8")
+     .to_string();
+ assert!(html.contains("<h1>Hello world!</h1>"));

& をつけたり、str や String の関係がよく分かってないが、とりあえずこんな感じで書くことで部分一致を検証できた。

次にやりたいこと

試したいことはだいたいできたので、次はコードを整理したい。

  • SQLに関する処理をModelに移す(もしくはRepositoryを作る)
  • テストコードを書く
  • jsonを返すように直す
  • テンプレートエンジンを使ってhtmlを返す
  • テスト環境の改善(コードのリファクタリングトランザクション周りの設定など)
  • Dockerで動くように直す
  • Herokuで動かす

Rustでwebアプリを実装して勉強 その4 - jsonを返すAPIの実装

Rust初心者が勉強したことを記録する備忘録。

今日やった事

今日やったことをプルリクにしておくと参照するのが楽なことに気づいた。

github.com

JSONシリアライズ/デシリアライズ

serde_json一択です。
引用: isucon7予選のアプリをRustに移植したから解説するね

という記事を見たので serde_json を使ってみた。

serde_json の使い方

jsonにしたいstructにSerialize, Deserializeをつける。

#[derive(Queryable, Serialize, Deserialize)]
pub struct Post {
    pub id: i32,
    pub title: String,
    pub body: String,
    pub published: bool,
}

あとは serde_json::to_string や serde_json::from_str を呼べばいい。

let post = Post { ...(略) };

// struct -> json
let json = serde_json::to_string(&post).unwrap();

// json -> struct
let json_post: Post = serde_json::from_str(&json).unwrap();

ルーティング

ルーティングは 前方一致 なので、 "/posts/{id}" より前に書く必要がある。

 cfg.route("/posts", web::get().to(posts::index))
+    .route("/posts/{id}.json", web::get().to(posts::show_json))
     .route("/posts/{id}", web::get().to(posts::show))

本当は content-type などを考慮して良い感じに書くべきなんだろうけど、今日は疲れたので雑に別の関数にした。

次にやりたいこと

  • SQLに関する処理をModelに移す(もしくはRepositoryを作る)
  • テストコードを書く
  • jsonを返すように直す
  • テンプレートエンジンを使ってhtmlを返す
  • テスト環境の改善(コードのリファクタリングトランザクション周りの設定など)
  • Dockerで動くように直す
  • Herokuで動かす