satoryuの日記

忘れっぽいから覚えてるうちに書いておかないと。

Railsでウィザード形式のフォームを実装する時はテーブルを分けよう

f:id:satoryu:20190811081742j:plain unsplash-logoEstée Janssens

ユーザーへの入力を便利にしようと、ウィザード形式を採用しようと思って、試しに雑に作ってみた時にハマってしまった。

実際のコードは晒せないので、サンプルとして以下のような状況を考える。

例えば、プロフィールサービスを作っていて、ユーザー登録後にユーザーのニックネームやアバター画像、TwitterFacebookなどSNSのアカウント情報を入力させたくて、ウィザード形式を採用しようとしている。とする。

ここで、プロフィール入力を2ステップで構成されるウィザード形式にしたい。

  1. ニックネームと誕生日を入力
  2. SNSのプロフィールのURL

また、各プロフィールは必須入力としたい。

当初はウィザード形式は考慮していない実装だったので、ユーザー登録後に入力する情報は、いずれもユーザーのプロフィールということでprofilesテーブルに登録する。 テーブルのスキーマは以下のようになる。

ActiveRecord::Schema.define(version: 2019_08_11_000716) do
  create_table "profiles", force: :cascade do |t|
    t.string "nickname"
    t.date "birthday"
    t.string "twitter_profile_url"
    t.string "facebook_profile_url"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["user_id"], name: "index_profiles_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "email"
    t.string "password_digest"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["email"], name: "index_users_on_email", unique: true
  end
end

良くない実装

必須入力の項目のチェックするために、Profileモデルにバリデーションを設定する。

class Profile < ApplicationRecord
  validates :nickname, presence: true
  validates :birthday, presence: true
  validates :twitter_profile_url, presence: true
  validates :facebook_profile_url, presence: true
end

これだけだと、ウィザードの最初のステップでニックネームと誕生日を入力し、その時点の情報をprofilesに保存する時にその後に入力するtwitter_profile_urlなどのバリデーションによって止められてしまう。 validatesメソッドのonオプションで、そのバリデーションを有効にするタイミングを制御することができるので、それを使うことで一応は解決できそうではある。

class Profile < ApplicationRecord
  validates :nickname, presence: true, on: [:step1, :step2]
  validates :birthday, presence: true, on: [:step1, :step2]
  validates :twitter_profile_url, presence: true, on: :step2
  validates :facebook_profile_url, presence: true, on: :step2
end

このようにしておけば、モデルを保存する時に、save(context: :step1) などとすればステップごとに有効にしたいバリデーションを指定できる。

問題点

on オプションでバリデーションを追加すると、指定されたコンテキストでしかバリデーションが実行されないので、通常のsaveやupdate時にも有効にさせたい場合との区別が難しい。 このウィザード以外にProfileを保存や更新する場合に、必須入力であるということがチェックされない。チェックするにはコンテキストを指定する必要がある。

良さそうな実装

profilesテーブルを分割する。 そもそもウィザード形式で分ける時に、各ステップでの入力項目には意味がある単位にわかれているはず。そうでなければ、何の入力の助けにもならない。

ということで、ニックネームと誕生日、TwitterFacebookのURLとそれぞれの組でテーブルを分ける。

ActiveRecord::Schema.define(version: 2019_08_11_134450) do
  create_table "profiles", force: :cascade do |t|
    t.string "nickname"
    t.date "birthday"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["user_id"], name: "index_profiles_on_user_id"
  end

  create_table "social_profiles", force: :cascade do |t|
    t.string "twitter_profile_url"
    t.string "facebook_profile_url"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["user_id"], name: "index_social_profiles_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "email"
    t.string "password_digest"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["email"], name: "index_users_on_email", unique: true
  end
end

当然だが、こうするとバリデーションは普通の記述になる。

class Profile < ApplicationRecord
  belongs_to :user

  validates :nickname, presence: true
  validates :birthday, presence: true
end

class SocialProfile < ApplicationRecord
  belongs_to :user

  validates :twitter_profile_url, presence: true
  validates :facebook_profile_url, presence: true
end

おわりに

ウィザード形式を採用する場合に、テーブルを意味のある単位に分割することでバリデーションが複雑にならずに済む。 データの設計がコードに影響することがわかる例だと思う。

分割することで悪い影響はまったく無いのだろうか。 1つあるとすれば、分割されたテーブルの情報を使ってユーザーを検索する場合にJOINするコストが発生することくらいではないだろうか。 例えば、上の例で、social_profilesテーブルのtwitter_profile_urlprofilesテーブルのnameusersテーブルのSELECT文のWHERE句で使う場合だ。 2つのテーブルくらいなら大丈夫そうだが、増えていった場合はパフォーマンスの影響も考慮した分割の方法を検討すると良いのかもしれない。

また、既に稼働しているサービスの場合、分割に際してデータ移行が必要となる。

参考

2年前くらいに読んだ「システム設計の原則」の中で似たようなことをやっていたのを思い出したのがきっかけ。 あらためてその部分を読もうと思ったのだけど、失くしてしまったのか、後輩にあげてしまったようなので、また買うかな…

38歳になりました&今年上期を振り返ってみた。

f:id:satoryu:20190728144415j:plain
Photo by Lidya Nada on Unsplash

本日で38歳になりました。

37歳のあいだも色々ありました。 色々な方に支えられての1年で、様々なことを経験させていただいており、周囲の方々への感謝の気持ちでいっぱいであります。

37歳の振り返りではありませんが、ちょうど良い頃合いなので、上半期の振り返りをしてみました。

TL;DR

  • 38歳になりました。
  • イベントで登壇したり、転職したり、色々ありました。
  • 最後の重要な連絡事項はこちら
続きを読む

Google AssistantのバックエンドをAzure WebAppで作った。

Google Homeで音声を入力とした何かを今後作るかもしれないので、試しにGoogle Assistantを作るためのActions on GoogleとDialogflowを使った開発をしてみた。

初めて触れる技術のことなので、アプリケーションのネタはとてもシンプルにこれまで作ってきた駄洒落の自動生成にした。 で、実際にできたものがこちら。

www.youtube.com

バックエンドを、Firebase Functionsで作ろうと思ったのだが、無料版だと外部へのネットワーク接続ができないため諦めた。バックエンドは、Webhookの呼び出しを受けられれば何でも良いので、今回は普段から慣れているAzure WebAppを使うことにした。本当は、Azure Functionsにしたかったのだが、npmパッケージのactions-on-googleがサポートしていなかったので、WebApp上で動くnode.jsアプリケーションとして作った。

ソースコードは、こちら。

github.com

ユーザーの認証や、リソースへのアクセス権限を取得したりできるようなので、できることは色々ありそう。

インスタンス名を取り締まるためのRuboCop拡張をリリースしました。 #rubocop

インスタンス名を取り締まるためのRuboCop拡張 rubocop-instance_variable_name をリリースしました。

github.com

何ができるの?

この拡張がやっているのは、とてもシンプルなことで、インスタンス変数名の長さが一定以下(デフォルトだと2)のインスタンス変数に警告を出します。

@ms = 'foo' # bad code

@message = 'foo' # good code

作った動機

最近、他人の書いたコードをレビューしたり、他人の書いたコードを修正する機会が増えてきました。 その中で、いつも悩まされるのが変数名がわかりづらい時でした。 特に、インスタンス変数名は重要だと思います。 メソッド名やattr_readerなどで定義されるゲッターが分かりづらかったり、複雑なコードだとしても、変数名が何を表しているのかがわかると読み解くことができます。 特にクラス変数名は重要です。

読みづらさの中で最も辛いのは、略語を使ったものです。 普段使わない単語を使っているのも読みづらいのですが、英和辞典やググれば理解することはできます。 ですが、略語になると、それを推測するのが非常に難しくなります。

ということで、インスタンス変数名の長さに注目し、受け入れられない長さの変数名を指摘するRuboCop拡張を作りました。

是非ご活用ください。 また、何か不具合等ございましたら、Issueを作っていただけると助かります。

「デザイン思考の先を行くもの」を読んだ。

デザイン思考の先を行くもの

デザイン思考の先を行くもの

最近身近に「デザイン思考」という言葉をよく聞くようになった。 言葉自体は以前から知っているが、一冊本を読んだくらいで、仕事でやったことは無い。

ということで、どんなものなのか知るためにとりあえず一冊読んでみた。 208ページある本だが、見開き2ページの片側のみに文章が書いてあり、文量もそれほど多くないのでサラッと読めた。

この本では、デザイン思考はPDCAと類似するものとして扱っている。 計画(Plan)の部分が、デザイン思考では課題からアイデアを出すまでのプロセスに相当するものとしている。 そのため、デザイン思考だけでまったく新しい何かが生まれることはないと注意している。 後半では、アイデアの作り方について書かれていて、参考になりそうだった。

軽く読める本だったが、知りたかったデザイン思考について、デザイン思考の実例については詳しく触れられていなかった。 単にプロセスだけを見て、PDCAと対応づけられるから同じものとして扱ってしまうのは少しやりすぎなのではないだろうか。 自分がデザイン思考について知っているわけではないが、何かしらプラクティスがあるのではないだろうかと思っている。 まだ1冊目なので、こういう見方もあるのだろうなというくらいでとどめておくことにした。

次に読みたい

なるべく原典に近いものを読みたいので、 デザイン思考ブーム(?)の発端になった「デザイン思考が世界を変える」を次は読んでみようかな。

デザイン思考が世界を変える (ハヤカワ・ノンフィクション文庫)

デザイン思考が世界を変える (ハヤカワ・ノンフィクション文庫)

Butterfly Effect #1 に行ってきた。 #バタエフ

Butterfly Effect というイベントが始まる、ということで、初回ゲストがチームメンバーの @TAKAKING22だったので行ってきた。

butterfly-effect.connpass.com

Butterfly Effectって何?

butterfly-effect.connpass.com

イベントページより抜粋すると、

「すごい人が話を聞きたい人の話はすごいはず」という発想で 毎回違ったゲストを呼んでパネルディスカッションをするというイベント。

ということで、懐かしのテレフォンショッキング形式でゲストを回していくイベント。 確かに、紹介してもらった方がいい話をしてくれる人が来るのはかなり保証できそうだし、運営が楽できる

運営として面白いのは、ゲストへの謝礼が当日Kyashで集められたお金(通称、投げ銭)が支払われるということ。 この集まった額って、どこかで公開されてしまったりするのかな?

当日の様子

会場に入るとウェルカムドリンクをいただき、また今回は石井食品さん提供のミートボールとハンバーグが振る舞われた。

はじめはネットワーキングから始まり、参加者同士で軽くドリンクを頂きながら雑談。 あとでの会場アンケートでわかったのだけど、エンジニアだけでなく、テスト関係の方々やマネージャーをしている人たちもけっこう居た。 参加者層は、ゲストによるのかな。

予めファシリテーターの蜂須賀さんが質問を用意もしているが、当日会場に来ている参加者からもsli.doを使って、質問が集められていた。 昨日集まった質問も下記から見れる。 app.sli.do

どんな雰囲気だったのかは、トギャッターにまとまってる。 togetter.com

感想

まぁ、いつも通りの@TAKAKING22を見てきた感ある。 ゲストのようで、途中から少しずつモデレートしていく感じで、入場から会場の場づくりまでが設計されているようだった*1

チーム転職の話は皆さん興味あったみたい。 特に辞めた理由は、みなさんネガティブなものを想像していたようなので、世間一般の退職理由ってそういうものなのかというのを察した。

途中自分も少しだけ引っ張り出されて、チームメンバーからはどう見えるのか聞かれた。

昨日言ったように、みなさんがイベントで見てるように何も普段と変わらない。 最後に次回ゲストから言われたように「意外とチャラくない」のである。 マネージメントの話とか聞くとわかるように、チームの成果のために動く人なので、一緒に仕事しやすいし、楽しい。

次回ゲストは?

昨年に少しだけ一緒に仕事をした繋がりのきょんさん! 日程はこれから調整らしいが、名古屋から来てくれるそうなので参加しよー

おまけ

石井食品さんのミートボールとハンバーグ、マンゴープリンはどれも美味しかった!

*1:というか台本作りを前日に見てた。

7月からデンソーにジョインしましたのお知らせ

タイトルにありますように、デンソーの人として7月より働いております。

今は聖地横浜アリーナ近くの新横浜オフィスでソフトウェアエンジニアとして勤務しています。

経緯

今年1月末にチームでFA宣言を公表し、それに反応していただいたのがきっかけでした。

takaking22.com

都内ではどうやら認知度がまだまだ低いようですが、もともと名古屋で生活していた時期があったので、デンソーという名前は知っていました。 ウェブサービスと連携したMaaS開発を本気で取り組んでいて、新横浜をその拠点として一昨年あたりから取り組んでいるところ、お声がけいただきました。 個人的にも、ウェブだけでなくモノと繋がったサービスに携わって、社会に貢献できればいいな、と思っていたのでとても良い機会をいただけました。

ということで、引き続きよろしくお願いいたします。

また、前職の退職祝い並びに入社祝いを受け付けておりますのでお気軽に送りつけてください。 お祝いの品のラインナップは以下のリストをご活用ください。

www.amazon.jp