Railsでウィザード形式のフォームを実装する時はテーブルを分けよう
ユーザーへの入力を便利にしようと、ウィザード形式を採用しようと思って、試しに雑に作ってみた時にハマってしまった。
例
実際のコードは晒せないので、サンプルとして以下のような状況を考える。
例えば、プロフィールサービスを作っていて、ユーザー登録後にユーザーのニックネームやアバター画像、TwitterやFacebookなどSNSのアカウント情報を入力させたくて、ウィザード形式を採用しようとしている。とする。
ここで、プロフィール入力を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
テーブルを分割する。
そもそもウィザード形式で分ける時に、各ステップでの入力項目には意味がある単位にわかれているはず。そうでなければ、何の入力の助けにもならない。
ということで、ニックネームと誕生日、TwitterとFacebookの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_url
とprofiles
テーブルのname
をusers
テーブルのSELECT文のWHERE句で使う場合だ。
2つのテーブルくらいなら大丈夫そうだが、増えていった場合はパフォーマンスの影響も考慮した分割の方法を検討すると良いのかもしれない。
また、既に稼働しているサービスの場合、分割に際してデータ移行が必要となる。
参考
2年前くらいに読んだ「システム設計の原則」の中で似たようなことをやっていたのを思い出したのがきっかけ。 あらためてその部分を読もうと思ったのだけど、失くしてしまったのか、後輩にあげてしまったようなので、また買うかな…
現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法
- 作者: 増田亨
- 出版社/メーカー: 技術評論社
- 発売日: 2017/07/05
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
Google AssistantのバックエンドをAzure WebAppで作った。
Google Homeで音声を入力とした何かを今後作るかもしれないので、試しにGoogle Assistantを作るためのActions on GoogleとDialogflowを使った開発をしてみた。
初めて触れる技術のことなので、アプリケーションのネタはとてもシンプルにこれまで作ってきた駄洒落の自動生成にした。 で、実際にできたものがこちら。
バックエンドを、Firebase Functionsで作ろうと思ったのだが、無料版だと外部へのネットワーク接続ができないため諦めた。バックエンドは、Webhookの呼び出しを受けられれば何でも良いので、今回は普段から慣れているAzure WebAppを使うことにした。本当は、Azure Functionsにしたかったのだが、npmパッケージのactions-on-google
がサポートしていなかったので、WebApp上で動くnode.jsアプリケーションとして作った。
ソースコードは、こちら。
ユーザーの認証や、リソースへのアクセス権限を取得したりできるようなので、できることは色々ありそう。
インスタンス名を取り締まるためのRuboCop拡張をリリースしました。 #rubocop
インスタンス名を取り締まるためのRuboCop拡張 rubocop-instance_variable_name
をリリースしました。
何ができるの?
この拡張がやっているのは、とてもシンプルなことで、インスタンス変数名の長さが一定以下(デフォルトだと2)のインスタンス変数に警告を出します。
@ms = 'foo' # bad code @message = 'foo' # good code
作った動機
最近、他人の書いたコードをレビューしたり、他人の書いたコードを修正する機会が増えてきました。
その中で、いつも悩まされるのが変数名がわかりづらい時でした。
特に、インスタンス変数名は重要だと思います。
メソッド名やattr_reader
などで定義されるゲッターが分かりづらかったり、複雑なコードだとしても、変数名が何を表しているのかがわかると読み解くことができます。
特にクラス変数名は重要です。
読みづらさの中で最も辛いのは、略語を使ったものです。 普段使わない単語を使っているのも読みづらいのですが、英和辞典やググれば理解することはできます。 ですが、略語になると、それを推測するのが非常に難しくなります。
ということで、インスタンス変数名の長さに注目し、受け入れられない長さの変数名を指摘するRuboCop拡張を作りました。
是非ご活用ください。 また、何か不具合等ございましたら、Issueを作っていただけると助かります。
「デザイン思考の先を行くもの」を読んだ。
- 作者: 各務太郎
- 出版社/メーカー: クロスメディア・パブリッシング(インプレス)
- 発売日: 2018/11/09
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
最近身近に「デザイン思考」という言葉をよく聞くようになった。 言葉自体は以前から知っているが、一冊本を読んだくらいで、仕事でやったことは無い。
ということで、どんなものなのか知るためにとりあえず一冊読んでみた。 208ページある本だが、見開き2ページの片側のみに文章が書いてあり、文量もそれほど多くないのでサラッと読めた。
この本では、デザイン思考はPDCAと類似するものとして扱っている。 計画(Plan)の部分が、デザイン思考では課題からアイデアを出すまでのプロセスに相当するものとしている。 そのため、デザイン思考だけでまったく新しい何かが生まれることはないと注意している。 後半では、アイデアの作り方について書かれていて、参考になりそうだった。
軽く読める本だったが、知りたかったデザイン思考について、デザイン思考の実例については詳しく触れられていなかった。 単にプロセスだけを見て、PDCAと対応づけられるから同じものとして扱ってしまうのは少しやりすぎなのではないだろうか。 自分がデザイン思考について知っているわけではないが、何かしらプラクティスがあるのではないだろうかと思っている。 まだ1冊目なので、こういう見方もあるのだろうなというくらいでとどめておくことにした。
次に読みたい
なるべく原典に近いものを読みたいので、 デザイン思考ブーム(?)の発端になった「デザイン思考が世界を変える」を次は読んでみようかな。
デザイン思考が世界を変える (ハヤカワ・ノンフィクション文庫)
- 作者: ティム・ブラウン,Tim Brown,千葉敏生
- 出版社/メーカー: 早川書房
- 発売日: 2014/05/10
- メディア: 文庫
- この商品を含むブログ (4件) を見る
Butterfly Effect #1 に行ってきた。 #バタエフ
Butterfly Effect というイベントが始まる、ということで、初回ゲストがチームメンバーの @TAKAKING22だったので行ってきた。
Butterfly Effectって何?
イベントページより抜粋すると、
「すごい人が話を聞きたい人の話はすごいはず」という発想で 毎回違ったゲストを呼んでパネルディスカッションをするというイベント。
ということで、懐かしのテレフォンショッキング形式でゲストを回していくイベント。
確かに、紹介してもらった方がいい話をしてくれる人が来るのはかなり保証できそうだし、運営が楽できる。
運営として面白いのは、ゲストへの謝礼が当日Kyashで集められたお金(通称、投げ銭)が支払われるということ。 この集まった額って、どこかで公開されてしまったりするのかな?
当日の様子
会場に入るとウェルカムドリンクをいただき、また今回は石井食品さん提供のミートボールとハンバーグが振る舞われた。
はじめはネットワーキングから始まり、参加者同士で軽くドリンクを頂きながら雑談。 あとでの会場アンケートでわかったのだけど、エンジニアだけでなく、テスト関係の方々やマネージャーをしている人たちもけっこう居た。 参加者層は、ゲストによるのかな。
予めファシリテーターの蜂須賀さんが質問を用意もしているが、当日会場に来ている参加者からもsli.doを使って、質問が集められていた。 昨日集まった質問も下記から見れる。 app.sli.do
どんな雰囲気だったのかは、トギャッターにまとまってる。 togetter.com
感想
まぁ、いつも通りの@TAKAKING22を見てきた感ある。 ゲストのようで、途中から少しずつモデレートしていく感じで、入場から会場の場づくりまでが設計されているようだった*1。
チーム転職の話は皆さん興味あったみたい。 特に辞めた理由は、みなさんネガティブなものを想像していたようなので、世間一般の退職理由ってそういうものなのかというのを察した。
途中自分も少しだけ引っ張り出されて、チームメンバーからはどう見えるのか聞かれた。
昨日言ったように、みなさんがイベントで見てるように何も普段と変わらない。 最後に次回ゲストから言われたように「意外とチャラくない」のである。 マネージメントの話とか聞くとわかるように、チームの成果のために動く人なので、一緒に仕事しやすいし、楽しい。チームメンバーを評価してほしい!
— びば(森のフレンズ) 7/27 技書博 B-11 (@viva_tweet_x) July 9, 2019
#バタエフ pic.twitter.com/dUGiYBOmE5
次回ゲストは?
昨年に少しだけ一緒に仕事をした繋がりのきょんさん! 日程はこれから調整らしいが、名古屋から来てくれるそうなので参加しよー
おまけ
石井食品さんのミートボールとハンバーグ、マンゴープリンはどれも美味しかった!
*1:というか台本作りを前日に見てた。
7月からデンソーにジョインしましたのお知らせ
タイトルにありますように、デンソーの人として7月より働いております。
今は聖地横浜アリーナ近くの新横浜オフィスでソフトウェアエンジニアとして勤務しています。
経緯
今年1月末にチームでFA宣言を公表し、それに反応していただいたのがきっかけでした。
都内ではどうやら認知度がまだまだ低いようですが、もともと名古屋で生活していた時期があったので、デンソーという名前は知っていました。 ウェブサービスと連携したMaaS開発を本気で取り組んでいて、新横浜をその拠点として一昨年あたりから取り組んでいるところ、お声がけいただきました。 個人的にも、ウェブだけでなくモノと繋がったサービスに携わって、社会に貢献できればいいな、と思っていたのでとても良い機会をいただけました。
ということで、引き続きよろしくお願いいたします。
また、前職の退職祝い並びに入社祝いを受け付けておりますのでお気軽に送りつけてください。 お祝いの品のラインナップは以下のリストをご活用ください。