Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20191111前編)Active Recordモデルをprivateで封じ込める、心折れないRailsスキーマ管理、Railsセッションをクロスドメイン共有ほか

こんにちは、hachi8833です。Rails 6.0.1が先週リリースされましたね🎉。

Rails 6.0.1がリリース!修正を追ってみました

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

お知らせ: 週刊Railsウォッチ「第16回公開つっつき会」(無料)

第16回目公開つっつき会は、来週11月14日(木)19:30〜にBPS会議スペースにて開催されます。今回は月初ではありませんのでご注意ください。

週刊Railsウォッチの記事にいち早く触れられるチャンス!発言・質問も自由です。引き続き皆さまのお気軽なご参加をお待ちしております🙇。

Rails: 先週の改修(Rails公式ニュースより)

公式情報を中心に見繕いました。

マルチDBのマイグレーション後に同じデータベースに再接続するよう修正

標準的なマルチDBセットアップで、2番目のデータベースがレプリカでなく、独立したテーブルセットを持っている状態で以下の非常にシンプルなタスクがあり、rails db:migrate fooを実行したとする。

task foo: :environment do
  puts User.last # or some model with a table that only exists in the primary db
end

実際の振る舞い: establish_connectionがマイグレーションタスクごとに実行されるため、プライマリではなく直前のマイグレーション対象データベースに対してクエリが実行される。
期待される振る舞い: マイグレーションタスクがクリーンアップされ、実行後プライマリ・データベースに再接続される。
#37578より大意


つっつきボイス:「Rails 6のマルチDBがらみの修正ですね」「お、rakeタスクですか」「コネクションをoriginal_configに保存しておいて、終わったらそれを復元しているんですね↓」「コネクションが1つだったら起きなかった問題っぽい」「本番で知らずに切り替わってたらびっくりして目を疑っちゃいそう😇」「実際に使わないと見つけにくそうなバグですね☺️」

# activerecord/lib/active_record/railties/databases.rake#L81
  desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
  task migrate: :load_config do
+   original_config = ActiveRecord::Base.connection_config
    ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
      ActiveRecord::Base.establish_connection(db_config)
      ActiveRecord::Tasks::DatabaseTasks.migrate
    end
    db_namespace["_dump"].invoke
+ ensure
+   ActiveRecord::Base.establish_connection(original_config)
  end

ローカルキャッシュを改変するときのバグを修正

このテストケースがバグをある意味正しく示してくれると思う。手短に言うと、戻り値の改変からは既に保護されていたにもかかわらず、元の値の改変から保護されていなかった。
同PRより大意

# 同PRより
my_string = "foo"
cache.write('key', my_string)
my_string << "bar"
cache.read('key') # => "foobar"

つっつきボイス:「上のコード例を見る方が早いと思うんですけど、キャッシュの値に入れた元の値を改変するとキャッシュの値まで変わっちゃってるという😳」「これはアカンやつ〜😆」「dupしないでそのまんまキャッシュに入っちゃってたか😇」「それを防ぐためにdup_value!を追加したんですね」

# activesupport/lib/active_support/cache/strategy/local_cache.rb#L60
-         def write_entry(key, value, **options)
-           @data[key] = value
+         def write_entry(key, entry, **options)
+           entry.dup_value!
+           @data[key] = entry
            true
          end

「これに限らずhashやarrayで割とよくあるバグですね☺️」「これはあるある」「単純に@data[key] = valueしたら、valueが変わるとキャッシュの値まで変わっちゃう😇」「誰も書き換えなければ問題ないんですけど、キャッシュだから書き換えあるでしょうし😆」「よけてたはずなのにどして?ってなったり😆」「ぱっと見正しそうなだけに見落としそう😅」

「まあ誰しもやりそうなバグですから☺️」「でもやったらアカン😆」「キャッシュに入れたmy_stringを後生大事に使い回すのがそもそもよくなかったという考え方もあるかも😆」「修正でdup入ったから微妙に遅くなるでしょうね😆」

「#37587の場合は同じKeyに対してread / write処理を書いて、かつ元のオブジェクトが参照できるときにしか発生しないので、『同一リクエスト内で同じオブジェクトをwrite / readした』『クラス変数などのリクエストをまたいでメモリにデータを保持する変数でwrite / readした』とかのケースぐらいで、割とレアな気はしますね☺️」「おぉ」


「こういうバグが起きにくい言語仕様ってあるのかなって、ついそっちを考えちゃいます😅」「全部値渡しにしたらデカいオブジェクトのコピーが半端ないコストになりますよ😆」「参照で渡したいときとコピーしたいときとありますからね〜☺️」「C言語知ってる人にならRubyは基本的にポインタ渡しだよって説明できますけど」「若い人だとポインタ知らなさそう😆」

参考: ポインタ (プログラミング) - Wikipedia

なおdup_value!はActiveSupport::Cache::Entryにありますが、なぜかapi.rubyonrails.orgで出てこなかったのでAPIdockを貼ります。

参考: dup_value! (ActiveSupport::Cache::Entry) - APIdock

インラインジョブを別スレッドで実行できるようになった

# activejob/lib/active_job/queue_adapters/inline_adapter.rb#L13
    class InlineAdapter
      def enqueue(job) #:nodoc:
-       Base.execute(job.serialize)
+       Thread.new { Base.execute(job.serialize) }.join
      end

      def enqueue_at(*) #:nodoc:
        raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at https://guides.rubyonrails.org/active_job_basics.html"
      end
    end

つっつきボイス:「インラインジョブを別スレッドで実行できるようにしたそうです」「今までは別スレッドにできなかったと😳」「Thread.newでジョブを実行してからjoinしてますね」「enqueueだし、もしかするとjob.serializeが重いのかも🤔」「joinするということはジョブの完了を待つってことなのかな?」「issueの方を見るとよさそう↓」

「これはThread.newした中でexecuteさせることで直ちにenqueueした処理を解放させて、次のenqueue 待ちをなるべくゼロにしたい、ということだと思います」「おぉ」「これはOSの割り込みハンドラによるコンテキストスイッチ実装とかでもよくある実装方針で、割り込みハンドラはなるべく限界まで小さくする、というやつですね☺️(割り込みハンドラの処理中は他の割り込みハンドラを受けられなくなってしまうので、その時間は最小限にするために、割り込みハンドラでは即queueに処理イベントを積むだけ積んでハンドラを脱出する)」「なるほど!」「多分、micro jobが大量に実行されるようなユースケースになってくると、ジョブのスループットに影響が出てくるんだと思われます」

「あ、issueに例のCurrentAttributes↓が登場してます😆」「憎っくきCurrentAttributes😆」「これってグローバルステートだからスレッドからこちょこちょするときに気を付けないといけないんでしょうね☺️」「スレッド周り難しくてよくわかんないけど、上の修正は何となくworkaroundっぽい雰囲気🤔」「この修正で切り抜けられるならまあいいのかなと☺️」「スレッド生成してもコストはそんなに変わらなさそうではある🤔」

Railsの`CurrentAttributes`は有害である(翻訳)

「上はDHHが入れたCurrentAttributesに反対してた人の記事を翻訳したもので、CurrentAttributesはやりすぎだという主張ですね」「そもそもグローバルステートですし😆」「記事の人はどちらかというと設計として好きになれないみたいです」「わかる😆」「一応CurrentAttributesにはスレッドローカルな変数もあるみたいですけど、それが回り回って今回のissueにつながったんだとしたら何となくわかる気がする☺️」「グローバルステートならスレッドセーフであって欲しいですよね」

「こういうCurrentAttributes的な機能って、わかってて使う人にはとっても有用なんですよ😆」「あ〜それはある意味難しい問題😅」「そしてよくわかってない人が飛びつくと詰む、みたいな害の方が大きくなったりしがち😆」「共有情報をスレッドに乗せるみたいなコードをJavaで見たことはあるので、CurrentAttributesがまったくナンセンスということはないんじゃないかとは思いますね☺️」「使う人を選ぶ機能😆」

GitHub Actionsに対応

# .github/workflows/rubocop.yml
name: RuboCop
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Set up Ruby 2.6
      uses: actions/setup-ruby@v1
      with:
        ruby-version: 2.6.x
    - name: Install required package
      run: |
        sudo apt-get install libmysqlclient-dev libpq-dev libsqlite3-dev libncurses5-dev
+   - name: Cache gems
+     uses: actions/cache@preview
+     with:
+       path: vendor/bundle
+       key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
+       restore-keys: |
+         ${{ runner.os }}-gem-
    - name: Build and run RuboCop
      run: |
+       bundle config path vendor/bundle
        bundle install --jobs 4 --retry 3
        bundle exec rubocop --parallel

つっつきボイス:「これはGitHub向けの改修ですね」「gemのキャッシュと最新のRuboCopに対応」「GitHubがRailsでできてるのは有名ですよね😋」「GitHub Actionsはウォッチでも何度か取り上げましたけど(ウォッチ20190925)結構期待できそう😍」

rubocop --parallelって知らなかった😳」「お、これは欲しいかも😋」「Rubocopちゃんで大量修正が必要なプロジェクトなんかでも、1ファイルごとにやれればいいからparallelにすれば速くなりますね😍」「どうせRSpecの方が断然遅いからまあ別にって感じですけど🤣」「🤣」

参考: rubocop/basic_usage.md at master · rubocop-hq/rubocop
参考: Rubocop を使っているアナタがするべき2つのこと - Qiita

ActiveStorage blobからrequire_dependencyを排除

# activestorage/app/models/active_storage/blob.rb#L17
class ActiveStorage::Blob < ActiveRecord::Base
- require_dependency "active_storage/blob/analyzable"
- require_dependency "active_storage/blob/identifiable"
- require_dependency "active_storage/blob/representable"
+ unless Rails.autoloaders.zeitwerk_enabled?
+   require_dependency "active_storage/blob/analyzable"
+   require_dependency "active_storage/blob/identifiable"
+   require_dependency "active_storage/blob/representable"
  end

つっつきボイス:「ZeitwerkがRails 6で殺したrequire_dependencyがActiveStorage::Blobにちょっぴり残ってたので排除したという小さい修正です」「サーチアンドデストロイ💀」「一応Zeitwerkなしでもやれるようにしないといけませんし☺️」「これで殺戮完了したのかな?」「どうでしょう😆」

参考: 定数の自動読み込みと再読み込み (Classic) - Rails ガイド

require_dependencyはRailsで独自に作ったメソッドでしたか〜」「以前のウォッチでも名前空間地獄の話題でrequire_dependencyの話になりましたね(ウォッチ20181022)」「Zeitwerkになってrequire_dependencyが不要になったというかむしろ完全排除される方向に」

Rails 6 Beta2時点のZeitwerk情報(要訳)

ActiveRecord::Baseから不要なrequireを削除

# activerecord/lib/active_record/base.rb#L
-require "yaml"
require "active_support/benchmarkable"
require "active_support/dependencies"
require "active_support/descendants_tracker"
require "active_support/time"
-require "active_support/core_ext/module/attribute_accessors"
-require "active_support/core_ext/array/extract_options"
-require "active_support/core_ext/hash/deep_merge"
-require "active_support/core_ext/hash/slice"
-require "active_support/core_ext/string/behavior"
-require "active_support/core_ext/kernel/singleton_class"
-require "active_support/core_ext/module/introspection"
require "active_support/core_ext/class/subclasses"
require "active_record/attribute_decorators"
require "active_record/define_callbacks"
require "active_record/log_subscriber"
require "active_record/explain_subscriber"
require "active_record/relation/delegation"
require "active_record/attributes"
require "active_record/type_caster"
require "active_record/database_configurations"

つっつきボイス:「Active Record::Baseに要らないrequireが結構残ってたのが削除されてました」「Baseから消えるってなかなかスゴい😆」「core extensionあたりのrequireはいつの間にか冗長になってたんだろうな〜」「あ、requireしなくてもautoloadでモジュールが自動読み込みされるようになってたのか☺️」「なるほどね!」

# b2c9ce3より
# activerecord/lib/active_record.rb#58
    autoload :Base
    autoload :Callbacks
+   autoload :Core
    autoload :CounterCache
    autoload :DynamicMatchers
    autoload :DynamicFinderMatch

参考: module function Kernel.#autoload (Ruby 2.6.0)

requireって雑に増えていきがちだからコワくてなかなか消せないのが大変😆」「いつかは消さないといけないんでしょうけど」「かぶっててもfalseが返るだけで害はないのでなかなか消されなさそう😆」「このたびfalseになることが確定したんでしょうね☺️」

Rails

Active Recordモデルをprivateにして大人しくさせてやった(Ruby Weeklyより)


つっつきボイス:「tameは『飼いならす』ですね😆」「モチベの説明が長いので後で追ってみます」

Account.public_methods.sizeでメソッドが685個出てきた😆」「むちゃくちゃや😆」「これは死にたくなる😇」

「これがコンセプトみたいです↓」「むむむ...?Accountクラスを複数形のAccountsモジュールにして、Accounts::Modelnewしてからテーブル名だけ指定し、そしてprivate_constant :Modelでモデルをprivateにした...だと..?うはぁ〜!」「ウケた😆、喜びですか驚きですかあきれてますか?」「いや〜、これは面白い!!🎉」

# 同記事より
module Accounts
  # Some important stuff up here, which will get to in a bit

  class Model < ApplicationRecord
    self.table_name = 'accounts'
  end
  private_constant :Model

  # Some important stuff down here, which will get to in a bit
end

「これは最近ActiveModel絡みでよく話している、永続化層切り離しですね〜❤️」「おぉ」「上のように書くことでActiveRecordのメソッドのほとんどを殺すことができる: そして以下みたいに欲しいメソッドだけself.fetchとかself.createみたいに書いて単にモデルにdelegateして、かつそのモデルを返している」「いわゆる委譲ですね」

ModelモデルはprivateなのでAccountsモジュールの中なら見えるけど外部からはシャットアウトされる」「Accounts::Modelで外からいじるんじゃねーぞ、と😆」「これ確かに面白〜い!😋」「Rails wayじゃありませんけどね😆」

# 同記事より
# app/models/accounts.rb
module Accounts
  def self.fetch(id:)
    Model.find(id)
  end

  def self.create(name:)
    Model.create!(name: name)
  end

  class Model < ApplicationRecord
    self.table_name = 'accounts'
  end
  private_constant :Model
end

「オレが委譲で許した以外の方法でモデルにアクセスするなよと😆」「返すものがモデルじゃなくてリレーションになるとまたちょっと微妙な話になるんですけど☺️」

「その発展型がこれか↓」「お、最後のAccountは普通のPORO(Pure Old Ruby Object)で、fetchcreateが今度はModelじゃなくてカスタムのAccountクラスを返すようにしたと、ほほぉ〜これはたぶんwhereみたいなリレーションを相手にしたくないと言ってそう😋」

# 同記事より
# app/models/accounts.rb
module Accounts

  # --- Public APIs
  def self.fetch(id:)
    db_object = Model.find(id)
    Account.new(
      id: db_object.id,
      name: db_object.name,
    )
  end

  def self.create(name:)
    db_object = Model.create!(name: name)
    Account.new(
      id: db_object.id,
      name: db_object.name,
    )
  end

  # --- Private ActiveRecord model
  class Model < ApplicationRecord
    self.table_name = 'accounts'
  end
  private_constant :Model

  # --- Entity for the outside world
  class Account
    attr_reader :id, :name

    def initialize(id:, name:)
      @id = id
      @name = name
    end
  end
end

「さらにトランザクションもこの形↓でやってるし😳」「業務コードらしくなってきた😋」

module Accounts
  def add_seat(id:)
    Model.transaction do
      db_object = Model.find(id)
      db_object.number_of_licenses += 1
      db.object.save!

      if db_object.number_of_licenses == 5
        SalesNotification.create!(account_id: id)
      end
    end
  end

  class SalesNotification
    belongs_to :account
  end
  private_constant :SalesNotification

  # Rest of implementation...
end

「まあこのパターンを既存のRailsアプリでいきなりやるのは無理あるのでそれはおいとくとして、今後はこういうふうに作ってみいやという感じかな〜😆」「自分には、ある意味カプセル化の基本に立ち返ったように見えますね☺️」「そう!ちゃんとカプセル化してる」「これを実際にやるかどうかは別としても、ちょっと新鮮ですね😍」

「一応記事の末尾にもいろいろ書いてますね: これはRailsのデフォルトのパターンじゃないし、どのActive Recordモデルに適用できるとも限らないと」「そりゃそうだ😆」「機が熟すまでこの設計には飛びつかない方がいいということみたいですね☺️」「最初からこう書いていれば無駄なメソッドが600個も生えてこなくて済むでしょうけど😆」

「元々Active Recordが継承でやるように作られちゃってるからなんでしょうけど😢」「まあそれはあるかも☺️」「この記事を書いた人は、たぶんデータベースに直接触らせたくないマン😆」

「このパターンでやれそうな例として『AccountUserをいつも同時に変更しているなら、同じモジュールに入れるべきじゃね?』と思えたときが挙げられてますね」「あ〜わかる!離れているモデルをいつも同時に扱ってわけわからなくなるぐらいだったらモジュールに閉じ込めてprivateにしちまえと😆」

「いわゆるPoEAA↓のActiveRecordパターンからきちんとビジネスオブジェクトを切り離す前段階としては悪くなさそうですね: テストコードがあればとりあえず不用意にActiveRecordの呼ばれたくないメソッドを隠蔽できるのはまあ悪くないのかも?(つらそうだけど)」

「それにしても面白いパターンだわ〜😋」「カプセル化としてはとてもキレイではある☺️」「これが実際にうまく当てはまる場合って何だろう?ん〜とん〜と🤔」(以下延々)


以下は記事冒頭の「モチベーション」より:

システムが大きくなったらカプセル化を強化すべきである。私たちはマイクロサービスや何ちゃらRailsエンジンでやりたいのではなく、Rubyの基本機能を少々用いることで実現する。
(中略)
そういうわけでモデリングにおいて防衛的なアプローチを始めた。つまりサポートできるものだけをpublicにしようということだ。これによってモデルの表面積が小さくなってサポートしやすくなるし、用途が絞られることで内部変更もしやすくなる。
(中略)
最後に、ROMSequelのようにData Accessパターンでこれに近いことをやれるライブラリはいろいろあるものの、それらに完全に乗り換えるのは簡単ではない。おそらく皆さんのアプリは最初からActive Recordを使っているだろう。本記事では、そうした技術への乗り換えが困難なまでに育ったRailsアプリを前提としている。
「データベースはインターフェイスじゃないんだけど!」とつぶやいてる人にはもしかするとこのパターンが向いているかもしれない。
同記事より大意

Railsのセッションをクロスドメインで共有(RubyFlowより)


つっつきボイス:「1本目はcookieとセッションの基本的な解説で、2本目が本題のようです」「クロスドメインでセッションを共有ぅ〜?」「無茶な😆」

「どうやらこの人たちはapp.kittens.iodev.kittens.ioという2つのドメインで認証を共有したいらしいです」「なるほどそっちですか😆」「cookieの仕様でこういうのってやれるんだったかな?🤔」「この記事ではセッションストアをRedisにしてるので、それならやれそう☺️」

「お、このconfig↓でdomain: :allにするのがポイントらしい😳」「これでドメインが変わってもcookieをよしなに扱えるってこと?」「へぇ〜😳」

# 同記事2より
Rails.application.config.session_store :redis_session_store,
  key: '_kittens_session',
  serializer: :json,
  domain: :all,
  redis: {
    expire_after: 1.week,
    key_prefix: 'kittens:session:',
    url: ENV['REDIS_SESSIONS_URL']
}

「この記事みたいにサブドメインの違う複数サーバーでセッションを共有したいことって結構あるんでしょうか?」「サブドメインがwwwとかliveとかloginみたいに分かれてて、loginで認証したら他のサブドメインも見られるようにする、なんてのは普通にやりますね☺️」「SSO↓でやると大げさになっちゃうんでしょうか?」「まあそのためだけにSSOは使わないでしょう😆」「これでやれるならサブドメインを気軽に作れますし☺️」「昔セッション共有やるべきかどうかについて議論になった気がするけど思い出せない😆」

参考: シングルサインオン - Wikipedia

追いかけボイス: 「web書くならにcookieのdomain指定は把握しておいてほしいなあ...大昔にこんなのを書いていました↓」「おぉありがとうございます😂」「まあ今はさらに色々ありますが😆」

社内勉強会でSOP (Same Origin Policy) の話をしました

Active StorageはRails 6でどう変わったか


つっつきボイス:「記事はActive StorageがRails 6でどう変わったかというまとめで、このSaeloun Blogは最近グロスで翻訳の許可ももらえました😋」


「お、mini_magickが置き換わった?」「そういえばウォッチでもimage_processingというgem↓に置き換わったのを扱ってた覚えが(ウォッチ20180511)」「画像の向きも自動で修正してくれるとか、image_processingよさげ😍」


「次は画像のvariantのサポート」「variantはサイズ違いの画像ですね」「carrierwaveとかでやってたのがActive Storageでもできるようになった☺️」

carrerwave↓のgemspecを見るとimage_processingが入ってますね😋。mini_magickもまだありますが。


「最後がhas_many_attached」「これだけで書けるのはアツい❤️」「そういえばRails 6で挙動が変更されたんでした(ウォッチ20190729)」「前はattachしてupdateすると追加されてたのが、Rails 6で更新されるようになって他と挙動を合わせたと」「countの結果↓違う〜😨」「breaking changeなのでオプションで選べるようになったんでした」

# 同記事より
# Rails 5まで
blog = ActiveStorage::Blob.create_after_upload!(filename: "updated_pic.jpg")
user.update(images: [blog])

user.images.count
=> 2

# Rails 6
blog = ActiveStorage::Blob.create_after_upload!(filename: "updated_pic.jpg")
user.update(images: [blog])

user.images.count
=> 1

心が折れないRailsスキーマ管理


つっつきボイス:「Railsスキーマ管理で心が折れないための方法😆」「冒頭で早速例の『マイグレーションでActive Recordモデルを参照するな』が出てきてますね」「morimorihogeさんも口を酸っぱくして言ってるヤツ(ウォッチ20190415)」「babaさんも昔記事書いてました↓」

[Rails 3] 失敗しないmigrationを書こう

「次は使い捨てのスクリプトでデータをインポート」「one-offは『使い捨ての』という意味ですね」「捨てスクリプトを全環境で実行したらもうgitにも登録するなと」「それわかる〜😋」「基本的には残す意味ないヤツ😆」「one-off migrationスクリプトを歴史としてコミット履歴に残すというのは別に悪くはない気もしますね: Wikiとかに書いてもいいんだけど『いつのコードで動かすことを想定していたか』を明らかにするという点ではコミットに挟まっててrevertした履歴があるというのも歴史管理としては一つの戦略だとは思う(これがベストだとは思いませんが)」「おぉ」

「次はどの環境のスキーマを『正』にするかみたいな話」「productionが正に決まっとる😆」「病欠でいない人がスクリプトをローカルで走らせてなくて、しかもスクリプトがもう消されたという状況になったら、production->staging->developmentの順で最新にすると」「考えたくない状況😅」「むか〜しスキーマのインデックス周りが環境ごとにちょっぴりずれてて修復したの思い出した😭」「マイグレーションの定番を押さえるのによさそうな記事ですね😋」

見出しより:

  • マイグレーションはスキーマだけを変更する
  • seedやデータインポートを使い捨てスクリプトでやる
  • pruneを徹底して環境を同期する
  • おまけ: いらないマイグレーションファイルを消す
  • 上のやり方から離れるべき場合

その他Rails


前編は以上です。

バックナンバー(2019年度第4四半期)

週刊Railsウォッチ(20191106後編)holiday_japan gemで日本の祝日判定、小さい関数が有害になるとき、Gitブランチのファジー検索ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。