- 投稿日:2020-02-22T22:34:27+09:00
SDK for RubyでAWSの環境作ってみるよハンズオン
概要
コードでAWS環境を構築する、いわゆるInfrastructure as Code(Iac)と言えばCloudFormationやTerraformが主流ですが、主要なプログラミング言語でリソースの構築や操作ができるSDKがAWSからは提供されています。
そのSDKのうち今回はRubyを使って環境(VPC、IGW、Subnet、RouteTable)を構築するハンズオンを紹介したいと思います!
※作業環境はMacOS、SDK for Rubyのバージョン3になりますおしながき
- 事前準備
- SDKのインストールと設定
- コードを書いていく
- どうやってコードを書いているのか?
- 今後
- まとめ
1.事前準備
必要なもの
AWSアカウント
いわずもがなですが、各リソースを起動させるためにAWSのアカウントが必要です。もう持ってるという前提で進めますのでご了承Ruby(バージョン1.9以降)
これもいわずもがな…Rubyでコードを書いていきますので作業端末にインストールしてください。参考サイト
AWS SDK for Ruby Developer Guide
こちらの公式サイトにそってやっていきます。2.SDKのインストールと設定
SDKをインストールする
SDK for Rubyはgemとして配布されています。Rubyの外部ライブラリですね。
- bundlerを使用している場合
Gemfileに下記を追記してbundle install
を実行するgem 'aws-sdk'
- bundler使わない場合
下記のコマンドを実行してgemをインストールsudo gem install aws-sdk※けっこう時間がかかります。5分くらいターミナル画面が止まったままになりますが、フリーズではありません。
認証情報の設定
SDK経由でAWSにアクセスするための認証情報をローカルファイルに記述します。場所は
~/.aws/credentials
です。# ディレクトリを作成 mkdir ~/.aws # 移動 cd ~/.aws # vimでcredentialsファイルを編集 sudo vim credentialsvimで下記を記述します
[default] aws_access_key_id = your_access_key_id aws_secret_access_key = your_secret_access_key※上記の
your_access_key_id
、your_secret_access_key
にアクセスキー、シークレットアクセスキーを入力するため、一旦おいといて下の作業を進めますアクセスキー、シークレットアクセスキーを作成する
- AWSマネジメントコンソールにログインします
- マネジメントコンソールの右上のアカウント部分をクリック
- ドロップダウンメニューから「マイセキュリティ資格情報」を選択
- 「セキュリティ認証情報」画面に移動したら「アクセスキーの作成」ボタンを押す
※アクセスキーは1アカウントにつき2つまでしか作成できません。既に2つある場合は使っていないものを削除するなどしてください
- 「アクセスキーの作成」ボタンを押して出てきたウインドウの情報をメモする
※このウインドウは一旦閉じると2度とシークレットアクセスキーを見られなくなるので注意!もし閉じた場合は、作成したキーを削除してもう一度作成しなおしてください
※画像のアクセスキーは削除してるので使えません あしからず(笑)
- vimに戻ってメモしたアクセスキーとシークレットアクセスキーの情報をそれぞれコピペする
esc
キー =>:wq
で編集内容を保存してvimを閉じる環境変数でリージョンを設定する
東京リージョンを使用するよう設定します
export AWS_REGION=ap-northeast-13.コードを書いていく
今回のゴールはVPC、サブネット、インターネットゲートウェイ、ルートテーブルを作るところまでです。出来上がったrbファイルを実行するだけでAWSリソースが立ち上がる!というハンディさを実感してもらうのが目的なのでひとまずここまでで。
# sdkをrubyファイルに読み込み require 'aws-sdk' # VPCなどはEC2のカテゴリなのでAws::EC2::Clientクラスからインスタンスを作成 client = Aws::EC2::Client.new(region: "ap-northeast-1") # VPC ---------------------------------------------------------------- # VPCを作成 上で作成したclientインスタンスに対してcreate_vpc()メソッドを適用する resp_vpc = client.create_vpc({ cidr_block: "10.0.0.0/16", # 必須項目 IPアドレス範囲を指定する }) # 出来上がったVPCのIDを取得(他のリソース作成時やアタッチする時に使用する) VPC_ID = resp_vpc.vpc.vpc_id puts VPC_ID # IGW ---------------------------------------------------------------- # インターネットゲートウェイ(IGW)を作成 resp_igw = client.create_internet_gateway({ }) # IGWのID IGW_ID = resp_igw.internet_gateway.internet_gateway_id # IGWをVPCにアタッチ client.attach_internet_gateway({ internet_gateway_id: IGW_ID, # 上で作成したIGWのID vpc_id: VPC_ID, # 上で作成したVPCのID }) puts IGW_ID # Subnet ---------------------------------------------------------------- # サブネットを作成(インターネット向けパブリックサブネット) resp_pubsub1 = client.create_subnet({ availability_zone: "ap-northeast-1a", cidr_block: "10.0.0.0/24", # 必須項目 vpc_id: VPC_ID, # 必須項目 }) # サブネットのID SUB_ID1 = resp_pubsub1.subnet.subnet_id puts SUB_ID1 # RouteTable ---------------------------------------------------------------- # ルートテーブルを作成 resp_rt = client.create_route_table({ vpc_id: VPC_ID, # 必須項目 }) # ルートテーブルのID RT_ID = resp_rt.route_table.route_table_id # ルートを作成(IGWに向けたルート) resp_route = client.create_route({ destination_cidr_block: "0.0.0.0/0", # インターネット向けルート gateway_id: IGW_ID, # IGWのID route_table_id: RT_ID, # 必須項目 ルートテーブルのID }) # ルートテーブルをサブネットに紐付け client.associate_route_table({ route_table_id: RT_ID, # 必須項目 ルートテーブルのID subnet_id: SUB_ID1, # 紐付けるサブネットのID }) puts RT_ID # まとめてリソースにタグ(名前)を追加 ---------------------------------------------------------------- client.create_tags({ resources: [VPC_ID, IGW_ID, SUB_ID1, RT_ID], # 必須項目 名前をつけるリソースのID tags: [ { key: 'Name', value: 'HogeTestVPC', # VPCの名前 }, { key: 'Name', value: 'HogeTestIGW', # IGWの名前 }, { key: 'Name', value: 'HogeTestPublicSubnet1a', # Subnetの名前 }, { key: 'Name', value: 'HogeTestPublicRT', # RTの名前 }, ], }) puts "Create environment successfully done!"適当なディレクトリに上記のコードを.rbファイルとして保存して、
cd
コマンドで保存したディレクトリに移動。ruby 保存したファイル名.rb
で実行するとリソースの作成がAWS上で始まります。4.どうやってコードを書いているのか?
ひたすら下記の公式ドキュメントから該当するコードを引っ張ってきて、必要な要素を記述するだけです。ここが一番のツボというか、どのリソースに対して何をしたいのか?というのを考えてドキュメントから探し出すことさえ出来れば(書き方の良し悪しは置いておいて)リソースを操作するためのコードが書けるようになるはずです。
5.今後
公式ドキュメントの読み方や、EC2やその他のリソースに関する記述方法なども記事にしていけたらと考えています。また、今回はリソースを作成するだけでしたが、SDKの本領はプログラマブルなところで、書き方次第でAWSリソースを思い通りに操作することもできるみたいなので、そのへんも研究して記事にできたらいいなぁ、と思っています。
6.まとめ
ボク自身、ぜんぜんRubyやAWSに関しては初心者をようやく脱したかな?というレベルで、業務でSDK for Rubyを使うことになりました。思いの外、日本語での記事が少なく、四苦八苦しながら英語ドキュメントと戦って書いたので、同じようにSDKを使い始めようかという方の一助になればと思い記事を書きました。
また、SDKが提供されている言語を学習している方であれば、Gitでバージョン管理しつつGitHubに上げてポートフォリオとしてもいいかもな〜とか思いました。(CFnでもTerraformでもなくなぜSDK?ってツッコミには答えられるようにしたほうがいいかもですが笑…個人的にはSDKの方が学習コスト低いし、柔軟なリソース操作ができる可能性を感じます。)最後に、掲載したコードはちゃんと走ることを確認していますが「書き方がなってない、もっとうまい書き方があるぞ」というコメントやご指摘などあれば、お手柔らかにお願いします(>人<;)
- 投稿日:2020-02-22T21:11:10+09:00
[Rails] ActiveRecordでデータを更新したときに、各カラムが更新されたかどうか知りたかったんだ
はじめに
タイトル通りです。
下のアプリはチャットルームに「タイトル」と「ルームの説明」、「カテゴリ」を設定できるんだけれども、
「タイトル」が変更された時だけトークルームに通知が来るようにしたかった。
変更前のパラメーターを取得するか、変更されたかどうかを判定するメソッドがあったら嬉しいなーと思って探した。
実装
どうやらActiveRecordさんには「カラム名_changed?」っていうヘルパーメソッドがあるらしいと聞いて...
room_controllerdef update if @room.update(room_update_params) if @room.title_changed? # ココ。title_cahged?がtrueであれば自動メッセージを送るようにしたつもり。 Message.create(room_id: @room.id, user_id: current_user.id, content: "*自動投稿* タイトルを「#{@room.title}」に編集しました") end redirect_to room_path(@room.id) else render :edit end endこんな感じで実装してみたのだけれども失敗した。
Githubのrailsを見てみると、なんか更新されているらしい。
つまり
カラム名_changed?は、ActiveRecordのコールバックを使って処理を分けることができていたが、
save前の場合は、
オブジェクト.will_save_change_to_カラム名save後の場合は、
オブジェクト.saved_change_to_{カラム名}?という風に書き変える必要があるみたい。
再度実装
room_controller.rbdef update if @room.update(room_update_params) if @room.title_updated? Message.create(room_id: @room.id, user_id: current_user.id, content: "*自動投稿* タイトルを「#{@room.title}」に編集しました") end redirect_to room_path(@room.id) else render :edit end endコントローラーで「saved_change_to」やら「will_save_change_to」やらを呼び出すとうまくいかなかったので、
モデル側に「更新されたかどうか判別するインスタンスメソッド」を作りました。room.rbdef title_updated? if self.saved_change_to_title? return true else return false end endこれでタイトル編集時のみメッセージが投稿されるようになりました。
おわりに
今回初めてrailsのソースコードを見ましたが、もっと見ていくととても勉強になりそうです。
暇を見つけてもっと読んでみようと思いました。おわり。
- 投稿日:2020-02-22T19:27:59+09:00
[取り置き機能]Rails初学者が某フリマアプリにオリジナル機能を実装してみた。
某スクールのチーム開発にて某フリマアプリを作成しております。
先日、必須機能の実装が完了し、追加機能を実装していくフェーズになりました。
そこで、自分なりに色々と考えた結果、タイトルに記載いたしました「取り置き機能」を実装しました!
今回は、機能追加を決めるにあたっての背景含めて以下に記載いたします。今までは既存のコンテンツの実装であったため、様々な記事や情報があったため、自分で実装コンテンツや実装方法を考えるという機会は少し少なかった印象ありました。
しかしながら、今回はオリジナル機能であったため、実際のアプリユーザのことを考えながら実装をしていくのは、非常に楽しかったです!!背景(「取り置き機能」を追加機能に選んだ理由)
今回、追加機能を選ぶにあたり、ネットで某フリマアプリユーザのコメントをネットで探したり、自分でサイトをいじっていました。
その結果、以下の状況が見えてきたため、今回の「取り置き機能」実装を決断しました。
- 問題・課題:
特定のユーザに販売する際に、出品写真やタイトルなどに”〇〇様向け”などと書くしかできず、他ユーザに購入できないようにするブロックができない。
その状況のため、別ユーザがユーザ名を偽ることで、本来購入をする予定であったユーザが購入できず、別のユーザに購入されてしまうという問題が発生している。「取り置き機能」実装に関する考え方
今回の機能実装に関して、以下のように考えました。
1. 商品を出品するユーザが、商品に対して”キー”を設定する。
2. その”キー”を持っているユーザは購入をできるが、”キー”を持っていないユーザは購入をできないようにする。ここで問題となったのは何を"キー”とするかでした。
「出品するユーザが合言葉を設定し、それを入力させる」や「ユーザ名を”キー”とする」などを考えましたが、どれもユニーク性がなかったためボツとし、今回は購入して欲しいユーザのemailアドレスを”キー”とすることにし,”reservation_email”というカラムを追加することにしました。
"reservation_email"の情報によって、入力された情報とログインしているユーザの情報が一致すれば、購入できる/一致しなければ、購入できないようにします。*実際はemailアドレスを"キー”とするのはプライバシーの観点からあまりよろしくはないと思われますため、可能で有れば、会員番号などを”キー”とするのがベターかと考えます。(今回は会員番号を設定していなかったため、アドレスにしました)
実装内容(イメージ)
今回実装した機能のイメージは以下の通りになってます。
出品ユーザ側機能
機能1:取り置きをする
機能2:取り置きをやめる
機能3:その他ユーザに購入できないようにする
購入ユーザ側機能
機能1:取置き品を購入する
*購入すると以下のように、SOLDとなります。
購入についての実装は、以下の記事をご参照ください。
[HowTo]Pay.jpを用いた商品購入機能実装から商品購入後の設定まで
https://qiita.com/Tatsu88/items/eb420e372077939a4627#%E5%95%86%E5%93%81%E8%B3%BC%E5%85%A5%E7%A2%BA%E8%AA%8D%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AE%E7%B7%A8%E9%9B%86機能実装:マイグレーションファイル
マイグレーションファイルは以下のように実装してます。
今回は、t.string :reservation_emailを追加してます。
こちらが、取り置き機能を実現するための要となります。
"reservation_email"の情報によって、入力された情報とログインしているユーザの情報が一致すれば、購入できる/一致しなければ、購入できないようにします。マイグレーションファイルclass CreateProducts < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.string :name, limit: 191,null:false,index: true t.integer :price, index: true t.text :explain,null:false t.integer :postage,null:false t.integer :status t.integer :shipping_date t.integer :size t.integer :brand_id t.integer :category_id t.integer :prefecture t.integer :buyer_id t.references :user,index: true, foreign_key: true t.string :reservation_email t.timestamps end end end機能実装(ルーティング)
ルーティングは以下のように実装してます。
'reserve'、'reserved',patch 'reserve_cancel'を今回追加してます。
内容としては、予約・予約完了・予約取り消しとなります。ルーティングresources :products do member do post 'purchase' get 'purchased' get 'buy' get 'reserve' patch 'reserved' patch 'reserve_cancel' end resources :comments,only:[:create,:destroy] end機能実装(ビュー)
ビューは以下のように実装してます。
基本的に"reservation_email"の情報有無によって表示内容を変えてます。1. 商品一覧
こちらでは購入済みか取り置き済みかでサムネイルとして表示する写真にラベルを取り付けます。
view.product__thumbnail--image =image_tag (product.images[0].product_image.url) #購入されていれば、"SOLD"のラベルをつけます。 -if product.buyer_id.present? .items-box_photo__sold .items-box_photo__sold__inner SOLD #取り置きされていれば、"Reserved"のラベルをつけます。 -if product.reservation_email.present? .items-box_photo__reserved .items-box_photo__reserved__inner Reserved2. 商品詳細
こちらではユーザの状態によって、ビューの表示内容を変えてます。
view.product-buy__btn__box #ログインユーザが出品者の場合の、表示内容 - if user_signed_in? && current_user.id ==@product.user_id = link_to "削除する", product_path(@product.id), method: :delete,class:"product-details-delete__btn" = link_to "編集する", edit_product_path(@product.id),class:"product-details-edit__btn" = link_to "取り置きする/編集する", reserve_product_path(@product.id),class:"product-details-resorve__btn" #購入者向けの表示内容 #既に購入されている時 - elsif @product.buyer_id.present? = link_to "売り切れました",buy_product_path,class:"disabled-button bold" #取り置きされていて、その取り置きを商品を購入することを許可されているユーザの時 - elsif @product.reservation_email.present? && @product.reservation_email == current_user.email = link_to "取り置き商品を購入する",buy_product_path,class:"product-purchase__btn" #取り置きされていて、その取り置きを商品を購入することを許可されていないユーザの時 - elsif @product.reservation_email.present? && @product.reservation_email != current_user.email = link_to "取り置き商品のため購入できません",buy_product_path,class:"disabled-button bold" - else = link_to "購入画面に進む",buy_product_path,class:"product-purchase__btn"2. 取り置き画面
こちらでは、"reservation_email"というカラムの情報有無によって表示する内容を変えております。
情報が有れば、「取り消す」ボタンが出てきます。ビュー(取り置き画面)%main.buy-main .buy-item-container %h2.buy-item-head 取り置き内容の確認 %section.buy-content.buy-item .buy-content-inner .buy-item-box .buy-item-image =image_tag(@product.images[0].product_image.url,class:"buy-image") .buy-item-detail %p.buy-item-name =@product.name %p.buy-item-price.bold = number_to_currency(@product.price,format: "%u%n",unit:"¥",precision: 0) %span.item-shipping-fee.f14.bold (税込)送料込み =form_for(@product, url: reserved_product_path,method: :patch) do |f| .form-group =f.label :お取り置きをする方のアドレス %span.form-group__require 必須 %br/ = f.email_field :reservation_email, {autofocus: true, autocomplete: "email", placeholder: "PC・携帯どちらでも可",class:'form-group__input'} = f.submit '取り置きする', class: "reserve" - if @product.reservation_email.present? =link_to reserve_cancel_product_path,method: :patch, class:"btn-default btn-red" do 取り置きをやめる機能実装について(コントローラ)
コントローラは以下のように実装してます。
ポイントは以下の通りになってます。
1. reservedアクションで入力された”reservation_email”をproductに追加します。
*この”reservation_email”が取置き品を購入するためのキーとなります。
2. 取り置きをキャンセルする時と購入がされた後は”reservation_email”のvalueをなくします。コントローラ(該当箇所のみ)before_action :set_product, only: [:reserved,:reserve,:reserve_cancel,:purchase] def reserve end def reserved #reservedアクションで入力された”reservation_email”をproductに追加します。 @product.update(product_params) if @product.reservation_email.present? else render :reserve end end def reserve_cancel #”reservation_email”のvalueをなくします。 if @product.update(reservation_email:"") redirect_to product_path else redirect_to product_path end end def purchase Payjp.api_key = Rails.application.secrets.payjp_access_key charge = Payjp::Charge.create( amount: @product.price, customer: Payjp::Customer.retrieve(@creditcard.customer_id), currency: 'jpy' ) #”reservation_email”がある場合は、valueをなくします。 if @product.reservation_email.present? @product.update(reservation_email:"") end @product_buyer= Product.find(params[:id]) @product_buyer.update( buyer_id: current_user.id) redirect_to purchased_product_path end private def product_params params.require(:product).permit(:name,:category_id,:price,:explain,:size,:brand_id,:status,:postage,:shipping_date,:prefecture,:reservation_email,images_attributes: [:product_image,:_destroy,:id]).merge(user_id: current_user.id) end def set_product @product = Product.includes(:comments).find(params[:id]) end参照
【Rails】updateメソッドの使い方を徹底解説!
https://pikawaka.com/rails/update【Rails】form_forの使い方を徹底解説!
https://pikawaka.com/rails/form_for#form_for%E3%81%A7%E3%81%AE%E4%BF%9D%E5%AD%98%E6%96%B9%E6%B3%95%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9Railsのモデルの作成、検索、更新、削除のよく使うメソッドのまとめ
https://ruby-rails.hatenadiary.com/entry/20140724/1406142120以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。
- 投稿日:2020-02-22T19:27:59+09:00
[取り置き機能]某フリマアプリにオリジナル機能を実装してみた。
某スクールのチーム開発にて某フリマアプリを作成しております。
先日、必須機能の実装が完了し、追加機能を実装していくフェーズになりました。
そこで、自分なりに色々と考えた結果、タイトルに記載いたしました「取り置き機能」を実装しました!
今回は、機能追加を決めるにあたっての背景含めて以下に記載いたします。今までは既存のコンテンツの実装であったため、様々な記事や情報があったため、自分で実装コンテンツや実装方法を考えるという機会は少し少なかった印象ありました。
しかしながら、今回はオリジナル機能であったため、実際のアプリユーザのことを考えながら実装をしていくのは、非常に楽しかったです!!より良いコードの書き方や修正すべき点などご意見ございましたら、是非いただけますと幸いです!
背景(「取り置き機能」を追加機能に選んだ理由)
今回、追加機能を選ぶにあたり、ネットで某フリマアプリユーザのコメントをネットで探したり、自分でサイトをいじっていました。
その結果、以下の状況が見えてきたため、今回の「取り置き機能」実装を決断しました。
- 問題・課題:
特定のユーザに販売する際に、出品写真やタイトルなどに”〇〇様向け”などと書くしかできず、他ユーザに購入できないようにするブロックができない。
その状況のため、別ユーザがユーザ名を偽ることで、本来購入をする予定であったユーザが購入できず、別のユーザに購入されてしまうという問題が発生している。「取り置き機能」実装に関する考え方
今回の機能実装に関して、以下のように考えました。
1. 商品を出品するユーザが、商品に対して”キー”を設定する。
2. その”キー”を持っているユーザは購入をできるが、”キー”を持っていないユーザは購入をできないようにする。ここで問題となったのは何を"キー”とするかでした。
「出品するユーザが合言葉を設定し、それを入力させる」や「ユーザ名を”キー”とする」などを考えましたが、どれもユニーク性がなかったためボツとし、今回は購入して欲しいユーザのemailアドレスを”キー”とすることにし,”reservation_email”というカラムを追加することにしました。
"reservation_email"の情報によって、入力された情報とログインしているユーザの情報が一致すれば、購入できる/一致しなければ、購入できないようにします。*実際はemailアドレスを"キー”とするのはプライバシーの観点からあまりよろしくはないと思われますため、可能で有れば、会員番号などを”キー”とするのがベターかと考えます。(今回は会員番号を設定していなかったため、アドレスにしました)
実装内容(イメージ)
今回実装した機能のイメージは以下の通りになってます。
出品ユーザ側機能
機能1:取り置きをする
機能2:取り置きをやめる
機能3:その他ユーザに購入できないようにする
購入ユーザ側機能
機能1:取置き品を購入する
*購入すると以下のように、SOLDとなります。
購入についての実装は、以下の記事をご参照ください。
[HowTo]Pay.jpを用いた商品購入機能実装から商品購入後の設定まで
https://qiita.com/Tatsu88/items/eb420e372077939a4627#%E5%95%86%E5%93%81%E8%B3%BC%E5%85%A5%E7%A2%BA%E8%AA%8D%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AE%E7%B7%A8%E9%9B%86機能実装:マイグレーションファイル
マイグレーションファイルは以下のように実装してます。
今回は、t.string :reservation_emailを追加してます。
こちらが、取り置き機能を実現するための要となります。
"reservation_email"の情報によって、入力された情報とログインしているユーザの情報が一致すれば、購入できる/一致しなければ、購入できないようにします。マイグレーションファイルclass CreateProducts < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.string :name, limit: 191,null:false,index: true t.integer :price, index: true t.text :explain,null:false t.integer :postage,null:false t.integer :status t.integer :shipping_date t.integer :size t.integer :brand_id t.integer :category_id t.integer :prefecture t.integer :buyer_id t.references :user,index: true, foreign_key: true t.string :reservation_email t.timestamps end end end機能実装:ルーティング
ルーティングは以下のように実装してます。
'reserve'、'reserved',patch 'reserve_cancel'を今回追加してます。
内容としては、予約・予約完了・予約取り消しとなります。ルーティングresources :products do member do post 'purchase' get 'purchased' get 'buy' get 'reserve' patch 'reserved' patch 'reserve_cancel' end resources :comments,only:[:create,:destroy] end機能実装:ビュー
ビューは以下のように実装してます。
基本的に"reservation_email"の情報有無によって表示内容を変えてます。1. 商品一覧
こちらでは購入済みか取り置き済みかでサムネイルとして表示する写真にラベルを取り付けます。
view.product__thumbnail--image =image_tag (product.images[0].product_image.url) #購入されていれば、"SOLD"のラベルをつけます。 -if product.buyer_id.present? .items-box_photo__sold .items-box_photo__sold__inner SOLD #取り置きされていれば、"Reserved"のラベルをつけます。 -if product.reservation_email.present? .items-box_photo__reserved .items-box_photo__reserved__inner Reserved2. 商品詳細
こちらではユーザの状態によって、ビューの表示内容を変えてます。
view.product-buy__btn__box #ログインユーザが出品者の場合の、表示内容 - if user_signed_in? && current_user.id ==@product.user_id = link_to "削除する", product_path(@product.id), method: :delete,class:"product-details-delete__btn" = link_to "編集する", edit_product_path(@product.id),class:"product-details-edit__btn" = link_to "取り置きする/編集する", reserve_product_path(@product.id),class:"product-details-resorve__btn" #購入者向けの表示内容 #既に購入されている時 - elsif @product.buyer_id.present? = link_to "売り切れました",buy_product_path,class:"disabled-button bold" #取り置きされていて、その取り置きを商品を購入することを許可されているユーザの時 - elsif @product.reservation_email.present? && @product.reservation_email == current_user.email = link_to "取り置き商品を購入する",buy_product_path,class:"product-purchase__btn" #取り置きされていて、その取り置きを商品を購入することを許可されていないユーザの時 - elsif @product.reservation_email.present? && @product.reservation_email != current_user.email = link_to "取り置き商品のため購入できません",buy_product_path,class:"disabled-button bold" - else = link_to "購入画面に進む",buy_product_path,class:"product-purchase__btn"2. 取り置き画面
こちらでは、"reservation_email"というカラムの情報有無によって表示する内容を変えております。
情報が有れば、「取り消す」ボタンが出てきます。ビュー(取り置き画面)%main.buy-main .buy-item-container %h2.buy-item-head 取り置き内容の確認 %section.buy-content.buy-item .buy-content-inner .buy-item-box .buy-item-image =image_tag(@product.images[0].product_image.url,class:"buy-image") .buy-item-detail %p.buy-item-name =@product.name %p.buy-item-price.bold = number_to_currency(@product.price,format: "%u%n",unit:"¥",precision: 0) %span.item-shipping-fee.f14.bold (税込)送料込み =form_for(@product, url: reserved_product_path,method: :patch) do |f| .form-group =f.label :お取り置きをする方のアドレス %span.form-group__require 必須 %br/ = f.email_field :reservation_email, {autofocus: true, autocomplete: "email", placeholder: "PC・携帯どちらでも可",class:'form-group__input'} = f.submit '取り置きする', class: "reserve" - if @product.reservation_email.present? =link_to reserve_cancel_product_path,method: :patch, class:"btn-default btn-red" do 取り置きをやめる機能実装:コントローラ
コントローラは以下のように実装してます。
ポイントは以下の通りになってます。
1. reservedアクションで入力された”reservation_email”をproductに追加します。
*この”reservation_email”が取置き品を購入するためのキーとなります。
2. 取り置きをキャンセルする時と購入がされた後は”reservation_email”のvalueをなくします。コントローラ(該当箇所のみ)before_action :set_product, only: [:reserved,:reserve,:reserve_cancel,:purchase] def reserve end def reserved #reservedアクションで入力された”reservation_email”をproductに追加します。 @product.update(product_params) if @product.reservation_email.present? else render :reserve end end def reserve_cancel #”reservation_email”のvalueをなくします。 if @product.update(reservation_email:"") redirect_to product_path else redirect_to product_path end end def purchase Payjp.api_key = Rails.application.secrets.payjp_access_key charge = Payjp::Charge.create( amount: @product.price, customer: Payjp::Customer.retrieve(@creditcard.customer_id), currency: 'jpy' ) #”reservation_email”がある場合は、valueをなくします。 if @product.reservation_email.present? @product.update(reservation_email:"") end @product_buyer= Product.find(params[:id]) @product_buyer.update( buyer_id: current_user.id) redirect_to purchased_product_path end private def product_params params.require(:product).permit(:name,:category_id,:price,:explain,:size,:brand_id,:status,:postage,:shipping_date,:prefecture,:reservation_email,images_attributes: [:product_image,:_destroy,:id]).merge(user_id: current_user.id) end def set_product @product = Product.includes(:comments).find(params[:id]) end参照
【Rails】updateメソッドの使い方を徹底解説!
https://pikawaka.com/rails/update【Rails】form_forの使い方を徹底解説!
https://pikawaka.com/rails/form_for#form_for%E3%81%A7%E3%81%AE%E4%BF%9D%E5%AD%98%E6%96%B9%E6%B3%95%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9Railsのモデルの作成、検索、更新、削除のよく使うメソッドのまとめ
https://ruby-rails.hatenadiary.com/entry/20140724/1406142120以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。
- 投稿日:2020-02-22T19:27:01+09:00
rails:ページ移行で、(navbarなど)リンク表示を変更する
いらないリンクを消したい時
ログインページにいるのにログインリンク。新規登録ページにいるのにsignup(新規登録)のリンクはいらないですよね。
僕も今回、学習段階で実装する場面が来たので簡単にご紹介します。・新規登録ページ
navbarのリンクはlogin表示のみ。signup(新規登録)リンクは隠す
・ログインページ
navbarのリンクはsignup(新規登録)表示のみ。loginリンクは隠す
該当コード
request:ユーザのヘッダー情報や環境変数を取得
#新規登録画面におけるnavbarのコード <% unless request.path.include?("login") %> #"login"とのurlを含まなければtrue <li class="nav-item"><%= link_to 'Log in', login_path, class:'nav-link' %></li> <% end %> #ログイン画面におけるnavbarのコード <% unless request.path.include?("users/new") %> #"users/new"とのurlを含まなければtrue <li class="nav-item"><%= link_to 'Sign up', new_user_path, class:'nav-link'%></li> <% end %>#"signup"または"users/new"とのurlを含まなければtrue <% unless request.path.include?("signup") || request.path.include?("users/new")%> <li class="nav-item"><%= link_to 'Sign up', new_user_path, class:'nav-link'%></li> <% end %>補足
足りない部分や、間違っている箇所、もっときれいにコードを書ける部分があればご指摘いただきたいです。
- 投稿日:2020-02-22T19:10:49+09:00
【Tips】Ubuntuへのmysql2 gemインストール時に発生しうるエラーへの対処
問題
Ruby on Rails 等で利用する Ruby の MySQL 用ライブラリ
mysql2
インストールが失敗する。入力
gem install mysql2
出力
Building native extensions. This could take a while... ERROR: Error installing mysql2: ERROR: Failed to build gem native extension. current directory: /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/mysql2-0.5.3/ext/mysql2 /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/bin/ruby -I /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/2.7.0 -r ./siteconf20200222-2823-u3a2hd.rb extconf.rb checking for rb_absint_size()... yes checking for rb_absint_singlebit_p()... yes checking for rb_wait_for_single_fd()... yes *** extconf.rb failed *** Could not create Makefile due to some reason, probably lack of necessary libraries and/or headers. Check the mkmf.log file for more details. You may need configuration options. Provided configuration options: --with-opt-dir --without-opt-dir --with-opt-include --without-opt-include=${opt-dir}/include --with-opt-lib --without-opt-lib=${opt-dir}/lib --with-make-prog --without-make-prog --srcdir=. --curdir --ruby=/home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/bin/$(RUBY_BASE_NAME) --with-mysql-dir --without-mysql-dir --with-mysql-include --without-mysql-include=${mysql-dir}/include --with-mysql-lib --without-mysql-lib=${mysql-dir}/lib --with-mysql-config --without-mysql-config --with-mysqlclient-dir --without-mysqlclient-dir --with-mysqlclient-include --without-mysqlclient-include=${mysqlclient-dir}/include --with-mysqlclient-lib --without-mysqlclient-lib=${mysqlclient-dir}/lib --with-mysqlclientlib --without-mysqlclientlib /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/2.7.0/mkmf.rb:1050:in `block in find_library': undefined method `split' for nil:NilClass (NoMethodError) from /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/2.7.0/mkmf.rb:1050:in `collect' from /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/2.7.0/mkmf.rb:1050:in `find_library' from extconf.rb:87:in `<main>' To see why this extension failed to compile, please check the mkmf.log which can be found here: /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/extensions/x86_64-linux/2.7.0/mysql2-0.5.3/mkmf.log extconf failed, exit code 1 Gem files will remain installed in /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/mysql2-0.5.3 for inspection. Results logged to /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/extensions/x86_64-linux/2.7.0/mysql2-0.5.3/gem_make.out環境
- Windows Subsystem for Linux (WSL)
- Ubuntu 18.04 LTS
- anyenv 1.1.1
- rbenv 1.1.2-20-g143b2c9
- ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]
原因:MySQLの開発用ライブラリ不足
「Could not create Makefile due to some reason, probably lack of necessary libraries and/or headers.」とあるように、native extensionとして必要なMySQLに関連するライブラリがインストールされていない。
解決方法
入力
sudo apt install libmysqld-dev gem install mysql2出力
Building native extensions. This could take a while... Successfully installed mysql2-0.5.3 Parsing documentation for mysql2-0.5.3 Installing ri documentation for mysql2-0.5.3 Done installing documentation for mysql2 after 0 seconds 1 gem installed参考文献
- 投稿日:2020-02-22T18:00:41+09:00
Ruby(とC)でRubyを実装してみた(builtinで遊んでみた)
はじめに
タイトルにもあるようにRubyのbuiltin(正式名称を知らないので呼び出し方法から拝借)というものを使ってRuby自体をRuby(とC)で実装してみた話です。
内容としてはRuby自体の実装に興味のある方向けの話になります。
builtinって?
builtinとはRuby(とC)でRuby自体を実装するというものです(正式な名前は今のところないみたい?)。以下のように
__builtin_<Cで定義した関数名>
をRubyのコードから呼び出すことでRubyとCを使い、より簡単にRubyの実装を行うことができます。たとえば、
Hash#delete
はCで以下のように実装されています。static VALUE rb_hash_delete_m(VALUE hash, VALUE key) { VALUE val; rb_hash_modify_check(hash); val = rb_hash_delete_entry(hash, key); if (val != Qundef) { return val; } else { if (rb_block_given_p()) { return rb_yield(key); } else { return Qnil; } } }第一引数の
hash
はハッシュ自体を引数に受け取り、第二引数のkey
はHash#delete
で渡しているキーを受け取っています。ちなみに、Ruby側の変数などの値はVALUE
型で受け取り、Cの関数で処理されています。rb_hash_modify_check(hash);
rb_hash_modify_check
関数は内部でrb_check_frozen
関数を実行し、ハッシュが凍結されているかを確認しています。static void rb_hash_modify_check(VALUE hash) { rb_check_frozen(hash); // オブジェクトが凍結されているか確認 }
val = rb_hash_delete_entry(hash, key);
では引数に受け取ったキーをもとに削除する値を取得し、同時に削除を行っています。キーと対になる値がない場合はQundef
というCで使用する未定義の値が入ります。if (val != Qundef) { return val; } else { if (rb_block_given_p()) { return rb_yield(key); } else { return Qnil; } }
val
の値で処理を分岐させ、Qundef
ではない場合(つまりキーを使って値が取れ、削除できた場合)は削除された値を返します。
Qundef
だった場合はQnil
(Rubyでのnil
)を返します。ブロックが渡されている場合はrb_yield(key)
を実行し、その結果を返しています。このように皆さんが普段使っているRubyは、Cを使って実装されています。
builtin機能を使うことで先ほどのコードが以下のようになります。
class Hash def delete(key) value = __builtin_rb_hash_delete_m(key) if value.nil? if block_given? yield key else nil end else value end end endstatic VALUE rb_hash_delete_m(rb_execution_context_t *ec, VALUE hash, VALUE key) { VALUE val; rb_hash_modify_check(hash); val = rb_hash_delete_entry(hash, key); if (val != Qundef) { return val; } else { return Qnil; } }Ruby側でブロックの実行などを処理させているため。Cでの実装はよりシンプルで読みやすくなったと思います。
またこのようにbuiltin機能を使うことでRubyと少しのCのコードでRubyを実装することができます。
またCで実装するよりもRubyで実装した場合、パフォーマンスが向上するケースもあるようです。
より具体的な話は笹田さんがRubyKaigi 2019にて話されていますのでそちらを参照して頂ければと思います。Write a Ruby interpreter in Ruby for Ruby 3
やってみた
builtinを使うことでRubyのコードを使い、メソッドを実装できることがわかったので実際にやってみました。
開発環境構築
まずはRubyの開発環境を作成するところからはじめました。僕の環境としてはWSL+Ubuntu 18.04を使い、開発環境を構築しました。
基本的な手順としてはRuby Hack Challengeの(2) MRI ソースコードの構造を参考に進めました。まずは使用するライブラリなどをインストールしていきます。
sudo apt install git ruby autoconf bison gcc make zlib1g-dev libffi-dev libreadline-dev libgdbm-dev libssl-dev次に、作業用のディレクトリを作成し、そこへ移動します。。
mkdir workdir cd workdir作業用のディレクトリに移動後、Rubyのソースコードをcloneします。結構時間がかかるのでこの間にコーヒーでも入れておくといいでしょう。
git clone https://github.com/ruby/ruby.gitソースコードのcloneが終わったら、
ruby
ディレクトリへと移動し、autoconf
を実行します。あとで実行するconfigure
スクリプトを生成するためですね。実行後、workdir
まで戻ります。cd ruby autoconf cd ..次に、ビルド用のディレクトリを作成し、そこへ移動します。
mkdir build cd build
../ruby/configure --prefix=$PWD/../install --enable-shared
を実行してビルドするためのMakefileを作成します。また--prefix=$PWD/../install
ではRubyをインストールする先を指定しています../ruby/configure --prefix=$PWD/../install --enable-sharedその後、
make -j
を実行してビルドします。-j
は並列にコンパイルを実行するためのオプションです。特に急ぐわけでもない場合はmake
だけでも良いでしょう。make -j
最後に
make install
を実行するとworkdir
ディレクトリ内にinstall
ディレクトリが作成され、Rubyがインストールされます。make install
これで最新のRubyが
workdir/install
にインストールされています。ちなみに、本当にインストールされているか気になる方は
../install/bin/ruby -v
を実行してみましょう。ruby 2.8.0dev
とRubyのバージョンが表示されていればRubyは正しくインストールされています。builtinでメソッドを再定義してみる
開発環境が整ったのでbuiltinを使い、メソッドを再定義していきます。先ほど例にも挙げた
Hash#delete
を再実装していきます。common.mkの修正
まずは、ビルドの際にRubyのソースコードを使用するための諸設定を
common.mk
に追加します。
common.mk
の1000行目辺りに、BUILTIN_RB_SRCS
という記述があります。このBUILTIN_RB_SRCS
で読み込むRubyのコードが記述されているファイルを追加します。common.mkBUILTIN_RB_SRCS = \ $(srcdir)/ast.rb \ $(srcdir)/gc.rb \ $(srcdir)/io.rb \ $(srcdir)/pack.rb \ $(srcdir)/trace_point.rb \ $(srcdir)/warning.rb \ $(srcdir)/array.rb \ $(srcdir)/prelude.rb \ $(srcdir)/gem_prelude.rb \ $(empty) BUILTIN_RB_INCS = $(BUILTIN_RB_SRCS:.rb=.rbinc)今回は、Hashの実装を行うため
hash.rb
を以下のように追加します。BUILTIN_RB_SRCS = \ $(srcdir)/ast.rb \ $(srcdir)/gc.rb \ $(srcdir)/io.rb \ $(srcdir)/pack.rb \ $(srcdir)/trace_point.rb \ $(srcdir)/warning.rb \ $(srcdir)/array.rb \ $(srcdir)/prelude.rb \ $(srcdir)/gem_prelude.rb \ + $(srcdir)/hash.rb \ $(empty) BUILTIN_RB_INCS = $(BUILTIN_RB_SRCS:.rb=.rbinc)次に、2520行目辺りにあるHashのビルドで読み込むファイルを指定している部分を修正します。
このようにhash.c
など読み込むファイルが指定されています。common.mkhash.$(OBJEXT): {$(VPATH)}hash.c hash.$(OBJEXT): {$(VPATH)}id.h hash.$(OBJEXT): {$(VPATH)}id_table.h hash.$(OBJEXT): {$(VPATH)}intern.h hash.$(OBJEXT): {$(VPATH)}internal.h hash.$(OBJEXT): {$(VPATH)}missing.hここに、
hash.rbinc
とbuiltin.h
を追加します。hash.$(OBJEXT): {$(VPATH)}hash.c +hash.$(OBJEXT): {$(VPATH)}hash.rbinc +hash.$(OBJEXT): {$(VPATH)}builtin.h hash.$(OBJEXT): {$(VPATH)}id.h hash.$(OBJEXT): {$(VPATH)}id_table.h hash.$(OBJEXT): {$(VPATH)}intern.h hash.$(OBJEXT): {$(VPATH)}internal.h hash.$(OBJEXT): {$(VPATH)}missing.h
hash.rbinc
はmake
実行時に自動的に生成されるファイルで、hash.rb
内の__builtin_<呼び出すCの関数名>
をチェックした内容をもとに生成されています。またbuiltin.h
はbuiltinを使うための実装などがかかれたヘッダーファイルです。これで
common.mk
での修正は完了です。inits.cの修正
次に、
inits.c
を修正します。といっても非常に修正は簡単なものです。inits.c#define BUILTIN(n) CALL(builtin_##n) BUILTIN(gc); BUILTIN(io); BUILTIN(ast); BUILTIN(trace_point); BUILTIN(pack); BUILTIN(warning); BUILTIN(array); Init_builtin_prelude(); }
inits.c
では上記のようにbuiltinを使用しているRubyのソースファイルを追加しています。ここに同じようにBUILTIN(hash);
を追加します。#define BUILTIN(n) CALL(builtin_##n) BUILTIN(gc); BUILTIN(io); BUILTIN(ast); BUILTIN(trace_point); BUILTIN(pack); BUILTIN(warning); BUILTIN(array); + BUILTIN(hash); Init_builtin_prelude();
inits.c
の修正はこれでOKです。hash.cの修正
いよいよ、
hash.c
のコードを修正していきます。builtin.hの読み込み
まずは、40行目辺りのヘッダー読み込み部分に
#include "builtin.h"
を追加します。#include "ruby/st.h" #include "ruby/util.h" #include "ruby_assert.h" #include "symbol.h" #include "transient_heap.h" + #include "builtin.h"
これでbuiltinに必要な構造体などを
hash.c
で使用することができます。Hash#deleteの定義を削除
次に、
Hash#delete
を定義している部分を取り除きます。
hash.c
の下部にInit_Hash(void)
という関数が定義されていると思います。void Init_Hash(void) { /// Hashの実装コードなどが記述されています。 }Rubyの各クラスのメソッドはこの関数内で以下のように定義されています。
rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);
rb_define_method
はRubyでいうところのメソッドの定義と同じと考えてください。第一引数にメソッドを定義するクラスのVALUE
を渡し、第二引数がメソッド名となっています。
第三引数がCで定義された関数(メソッドで実行される処理)で、第四引数がメソッドが受け取る引数の数となっています。builtinでRubyのメソッドを定義する場合はこの定義部分を削除する必要があります。今回は
Hash#delete
を再実装しますので、delete
が定義されている部分を削除します。rb_define_method(rb_cHash, "shift", rb_hash_shift, 0); - rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1); rb_define_method(rb_cHash, "delete_if", rb_hash_delete_if, 0);
rb_hash_delete_mをbuiltinから使用できるように修正
先ほど削除した
rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);
で呼び出されているrb_hash_delete_m
をbuiltinで使用できるように修正します。2380行辺りに
rb_hash_delete_m
の実装があります。static VALUE rb_hash_delete_m(VALUE hash, VALUE key) { VALUE val; rb_hash_modify_check(hash); val = rb_hash_delete_entry(hash, key); if (val != Qundef) { return val; } else { if (rb_block_given_p()) { return rb_yield(key); } else { return Qnil; } } }これを以下のように修正します。
static VALUE rb_hash_delete_m(rb_execution_context_t *ec, VALUE hash, VALUE key) { VALUE val; rb_hash_modify_check(hash); val = rb_hash_delete_entry(hash, key); if (val != Qundef) { return val; } else { return Qnil; } }builtin対応のために第一借り引数に
rb_execution_context_t *ec
を渡しているところが実装の肝ですね。これでRubyからCで定義した関数を呼び出すことができるようになります。
hash.rbincの読み込み
最後に、自動生成される
hash.rbinc
を読み込むようにします。
#include "hash.rbinc"
をhash.c
の一番下に追加します。#include "hash.rbinc"
これでCのコード側での修正は完了しました。
hash.rbの作成
それではRubyで
Hash#delete
を実装してみましょう。hash.c
と同じ階層にhash.rb
を作成します。
作成後、以下のようにコードを追加します。class Hash def delete(key) puts "impl by Ruby(& C)!" value = __builtin_rb_hash_delete_m(key) if value.nil? if block_given? yield key else nil end else value end end end先ほどbuiltinで呼び出せるようにした
__builtin_rb_hash_delete_m
に受け取ってきた引数を渡し、その結果をvalue
に代入しています。あとは、
value
の値がnil
か同課で処理を分岐させています。nil
の場合かるブロックが渡されている場合はkey
を引数にブロックを実行しています。
puts "impl by Ruby(& C)!"
は実際に試す際に確認するためのメッセージになりますね。これでbuiltinでの実装はすべて完了しました!
ビルドしてみる
それでは開発環境を構築した時同様にビルドしてみましょう。
make -j && make installビルドが成功すればOKです!もしビルドが失敗した場合はtypoなどが無いか確認してみましょう。
実際にirbで試してみる
それでは
irb
を使ってbuiltinで実装したHash#delete
を試してみましょう!../install/bin/irb後は以下のコードを貼り付けてみましょう!
hash = {:key => "value"} hash.delete(:k) hash.delete(:key)以下のように結果が表示されていればbuiltinでの実装は完了です!
irb(main):001:0> hash = {:key => "value"} irb(main):002:0> hash.delete(:k) impl by Ruby(& C)! => nil irb(main):003:0> hash.delete(:key) impl by Ruby(& C)! => "value" irb(main):004:0>
impl by Ruby(& C)!
と表示されているのでRubyで定義したHash#delete
が実行されていることが分かりますね。これでRuby(とC)でRubyを実装できました!
終わりに
このようにbuiltinを使うことでRubyと(少しのC)のコードを使ってRuby自体を実装することができます。
そのため普段Rubyを書いている人でも気軽にメソッドの修正などのパッチを送ることができるようになるのではないかと思います。あとやってみてRuby側で処理が書けたりするので意外と書きやすいというのはうれしいですね。
個人的にはExtensionなどでも使用できるようになるとC/C++でのRuby拡張がより書きやすくなるのではないかと思うので、今後の展望が非常に楽しみですね。
参考
- 投稿日:2020-02-22T15:19:47+09:00
ユーザ情報をどうDB設計するか突き詰めて考えてみる
この記事について
- システム開発するうえで絶対に避けては通れないユーザ情報周りの設計を自分なりに考察してみた
- 結局何が最適かわからないけど色々と考え出すと奥が深かった
きっかけ的な
Railsでアプリ作ったんだけど、bootstrapとかdeviseとか使って横着したので、正直やった感ないなーって思いました。HTML/CSS多少勉強しなおして、あとこの本読んで、もう一回横着しないでなんちゃってQiitaみたいななんちゃってメディアシステムみたいなの作ろうかなと。そうなると、当然ユーザ情報の設計とか考えなきゃなんですが、いろいろ考えたら奥が深かったのでQiitaに載せて整理しようかなーって思いました。あと、ここに公開すればありがたいお言葉を頂戴いただけるのではないかとか思ったり。
本記事のユーザ情報の定義
利用用途
- ログイン認証に用いる。
- サインアップ画面より、新規登録をユーザ自身で可能とする。
- ユーザによるプロフィール編集を可能とする。
- プロフィールは他ユーザにより、CMS上のある画面から一覧表示できる。また、個別のユーザはユーザ名で検索可能とする。
- プロフィール詳細はほかユーザにより、CMS上のある画面から確認可能とする。
- ユーザ削除時は、ログイン情報も含めてすべて物理削除する
項目定義
- 一意のログインID、パスワードをもつ。また、最終ログイン日時を保持する。
- CMS内で利用される一意のユーザ名を保持する。
- 上記ユーザ名とは別でユーザの氏名、氏名カナを保持する。ユーザの氏名、氏名カナは他ユーザは見れないものとする。
- ユーザ登録時や運営お知らせに利用するためのメールアドレスを保持する。
- プロフィール情報として年齢、誕生日、自由記述欄を保持する。
私の設計
こうなりました。詳細は追ってですが、この後簡単に思考プロセスを書いていきます。なお、簡単にするため、インデックスどうするとかは一旦考えません。
テーブルにどう持たせるか
まあ単純に考えるとusersテーブルにえいやって全項目突っ込みたくなりますよね。こんな感じです。
別に個人で勉強用途に使う程度ならこれでいいんですが、ある程度ユーザ増えるという前提で考えると、ユーザに関する全操作のたびにテーブルがロックされて、パフォーマンス落ちるんじゃない?とかデータ量多くなって適切にインデックス設計とかしても限界ありそうだよねとか思ったわけです。あと、このテーブルにカラム追加するとか、検索のためのタグ付けするとか、そういうの考えるとなんだか背筋が凍ります。特に住所情報持たせるとかなったらとんでもないことになりそう。テーブルを分けてみる
要件に立ち返ると、認証、プロフィールとあるので、この観点で分けてみます。
ログインIDとパスワード、最終ログイン日時をauthenticatesとして定義しました。なお両テーブルは1:1で関連付けられています。
とりあえずusersテーブルのごった煮感はなくなりましたが、profileもう少しなんとかできないかなーとか思うので、もう少し分けてみます。
よく見るとほかユーザに見られてほしくない氏名の情報が混じってるのでこれは分けたほうがよさげ。と思ったのですが、、、
いい感じに分けられたんですけど、なんかセンスないというか、どう関連付けていいのかぱっと見わからないですね。。。
というわけで、fullnamesを親にして、profilesとauthenticatesと関連付けてみます。
なんかいい感じに見えました。でも親にfullnameってのがなんかあれですね。もう分けてしまいましょう。
いい感じになったけど、なんかこれでいいのか感があります。ちょっと大げさに分けすぎてしまったかな。。。
なんかもやっとしてますけど、これで終わりにします。名前のセンスないなとかそういうのはありますが。。。最後に
もやっとした感じで終わってしまったので、なんか不完全燃焼ですが、これはこれで結論ということで。
authenticatesとusersの関連付けっているのかな。正直model側でdestroyの依存をかけてしまうだけでいい気もする。でも、直接SQLで触られたりすると一気にデータがおかしくなる。。。
あと、Railsの規約って観点でもどうなのかなとも少し思ったりもした。
- 投稿日:2020-02-22T15:13:34+09:00
ユーザ情報のDB設計について突き詰めて考えてみる
この記事について
- システム開発するうえで絶対に避けては通れないユーザ情報周りの設計を自分なりに考察してみた
- 結局何が最適かわからないけど色々と考え出すと奥が深かった
きっかけ的な
Railsでアプリ作ったんだけど、bootstrapとかdeviseとか使って横着したので、正直やった感ないなーって思いました。HTML/CSS多少勉強しなおして、あとこの本読んで、もう一回横着しないでなんちゃってQiitaみたいななんちゃってメディアシステムみたいなの作ろうかなと。そうなると、当然ユーザ情報の設計とか考えなきゃなんですが、いろいろ考えたら奥が深かったのでQiitaに載せて整理しようかなーって思いました。あと、ここに公開すればありがたいお言葉を頂戴いただけるのではないかとか思ったり。
本記事のユーザ情報の定義
利用用途
- ログイン認証に用いる。
- サインアップ画面より、新規登録をユーザ自身で可能とする。
- ユーザによるプロフィール編集を可能とする。
- プロフィールは他ユーザにより、CMS上のある画面から一覧表示できる。また、個別のユーザはユーザ名で検索可能とする。
- プロフィール詳細はほかユーザにより、CMS上のある画面から確認可能とする。
- ユーザ削除時は、ログイン情報も含めてすべて物理削除する
項目定義
- 一意のログインID、パスワードをもつ。また、最終ログイン日時を保持する。
- CMS内で利用される一意のユーザ名を保持する。
- 上記ユーザ名とは別でユーザの氏名、氏名カナを保持する。ユーザの氏名、氏名カナは他ユーザは見れないものとする。
- ユーザ登録時や運営お知らせに利用するためのメールアドレスを保持する。
- プロフィール情報として年齢、誕生日、自由記述欄を保持する。
私の設計
こうなりました。詳細は追ってですが、この後簡単に思考プロセスを書いていきます。なお、簡単にするため、インデックスどうするとかは一旦考えません。
テーブルにどう持たせるか
まあ単純に考えるとusersテーブルにえいやって全項目突っ込みたくなりますよね。こんな感じです。
別に個人で勉強用途に使う程度ならこれでいいんですが、ある程度ユーザ増えるという前提で考えると、ユーザに関する全操作のたびにテーブルがロックされて、パフォーマンス落ちるんじゃない?とかデータ量多くなって適切にインデックス設計とかしても限界ありそうだよねとか思ったわけです。あと、このテーブルにカラム追加するとか、検索のためのタグ付けするとか、そういうの考えるとなんだか背筋が凍ります。特に住所情報持たせるとかなったらとんでもないことになりそう。テーブルを分けてみる
要件に立ち返ると、認証、プロフィールとあるので、この観点で分けてみます。
ログインIDとパスワード、最終ログイン日時をauthenticatesとして定義しました。なお両テーブルは1:1で関連付けられています。
とりあえずusersテーブルのごった煮感はなくなりましたが、profileもう少しなんとかできないかなーとか思うので、もう少し分けてみます。
よく見るとほかユーザに見られてほしくない氏名の情報が混じってるのでこれは分けたほうがよさげ。と思ったのですが、、、
いい感じに分けられたんですけど、なんかセンスないというか、どう関連付けていいのかぱっと見わからないですね。。。
というわけで、fullnamesを親にして、profilesとauthenticatesと関連付けてみます。
なんかいい感じに見えました。でも親にfullnameってのがなんかあれですね。もう分けてしまいましょう。
いい感じになったけど、なんかこれでいいのか感があります。ちょっと大げさに分けすぎてしまったかな。。。
なんかもやっとしてますけど、これで終わりにします。名前のセンスないなとかそういうのはありますが。。。最後に
もやっとした感じで終わってしまったので、なんか不完全燃焼ですが、これはこれで結論ということで。
authenticatesとusersの関連付けっているのかな。正直model側でdestroyの依存をかけてしまうだけでいい気もする。でも、直接SQLで触られたりすると一気にデータがおかしくなる。。。
あと、Railsの規約って観点でもどうなのかなとも少し思ったりもした。
- 投稿日:2020-02-22T14:38:38+09:00
LeetCode - 326. Power of Three
問題
326. Power of Three - 与えられた整数が3の累乗数かどうかを判定せよ
解答
ループまわすのと、再帰と考えてみる
ループ
# @param {Integer} n # @return {Boolean} def is_power_of_three(n) if n == 1 true elsif n < 3 false else while n != 3 if n % 3 == 0 n = n / 3 else return false exit end end true end end
- 3 の累乗数ならば、商が 3 になるまで 3 で割りづつけても、あまりは 0 であるはず
- X の 0 乗は 1 なので
true
スコア
Runtime: 64 ms, faster than 85.71% of Ruby online submissions for Power of Three. Memory Usage: 9.2 MB, less than 100.00% of Ruby online submissions for Power of Three.再帰
# @param {Integer} n # @return {Boolean} def is_power_of_three(n, power_value = 1) #puts "n=#{n} power_value=#{power_value}" return true if n == power_value return false if n < power_value is_power_of_three(n, power_value * 3) end
- これは、LeetCodeのサイトにあった ぱっと思いつかなかったが、すばらしい解ですね
- 3 の累乗数を小さいものから作っていき、どこかで
n
と 一致すればn
は 3 の累乗数、通り越してしまったら累乗数ではないスコア
Runtime: 92 ms, faster than 20.00% of Ruby online submissions for Power of Three. Memory Usage: 9.3 MB, less than 100.00% of Ruby online submissions for Power of Three.きれいだけど速くはないんですね
テスト
require 'minitest/autorun' require '../326-power-of-three.rb' class DoTest < Minitest::Test def test_1 assert_equal true, is_power_of_three(27) end def test_2 assert_equal false, is_power_of_three(0) end def test_3 assert_equal true, is_power_of_three(9) end def test_4 assert_equal false, is_power_of_three(45) end def test_5 assert_equal true, is_power_of_three(1) end def test_6 assert_equal false, is_power_of_three(19684) end def test_7 assert_equal false, is_power_of_three(-3) end def test_8 assert_equal false, is_power_of_three(6) end end
- 投稿日:2020-02-22T14:13:59+09:00
Railsチュートリアル bitbucketにプッシュできない
bitbucketにプッシュする方法。
軽くハマったので備忘録。git push -u origin --allだと失敗するので、
git push --mirror git@bitbucket.org:[ユーザー名]/sample_appこっちを使いましょう。
- 投稿日:2020-02-22T14:11:09+09:00
[Ruby on Rails]画像投稿ボタンをFont Awesomeのアイコンにする方法
概要
表題の通りですが、調べるとHaml記法で書かれた投稿しか見つけられず、今のところHaml記法を使っていない私にはわかりづらかったので、メモがてら共有させていただきます。
環境
Ruby:2.6.3
Rails:5.1.6
bootstrap:4.4.1
FontAwesome方法
1.file_fieldで画像投稿ボタンを表示。
2.file_fieldボタンによって出力されるinputタグを見えなくする(dislay:none)。
3.file_fieldの上にFont Awesomeのアイコンをiタグで表示。
4.iタグをlabelタグで囲って、アイコンをボタンとして有効にする。home.html.erb<%= form_for(@dreampost) do |f| %> <div class="field"> <%= f.text_area :content, placeholder: "投稿できます" %> </div> <div class="space-between"> <span class="picture"> <label for="dreampost_picture"> <i class="far fa-image"></i> </label> <%= f.file_field :picture, placeholder: '',accept: 'image/jpeg,image/gif,image/png' %> <div class="clear"></div> </span> <%= f.submit "送信", class: "btn btn-primary" %> <% end %>custom.scss.space-between { display: flex; justify-content: space-between; } .picture>input { display: none; } .picture>label { margin-bottom: 0; float: left; } .fa-image { color: #fff; float: left; } .fa-image::before { font-size: 2rem; } .clear { clear: both; }補足
div.classに{ display: flex; justify-content: space-between;}と、
labelタグとiタグにfloat:leftで、
アイコンを左寄せ、送信ボタンを右寄せにしています。ご指摘などございましたら、ぜひよろしくお願いいたします。
- 投稿日:2020-02-22T13:23:03+09:00
Rails でトークン認証 API を 15 分で実装する
下記を作成してみます。SPA のバックエンド側を想定しています。
/auth
でログインし token を発行してもらえる。/users
でユーザー一覧を取得できる。
- ただし token が必要。
- 更に admin ユーザーのみでき、member ユーザーはアクセス許可がない。
上記を利用したフロント側の記事も書いておりますので宜しければご覧ください。
Vue.js で簡単なログイン画面 (トークン認証) を作ってみたUser API 作成
rails new で API モードで新規アプリ作成します。
$ rails new yourappname --apiscaffold で User Model と Controller を作ります。
lock_version というカラムを追加すると Rails で楽観ロックを実装してくれます。便利ですね。$ cd yourappname $ rails g scaffold User name:string \ email:string \ role:integer \ password_digest:string \ register_user:integer \ update_user:integer \ lock_version:integer \ activated_at:datetime \ deleted_at:datetimeseeds に初期データを書いてみましょう。
db/seeds.rbUser.create!([ { name: 'admin', email: 'admin@example.com', role: 'admin', password_digest: '$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK', register_user: 1, update_user: 1, lock_version: 0, activated_at: '2020-02-03 00:00:00', deleted_at: nil, }, { name: 'member', email: 'member@example.com', role: 'member', password_digest: '$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK', register_user: 1, update_user: 1, lock_version: 0, activated_at: '2020-02-03 00:00:00', deleted_at: nil, }, ])開発用の DB をマイグレーション & 初期データを投入します。
$ rails db:migrate && rails db:seedサーバーを起動します。
$ rails s
別ターミナルにて curl で叩くと初期データが返ってきます。
まずは簡単な API ができたことを確認します。# 別ターミナル $ curl -s http://localhost:3000/users | jq [ { "id": 1, "name": "admin", "email": "admin@example.com", "role": "admin", "password_digest": "$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK", "register_user": 1, "update_user": 1, "lock_version": 0, "activated_at": "2020-02-03T00:00:00.000Z", "deleted_at": null, "created_at": "2020-02-22T04:08:58.769Z", "updated_at": "2020-02-22T04:08:58.769Z" }, { "id": 2, "name": "member", "email": "member@example.com", "role": "member", "password_digest": "$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK", "register_user": 1, "update_user": 1, "lock_version": 0, "activated_at": "2020-02-03T00:00:00.000Z", "deleted_at": null, "created_at": "2020-02-22T04:08:58.775Z", "updated_at": "2020-02-22T04:08:58.775Z" } ]テストも実行してエラーが無いことも確認しましょう。
$ rails t Running via Spring preloader in process 16858 Run options: --seed 26013 # Running: ..... Finished in 0.320389s, 15.6060 runs/s, 21.8484 assertions/s. 5 runs, 7 assertions, 0 failures, 0 errors, 0 skipsちなみに jq が入っていない場合は入れると JSON 整形してくれて便利です。
$ brew install jqGemfile もろもろ
Gemfile は Node.js で言う、package.json 的なファイルだと思います。
下記を追記します。ついでにオススメの gem も。
gem 'redis-rails' # Redis を扱うための gem gem 'mock_redis' # Redis のモック。テスト実行時に使用。 gem 'config' # 環境ごとに yml の設定ファイルを作成可能。 gem 'pundit' # 認証周りを REST ベースでシンプルに実装できる。 gem 'paranoia' # 論理削除できる。下記をコメントアウトします。
has_secure_password を使う際に必要です。
これはテーブルに password_digest というカラムを用意すると、Rails がパスワードをハッシュ化してくれます。gem 'bcrypt', '~> 3.1.7'bundle install するとパッケージがインストールされます。
$ bundle installconfig をインストールすると rails g config:install が使えます。
環境ごとに yml ファイルができます。便利ですね。$ rails g config:install Running via Spring preloader in process 17408 create config/initializers/config.rb create config/settings.yml create config/settings.local.yml create config/settings create config/settings/development.yml create config/settings/production.yml create config/settings/test.yml append .gitignoreRedis 設定
セッション情報を保存するためにインメモリ DB の Redis を使います。
Mac の方は brew でインストール。$ brew install redisredis-server で起動できます。簡単でいいですね。
$ redis-server
いったん、全環境共通の settings.yml に url と timeout を追記します。
こうすると例えば、下記の url の値を取り出すにはSettings.session.url
と記述すれば OK です。config/settings.ymlsession: url: redis://localhost:6379 timeout: 7200 role: member: member admin: adminところで Redis 接続時に毎回 Redis.new() を書くのは不便なので、initializers/ 下に redis.rb を作成します。
ついでにテストの時は Redis 立ち上げなくても済むようにしました。config/initializers/redis.rbif Rails.env.test? REDIS = MockRedis.new else REDIS = Redis.new(url: Settings.session.url) endAuth コントローラー作成
コマンドラインから枠を作ります。
$ rails g controller auth
auth のルーティングを追加します。
個人的にはなるべく resources を使うようにすると綺麗だと思います。config/routes.rbRails.application.routes.draw do resources :users + resources :auth, :only => [:create, :destroy] endセッション作成処理を concerns に切り出してみます。
app/controllers/concerns/session.rbmodule Session def self.create(user) token = SecureRandom.hex(64) REDIS.mapped_hmset( token, 'user_id' => user.id, 'role' => user.role, ) REDIS.expire(token, Settings.session.timeout) return token end endAuthController で先ほど作った Session モジュールを利用します。
app/controllers/auth_controller.rbclass AuthController < ApplicationController + def create + user = User.find_by(email: params[:email]) + token = '' + status = :unauthorized + if user && user.authenticate(params[:password]) + token = Session.create(user) + status = :created + end + render json: { token: token }, status: status + end endmodels/user.rb に下記を追記。
物理削除とパスワードハッシュ化を利用します。
enum で member と admin も定義しておきます。app/models/user.rbclass User < ApplicationRecord + acts_as_paranoid + has_secure_password + enum role: { member: 0, admin: 1 } end動作確認してみましょう。
ログイン OK
まずは seeds.rb に記述した email と password で /auth を叩きます。
token が返ってきますね$ curl -s \ -X POST http://localhost:3000/auth \ -H "Content-Type: application/json" \ -d '{"email": "admin@example.com", "password": "password1234"}' {"token":"ed64bbcf830e4feca9203b0955c5916fda815a23da80da8a7bb62b8c6466dbc34a07e21e9dd96447882fc8b9cb06121c8ee414b79ffedef4892e7197bbe9edd1"}redis-cli で Redis に接続して、トークンが作成されたか確認してみましょう。
おお、できています!$ redis-cli 127.0.0.1:6379> keys * 1) "ed64bbcf830e4feca9203b0955c5916fda815a23da80da8a7bb62b8c6466dbc34a07e21e9dd96447882fc8b9cb06121c8ee414b79ffedef4892e7197bbe9edd1"ログイン NG
password を適当に変えてみます。
token が空になっていますね。$ curl -s -X POST http://localhost:3000/auth \ -H "Content-Type: application/json" \ -d '{"email": "admin@example.com", "password": "wrongpassword"}' {"token":""}認証を設定
Session モジュールにセッション情報を取得する function を追加します。
app/controllers/concerns/session.rbmodule Session + def self.get(token) + REDIS.hgetall(token) + end def self.create(user) token = SecureRandom.hex(64) REDIS.mapped_hmset( token, 'user_id' => user.id, 'role' => user.role, ) REDIS.expire(token, Settings.session.timeout) return token end end続いて基幹コントローラーに手を入れます。
authenticate_with_http_token
を利用すると、 リクエストヘッダに 'Authorization: Token hogehoge' がセットされていた場合に、トークン hogehoge を取り出せます。
- 上記を利用するには
include ActionController::HttpAuthentication::Token::ControllerMethods
する必要があります。before_action :set_session
で、Redis に登録してあるセッション (ここでは user.id と user.role ) をメンバにセットしています。before_action :require_login
で、基本的にセッションがない場合に認証エラーとしています。app/controllers/application_controller.rbclass ApplicationController < ActionController::API + include ActionController::HttpAuthentication::Token::ControllerMethods + + before_action :set_session + before_action :require_login + + @session = {} + + def require_login + render json: { error: 'unauthorized' }, status: :unauthorized if @session.empty? + end + + private + def set_session + authenticate_with_http_token do |token, options| + @session = Session.get(token) + end + end endAuthController に skip_before_action を追加します。
これを追加しないと一生ログインができません・・。app/controllers/auth_controller.rbclass AuthController < ApplicationController + skip_before_action :require_login, only: [:create] + def create user = User.find_by(email: params[:email]) token = '' status = :unauthorized if user && user.authenticate(params[:password]) token = Session.create(user) status = :created end render json: { token: token }, status: status end endユーザー一覧 OK
Authorization: Token xxx
には、先ほど /auth を叩いて得られた token をセットします。
/users の結果が返ってきています。$ curl -s http://localhost:3000/users \ -H "Content-Type: application/json" \ -H "Authorization: Token ed64bbcf830e4feca9203b0955c5916fda815a23da80da8a7bb62b8c6466dbc34a07e21e9dd96447882fc8b9cb06121c8ee414b79ffedef4892e7197bbe9edd1" \ | jq [ { "id": 1, "name": "admin", "email": "admin@example.com", "role": 0, "password_digest": "$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK", "register_user": 1, "update_user": 1, "lock_version": 0, "activated_at": "2020-02-03T00:00:00.000Z", "deleted_at": null, "created_at": "2020-02-21T14:28:10.627Z", "updated_at": "2020-02-21T14:28:10.627Z" }, { "id": 2, "name": "member", "email": "member@example.com", "role": 0, "password_digest": "$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK", "register_user": 1, "update_user": 1, "lock_version": 0, "activated_at": "2020-02-03T00:00:00.000Z", "deleted_at": null, "created_at": "2020-02-21T14:28:10.635Z", "updated_at": "2020-02-21T14:28:10.635Z" } ]ユーザー一覧 NG
token を適当なものに変えるとちゃんと認証エラーになりました
$ curl -s http://localhost:3000/users -H "Content-Type: application/json" -H "Authorization: Token wrong_token" | jq { "error": "unauthorized" }権限周り設定
Web アプリでは権限は必須と言えます。
Pundit を使うと Rest ベースでシンプルに実装できます。$ rails g pundit:install $ rails g pundit:policy userまずは application_policy.rb に管理者かどうかの admin? を追加してみます。
app/policies/application_policy.rbclass ApplicationPolicy attr_reader :user, :record def initialize(user, record) @user = user @record = record end def index? false end def show? false end def create? false end def new? create? end def update? false end def edit? update? end def destroy? false end + def admin? + @user['role'] == Settings.role.admin + end class Scope attr_reader :user, :scope def initialize(user, scope) @user = user @scope = scope end def resolve scope.all end end end続いて、user_policy.rb の index に先ほど作った admin? を追加します。
app/policies/user_policy.rbclass UserPolicy < ApplicationPolicy class Scope < Scope def resolve scope.all end end + def index? + admin? + end endあとは利用側です。
application_controller.rb で Pundit を include し、current_user メソッドを追加します。
また、Pundit の NotAuthorizedError を拾えるように rescue_from を追加します。
(エラー処理は増えてきたら concerns に切り出すと良いと思います。)app/controllers/application_controller.rbclass ApplicationController < ActionController::API include ActionController::HttpAuthentication::Token::ControllerMethods + include Pundit before_action :set_session before_action :require_login @session = {} + rescue_from Pundit::NotAuthorizedError do |e| + render json: { detail: e.message }, status: :unauthorized + end + def require_login render json: { error: 'unauthorized' }, status: :unauthorized if @session.empty? end + def current_user + @session + end private def set_session authenticate_with_http_token do |token, options| @session = Session.get(token) end end end後は users_controller.rb の index に Pundit を追加します。
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :set_user, only: [:show, :update, :destroy] # GET /users def index - @users = User.all - - render json: @users + users = authorize Pundit.policy_scope(@session, User) + render json: users end # GET /users/1 def show render json: @user end # POST /users def create @user = User.new(user_params) if @user.save render json: @user, status: :created, location: @user else render json: @user.errors, status: :unprocessable_entity end end # PATCH/PUT /users/1 def update if @user.update(user_params) render json: @user else render json: @user.errors, status: :unprocessable_entity end end # DELETE /users/1 def destroy @user.destroy end private # Use callbacks to share common setup or constraints between actions. def set_user @user = User.find(params[:id]) end # Only allow a trusted parameter "white list" through. def user_params params.require(:user).permit(:name, :email, :role, :password_digest, :register_user, :update_user, :lock_version, :activated_at, :deleted_at) end endさて、動作確認してみましょう。
まずは member ユーザーでログインし、/users を叩きます。
ユーザー一覧が取得できないことを確認します。$ curl -s -X POST http://localhost:3000/auth -H "Content-Type: application/json" -d '{"email": "member@example.com", "password": "password1234"}' {"token":"482125111d11c2882cad25b700221a45bc64d8745771f427cb8039337c0e7cda95fc8498f98d6d784781b177afe84699bccb2d62628b1bee3ccee0e96eb9e576"} $ curl -s http://localhost:3000/users -H "Content-Type: application/json" -H "Authorization: Token 482125111d11c2882cad25b700221a45bc64d8745771f427cb8039337c0e7cda95fc8498f98d6d784781b177afe84699bccb2d62628b1bee3ccee0e96eb9e576" {"error":"not allowed to index? this User::ActiveRecord_Relation"}続いて admin ユーザーでログインし、/users を叩きます。
結果が取得できましたね$ curl -s -X POST http://localhost:3000/auth -H "Content-Type: application/json" -d '{"email": "admin@example.com", "password": "password1234"}' {"token":"a87dee3d5cb1592e3b3b09f78931e26254292f1453ea77a1d491fd949ce508d5ea7e00dec6163fb83ed9c7f5d39b672aa429c770a6d0487952022fc55a493487"} $ curl -s http://localhost:3000/users -H "Content-Type: application/json" -H "Authorization: Token a87dee3d5cb1592e3b3b09f78931e26254292f1453ea77a1d491fd949ce508d5ea7e00dec6163fb83ed9c7f5d39b672aa429c770a6d0487952022fc55a493487" [{"id":1,"name":"admin","email":"admin@example.com","role":"admin","password_digest":"$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK","register_user":1,"update_user":1,"lock_version":0,"activated_at":"2020-02-03T00:00:00.000Z","deleted_at":null,"created_at":"2020-02-11T12:14:52.557Z","updated_at":"2020-02-11T12:14:52.557Z"},{"id":2,"name":"member","email":"member@example.com","role":"member","password_digest":"$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK","register_user":1,"update_user":1,"lock_version":0,"activated_at":"2020-02-03T00:00:00.000Z","deleted_at":null,"created_at":"2020-02-11T12:14:52.565Z","updated_at":"2020-02-11T12:14:52.565Z"}]
- 投稿日:2020-02-22T12:38:05+09:00
正規表現_validations_定められた文字を文字列に含む
【問題】
2行目のvalidationを完成させてください。ただし条件は、「TECH::EXPERT」という文字が投稿された文字列に含まれていることを確かめること。class Impression < ActiveRecord::Base
VALID_YOUTUBE_URL_REGEX = ???
validates :url, presence: true, format: { with: VALID_TECH::EXPERT_URL_REGEX }
end【解答】
class Impression < ActiveRecord::Base
VALID_YOUTUBE_URL_REGEX = /\A.TECH::EXPERT.\z/
validates :text, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }
end【解説】
class Impression < ActiveRecord::Base
- 投稿日:2020-02-22T12:38:05+09:00
正規表現validation_定められた文字を文字列に含む
【問題】
2行目のvalidationを完成させてください。ただし条件は、「YOUTUBE」という文字が投稿された文字列に含まれていることを確かめること。class Impression < ActiveRecord::Base
VALID_YOUTUBE_URL_REGEX = ???
validates :url, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }
end【解答】
class Impression < ActiveRecord::Base
VALID_YOUTUBE_URL_REGEX = /\A.* YOUTUBE. *\z/
validates :text, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }
end【解説】
1)class Impression < ActiveRecord::BaseRailsでActive Recordのバリデーション (検証: validation) 機能を使って、オブジェクトがデータベースに保存される前にオブジェクトの状態を検証。バリデーションは、正しいデータだけをデータベースに保存するために行われます。正しいデータだけをデータベースに保存するのであれば、model.rbモデルレベルでバリデーションを実行するのが最適です。モデルレベルでのバリデーションは、データベースに依存せず、エンドユーザーがバイパスすることもできず、テストもメンテナンスもやりやすいためです。Railsではバリデーションを簡単に利用できるよう、一般に利用可能なビルトインヘルパーが用意されており、独自のバリデーションメソッドも作成できるようになっています。
2)VALID_YOUTUBE_URL_REGEX = /\A.* YOUTUBE .*\z/
Regexは、「Regular Expression、正規表現」の略。
「/」正規表現オブジェクトを作成する時に「/パターン/」の形式で作成が可能となるため操作したい文字列をスラッシュで挟む
「\A」文字列の先頭
「\z」文字列の末尾
「.」は改行を除く任意の1文字にマッチ。ただし、複数行モードでは改行にもマッチする(例は後述の参考URLの3番目ご参照)
「*」は直前の正規表現の0回以上の反復(例は後述の参考URL、最後のURLご参照)3)validates :text, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }
validates: 検証対象のフィールド名を指定します。, presence: true presenceヘルバーは、指定された属性が「空でない」ことを確認します。trueで常に検証することを指定する、つまり入力済みかを常に検証, format: with:属性の値}
formatヘルパーは、withオプションで与えられた正規表現と属性の値がマッチするかどうかのテストによる検証を行います。デフォルトのエラーメッセージは「is invalid」です。【参考】
https://railsguides.jp/active_record_validations.html
https://docs.ruby-lang.org/ja/latest/doc/spec=2fregexp.html
https://www.javadrive.jp/ruby/regex/repeat/index1.html
https://www.javadrive.jp/ruby/regex/repeat/index2.html
- 投稿日:2020-02-22T12:38:05+09:00
正規表現validation_定められた文字が投稿された文字列に含まれる
【問題】
2行目のvalidationを完成させてください。ただし条件は、「YOUTUBE」という文字が投稿された文字列に含まれていることを確かめること。class Impression < ActiveRecord::Base
VALID_YOUTUBE_URL_REGEX = ???
validates :url, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }
end【解答】
class Impression < ActiveRecord::Base
VALID_YOUTUBE_URL_REGEX = /\A.* YOUTUBE. *\z/
validates :text, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }
end【解説】
1)class Impression < ActiveRecord::BaseRailsでActive Recordのバリデーション (検証: validation) 機能を使って、オブジェクトがデータベースに保存される前にオブジェクトの状態を検証。バリデーションは、正しいデータだけをデータベースに保存するために行われます。正しいデータだけをデータベースに保存するのであれば、model.rbモデルレベルでバリデーションを実行するのが最適です。モデルレベルでのバリデーションは、データベースに依存せず、エンドユーザーがバイパスすることもできず、テストもメンテナンスもやりやすいためです。Railsではバリデーションを簡単に利用できるよう、一般に利用可能なビルトインヘルパーが用意されており、独自のバリデーションメソッドも作成できるようになっています。
2)VALID_YOUTUBE_URL_REGEX = /\A.* YOUTUBE .*\z/
Regexは、「Regular Expression、正規表現」の略。
「/」正規表現オブジェクトを作成する時に「/パターン/」の形式で作成が可能となるため操作したい文字列をスラッシュで挟む
「\A」文字列の先頭
「\z」文字列の末尾
「.」は改行を除く任意の1文字にマッチ。ただし、複数行モードでは改行にもマッチする(例は後述の参考URLの3番目ご参照)
「*」は直前の正規表現の0回以上の反復(例は後述の参考URL、最後のURLご参照)3)validates :text, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }
validates: 検証対象のフィールド名を指定します。, presence: true presenceヘルバーは、指定された属性が「空でない」ことを確認します。trueで常に検証することを指定する、つまり入力済みかを常に検証, format: with:属性の値}
formatヘルパーは、withオプションで与えられた正規表現と属性の値がマッチするかどうかのテストによる検証を行います。デフォルトのエラーメッセージは「is invalid」です。【参考】
https://railsguides.jp/active_record_validations.html
https://docs.ruby-lang.org/ja/latest/doc/spec=2fregexp.html
https://www.javadrive.jp/ruby/regex/repeat/index1.html
https://www.javadrive.jp/ruby/regex/repeat/index2.html
- 投稿日:2020-02-22T10:08:06+09:00
Ruby on Rails チュートリアル学習記録 第1章
はじめに
初投稿なので簡単に自己紹介。
私は独学でプログラミング学習を始めたアラサーの男性です。
未経験からエンジニアへの転職を目指しています。
ProgateでHTML/CSS/Ruby/Ruby on Rails5をひと通り学習後Railsチュートリアル開始。
Progateと違い詰まったら自分で調べたり考えたりしながら進めていく必要がありそうなので学習記録を付けることにしました。
主に自分が詰まったところについて書いていくつもりなので章ごとに文章量は疎らになっていくと思います。1.1
特になし
1.2
特になし
1.3
gemfileの内容を書き換え
bundle install
を実行するとエラー発生You have requested: spring = 2.0.2 The bundle currently has spring locked at 2.1.0. Try running `bundle update spring` If you are updating multiple gems in your Gemfile at once, try passing them all to `bundle update`
bundle update
を実行後、bundle install
を再度実行で解決。1.4
Rails チュートリアルにおいては、マニュアル作成時点ではGithubで非公開型レポジトリを無料で使えなかったためにBitbucketを使用していようだが、現在はGitHubでも非公開型レポジトリを無料で使えるようになっていたので、自分は既にアカウントを作っていたGitHubを使用。
GitHubでもチュートリアル通りに進めて特に問題は発生せず。1.5
特になし
1.6
特になし
感想
マニュアル通りに進めていれば特に困ることはなかった。
正直やってることの意味はきちんと理解できていない部分もあると思うが、どうせ2週やるつもりなのでそこはスピード感重視で。
ファイルの内容を変更した後、保存をし忘れてうまくいかないというケアレスミスが何度かあったので気を付けたい。
- 投稿日:2020-02-22T07:46:03+09:00
【これで無理なら諦めろ】オブジェクト指向を世界一分かりやすく説明する
はじめに
個人的にオブジェクト指向を理解するのに苦戦しまくったので、その真髄を世界一分かりやすく説明していきます。
何か間違いなどあればどんどんご教示お願いします。言語はRubyです。対象読者
- Ruby、Railsを勉強中の方
- 業務でRubyを使っている方
- チェリー本やRailsチュートリアルで挫折しちゃった人
オブジェクト指向とは
オブジェクト指向とは、プログラムを「手順」ではなく、「モノの作成と反応」として見る考え方です。これだけではイマイチ分からないと思うので、具体的なコードを使って説明していきます。
オベジェクト指向を理解するには、手続き型のコードを知るのが早いです。
例えば、ある数字を加工して、その数字を使って文章を作って、結果を出力するとき、手続き型では以下のように書きます。
def calculate(number_1, number_2) number_1 * number_2 + 1 end def update_content(value) "結果は#{value}です" end def output(content) puts number end value = 100 content = "" new_value = calculate(value,20) // 2001 content = update_content(new_value) // "結果は2001です" output(content) => "結果は2001です"一方で、オブジェクト指向で書くとこんな感じ。
num = Num.new(100,"") num.calculate(20) // 2001 num.update_content // "結果は2001です" num.output => "結果は2001です"なんとなく違いは分かりますか?
前者は、先に関数を定義して、処理を実行するたびにいろんなメソッドに値を飛ばして、返ってきた値を変数に代入して...
と流れるようにプログラムが進んでいきます。なんだか忙しそうですよね?
一方で後者は、はじめに
num
というモノを作成し、そいつに対して「計算して」「中身を更新して」「出力して」と呼びかけるような感じで処理が進んでいきます。情報の記憶も処理の内容も「モノ」が全部やってくれるので、一度作ってしまえば見通しよく実行できるわけです。
この後者こそがまさに、オブジェクト指向の考え方であり、今回でいう所の
num
がオブジェクトと呼ばれるものです。つまり、オブジェクト指向とは、コードにオブジェクトという主人公を登場させ、それをメインに話を進めていく設計の考え方といえます。
やっぱり、ドラマを見るにも、主人公がいた方が面白いし分かりやすいじゃないですか。
プログラミングも同じで、処理の中に主人公がいた方が読みやすくて扱いやすくなるわけです。ちなみにnumというオブジェクトは以下のクラスから作成されていました。へぇ〜ぐらいで流してください。
num.rbclass Num attr_accessor :value, :content def initialize(value, content) @value = value @content = content end def calculate(number) self.value = value * number + 1 end def update_content self.content = "計算結果は#{value}です" end def output puts content end end num = Num.new(100,"") num.calculate(100) num.update_content num.outputオブジェクトとは
先ほどの例で少しはオブジェクト指向の雰囲気を掴めたかと思います。
でば、オブジェクトとは一体なんでしょうか?こいつの正体を掴むのに僕は半年ほどかかりました。そして、僕が腑に落ちた例えで分かりやすーーーく説明します。
結論から言うと、オブジェクトとは魔法の箱です。
この魔法の箱は2つの性質を持ちます。
- 情報を持つ
- 反応する
プログラミングをやっていると、オブジェクトが色んなことをしているように見えますが、結局やっていることはこの2つだけなんです。
情報を保持するから「箱」、メッセージに対して反応できるから「魔法の」と言う例えにしました。
オブジェクトとは、情報を持ち、メッセージに対して反応するただの箱なんだ。
とりあえず今の段階ではそのように理解してもらえれば大丈夫です。
1. 情報を持つとは?
オブジェクトは情報を持つことができます。分かりやすいのが以下の例。
user.name => 山田 user.age => 18これはuserという魔法の箱が
name
とage
という2つの情報を持っていることを示しています。すごく簡単ですね。この情報はいつでも呼び出すことができますし、その気になれば更新することも消すこともできます。
2. 反応するとは?
オブジェクトはあるメッセージに対して反応することもできます。
user.hello => "おはようございます。"これまで5万回は見たhelloの例です。
これは、hello
というメッセージを伝えたら"おはようございます。"
と返すようにuser
という魔法の箱が反応したと見ることができます。userをレシーバ、helloをメッセージと呼ぶのはこのような理由です。
ちなみに、そこらへんのゴミ箱にhelloと話しかけても何も返事しません。
dustbox.hello => NameError (undefined local variable or method `dustbox' for main:Object)userという箱が反応してくれたのは、事前に反応パターンを記憶させたからです。
class User def hello puts "おはようございます。" end endこのように、魔法の箱(オブジェクト)に教え込む反応方法のことをメソッドと言います。
では、この教えこむ場所のことを(学校の)クラスと呼ベばかなり分かりやすいのではないかと閃きました。
言い換えれば、classとは魔法の箱の養成クラスとでもいいましょうか。すごく簡単にまとめると...
- オブジェクトとは魔法の箱である
- 魔法の箱は、情報を持ったり、メッセージに対して反応したりできる
- 魔法の箱に教え込む反応パターンのことをメソッドと呼ぶ
- 反応パターンを教え込む場所のことをクラスと呼ぶ
- オブジェクト指向とは、プログラムを「手順」ではなく、「魔法の箱(オブジェクト)の反応」と見る考え方
以上が、オブジェクト指向の説明になります。
この記事でオブジェクト指向への理解が少しでも深まれば幸いです。追記1
Numクラスのソースコードに間違いがあったため、修正いたしました。
ご指定いただいた方々ありがとうございます。変更点
Class Num => class Num
へ修正- 属性名を
num => value
へ変更。(属性名とインスタンス名が同じだとややこしいため)- caluculateメソッドにおける、
value = => self.value
に修正(属性を上書きする際はself.の省略はできないようです。)
- 投稿日:2020-02-22T07:44:31+09:00
【Ruby on Rails で簡単!】PAY.JPを利用したクレジットカード決済の導入
何かサービスを作る際に、決済機能を導入したいはずです。
今回はPAY.JPを利用した決済機能を案内していきます。PAY.JPの導入準備
スクリプトの記述
PAY.JPを使うためのスクリプトを記述します。
下記をコピーしてください。スクリプト%script{src: "https://js.pay.jp/", type: "text/javascript"}コピーしたら、application.html.hamlに貼り付けましょう!!
application.html.haml%html %head %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ %title payjptest %script{src: "https://js.pay.jp/", type: "text/javascript"} -# このscriptを記載 = csrf_meta_tags = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' = javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %body = yieldPAY.JPの登録
PAY.JPのアカウントを作成しましょう!
https://pay.jp/APIキーを取得します
アカウントを作成してログインし、下記の場所のAPIキーを確認しましょう!
Appにgem 'payjp'を追加
Gemfile# PAY.JPのgem gem 'payjp' # 環境変数を簡単に定義できるENVファイルを対応させるgem gem 'dotenv-rails'追加したら
ターミナル$ bundle install $ rails s再起動しないとgemもscriptも読み込まれないので、エラーがおきます。
環境変数を利用して、APIキーをAppに登録
gem 'dotenv-rails'をインストールできたので、.envファイルを作成しましょう
app > .env の場所に作成します。
.gitignoreの上だと思えば簡単です。
秘密鍵をGithubにコミットしてしまうとAPIキーを世に公開してしまうので、.gitignoreに.envを記述します
gitignore/.env
ここ本当に重要なので、注意してくださいね!
では、APIキーを記述します
.envPAYJP_PRIVATE_KEY = 'sk_test_111111111111111111111111' PAYJP_KEY = 'pk_test_111111111111111111111111'記載したら、Githubのコミットに表示されていないか?確認します。
コミットに表示されてたら、ヤバイです。
危険です。.gitignoreを再確認してください。ここまでで準備完了です。
素材作成(view):ここは自身で記述すると良いかと思います。
ここはviewですが、必要なければ飛ばしてください。
クレジットカードを登録するためのviewを作成します。
下記はformの部分テンプレートですが下記のようなスクリプト
記述#{asset_path 'creditcards/master-card.svg'}master-card.svgの画像素材がないとエラーがおきますので、
コピペするなら素材を集めてください。form
formの部分テンプレート= form_with url:creditcards_path, method: :post, html: { name: "inputForm" },class:"form" do |f| .form__upper .form__upper__group = f.label :カード番号 %span.form-require 必須 = f.text_field :card_number, name: "card_number", id:"card_number", type: "text", placeholder: '半角数字のみ', class: 'input-default', maxlength: "16" %ol %li %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/visa.svg'}", width:"35", height:"20"} %li %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/master-card.svg'}", width:"35", height:"20"} %li %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/saison-card.svg'}", width:"35", height:"20"} %li %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/jcb.svg'}", width:"35", height:"20"} %li %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/american_express.svg'}", width:"35", height:"20", class:"american_express"} %li %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/dinersclub.svg'}", width:"35", height:"20"} %li %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/discover.svg'}", width:"35", height:"20"} .form__upper__group.exp .name = f.label :有効期限 %span.form-require 必須 = f.select :exp_year, [["19",2019],["20",2020],["21",2021],["22",2022],["23",2023],["24",2024],["25",2025],["26",2026],["27",2027],["28",2028],["29",2029]], {}, class: 'input-default harf', name: "exp_year", id:"exp_year" %span 年 = f.select :exp_month, [["01",1],["02",2],["03",3],["04",4],["05",5],["06",6],["07",7],["08",8],["09",9],["10",10],["11",11],["12",12]],{}, class: 'input-default harf', name: "exp_month", id:"exp_month" %span 月 .form__upper__group = f.label :セキュリティーコード %span.form-require 必須 = f.text_field :cvc, name: "cvc", id:"cvc", class:"cvc", type: "text", placeholder: 'カード背面4桁もしくは3桁の番号', class: 'input-default', maxlength: "16" .form__upper__group %p.about = fa_icon 'question-circle' %span = link_to 'カード裏面の番号とは?', root_path, class:'about__registered' .form__upper__group = f.submit '次へ進む', class: 'btn-default', id: "charge-form"data: "#{asset_path 'creditcards/visa.svg'}"は、
【 app > asset > images > creditcards > visa.svg 】を読み込む
という設定になります。scss.form{ &__upper{ margin: 0 auto; max-width: 343px; p { text-align: center; } &__group { font-size: 14px; color: #333; &:not( :first-child ){ margin-top: 32px; } label{ font-weight: 600; } ol{ display:flex; li { margin: 5px 8px 0 0; } } .form-require { background-color: $green; color: #fff; font-size: 12px; margin: 0 0 0 8px; padding: 2px 4px; border-radius: 2px; vertical-align:top; &-optional { background-color: gray; color: #fff; font-size: 12px; margin: 0 0 0 8px; padding: 2px 4px; border-radius: 2px; vertical-align:top; } } .input-default{ width: 90%; margin: 8px 0 0; height: 48px; padding: 10px 16px 8px; border-radius: 4px; border: 1px solid #ccc; background: #fff; line-height: 1.5; font-size: 16px; &.harf{ width: calc(40% - 6px); margin: 8px 8px 0 0; height: 45px; border-radius: 4px; border: 1px solid #ccc; background: #fff; line-height: 1.5; font-size: 16px; &.exp{ -webkit-appearance: none; -moz-appearance: none; appearance: none; } } &-select{ width: 76px; margin-top: 8px; height: 45px; border-radius: 4px; border: 1px solid #ccc; background: #fff; line-height: 1.5; font-size: 16px; } } h3 { font-size: 16px; font-weight: bold; } .attention{ margin: 8px 0 0; } .agree{ text-align: center; } a { color: #0099e8; text-decoration: none; } span { margin: 0 2px; } .about{ text-align: right; &__registered{ color: #0099e8; text-decoration: none; } .fa-chevron-right { color: #0099e8; } .fa-question-circle { color: #0099e8; font-size: 1rem } } } .form-info-text { color: #888; margin-top: 8px; font-size: 14px; } } &__bottom { margin: 0 auto; max-width: 343px; .registance { text-align: right; } } .btn-default { width: 100%; height: 50px; background-color: $green; color: #FFFFFF; font-size: 15px; cursor: pointer; } .btn-registration{ color: #fff; border-radius: 4px; width: 50%; line-height: 48px; border: 1px solid transparent; text-align: center; margin: 0 auto; position: relative; i{ font-size:20px; position: absolute; top:13.5px; left:15px; } .about__registered { color: #FFFFFF; text-decoration: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: pointer; } &.email{ background-color: $green; } &.facebook{ background-color:#385184; } &.google{ background-color:#FFFFFF; color: black; background: #fff image-url('google.svg') no-repeat 3px top; border: #979797 solid 1px; } &:not( :first-child ){ margin-top: 16px; } } }new.html.haml= render 'shared/main-header' .container-fluid .row.py-5.w-100 = render 'shared/mypage-side' .mypage-main.col-9 %h2.header_title 支払い方法 .single-container .rgs-main__section = render "shared/creditcard-form" .payment-explain = fa_icon 'chevron-right', class:"arrows" = link_to '支払い方法について', '#'モデル作成
ターミナル$ rails g model creditcardcreditcardsテーブル
Column Type Options user_id references foreign_key: true, null: false payjp_id string null: false Association
- belongs_to :user
ということでマイグレーションファイルは下記になります。
migrationファイルclass CreateCreditcards < ActiveRecord::Migration[5.2] def change create_table :creditcards do |t| t.references :user, foreign_key: true, null: false t.string :payjp_id, null: false t.timestamps end end endここは他の記事と異なります。
- user_id: AppのUser-ID
- payjp_id: PAYJPのUser-ID
他の記事だとカード用のカラムも作成していますが、PAYJPのアカウントから引っ張りだせばいいので不要です。
本題のjQueryです。
Payjp.js$(document).on('turbolinks:load',function(){ // PAY.JPの公開鍵をセットします。 Payjp.setPublicKey('pk_test_111111111111111111'); //formのsubmitを止めるために, クレジットカード登録のformを定義します。 var form = $(".form"); $("#charge-form").click(function() { // submitが完了する前に、formを止めます。 form.find("input[type=submit]").prop("disabled", true); // submitを止められたので、PAY.JPの登録に必要な処理をします。 // formで入力された、カード情報を取得します。 var card = { number: $("#card_number").val(), cvc: $("#cvc").val(), exp_month: $("#exp_month").val(), exp_year: $("#exp_year").val(), }; // PAYJPに登録するためのトークン作成 Payjp.createToken(card, function(status, response) { if (response.error){ // エラーがある場合処理しない。 form.find('.payment-errors').text(response.error.message); form.find('button').prop('disabled', false); } else { // エラーなく問題なく進めた場合 // formで取得したカード情報を削除して、Appにカード情報を残さない。 $("#card_number").removeAttr("name"); $("#cvc").removeAttr("name"); $("#exp_month").removeAttr("name"); $("#exp_year").removeAttr("name"); var token = response.id; form.append($('<input type="hidden" name="payjpToken" />').val(token)); form.get(0).submit(); }; }); }); });コントローラーの記述
コントローラー(new&create)の作成
creditcards_controller.rbclass CreditcardsController < ApplicationController require "payjp" before_action :set_card def new # cardがすでに登録済みの場合、indexのページに戻します。 @card = Creditcard.where(user_id: current_user.id).first redirect_to action: "index" if @card.present? end def create # PAY.JPの秘密鍵をセット(環境変数) Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"] # jsで作成したpayjpTokenがちゃんと入っているか? if params['payjpToken'].blank? # トークンが空なら戻す render "new" else # トークンがちゃんとあれば進めて、PAY.JPに登録されるユーザーを作成します。 customer = Payjp::Customer.create( description: 'test', email: current_user.email, card: params['payjpToken'], metadata: {user_id: current_user.id} ) # PAY.JPのユーザーが作成できたので、creditcardモデルを登録します。 @card = Creditcard.new(user_id: current_user.id, payjp_id: customer.id) if @card.save redirect_to action: "index", notice:"支払い情報の登録が完了しました" else render 'new' end end end private def set_card @card = Creditcard.where(user_id: current_user.id).first if Creditcard.where(user_id: current_user.id).present? end endコントローラーの追記(index、destory)
creditcards_controller.rbclass CreditcardsController < ApplicationController require "payjp" before_action :set_card def index # すでにクレジットカードが登録しているか? if @card.present? # 登録している場合,PAY.JPからカード情報を取得する # PAY.JPの秘密鍵をセットする。 Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"] # PAY.JPから顧客情報を取得する。 customer = Payjp::Customer.retrieve(@card.payjp_id) # PAY.JPの顧客情報から、デフォルトで使うクレジットカードを取得する。 @card_info = customer.cards.retrieve(customer.default_card) # クレジットカード情報から表示させたい情報を定義する。 # クレジットカードの画像を表示するために、カード会社を取得 @card_brand = @card_info.brand # クレジットカードの有効期限を取得 @exp_month = @card_info.exp_month.to_s @exp_year = @card_info.exp_year.to_s.slice(2,3) # クレジットカード会社を取得したので、カード会社の画像をviewに表示させるため、ファイルを指定する。 case @card_brand when "Visa" @card_image = "visa.svg" when "JCB" @card_image = "jcb.svg" when "MasterCard" @card_image = "master-card.svg" when "American Express" @card_image = "american_express.svg" when "Diners Club" @card_image = "dinersclub.svg" when "Discover" @card_image = "discover.svg" end end end def new @card = Creditcard.where(user_id: current_user.id).first redirect_to action: "index" if @card.present? end def create Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"] if params['payjpToken'].blank? render "new" else customer = Payjp::Customer.create( description: 'test', email: current_user.email, card: params['payjpToken'], metadata: {user_id: current_user.id} ) @card = Creditcard.new(user_id: current_user.id, payjp_id: customer.id) if @card.save if request.referer&.include?("/registrations/step5") redirect_to controller: 'registrations', action: "step6" else redirect_to action: "index", notice:"支払い情報の登録が完了しました" end else render 'new' end end end def destroy # 今回はクレジットカードを削除するだけでなく、PAY.JPの顧客情報も削除する。これによりcreateメソッドが複雑にならない。 # PAY.JPの秘密鍵をセットして、PAY.JPから情報をする。 Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"] # PAY.JPの顧客情報を取得 customer = Payjp::Customer.retrieve(@card.payjp_id) customer.delete # PAY.JPの顧客情報を削除 if @card.destroy # App上でもクレジットカードを削除 redirect_to action: "index", notice: "削除しました" else redirect_to action: "index", alert: "削除できませんでした" end end private def set_card @card = Creditcard.where(user_id: current_user.id).first if Creditcard.where(user_id: current_user.id).present? end end登録したカード情報を表示
index.html.haml.mypage-main.col-9 %h2.header_title 支払い方法 .single-container %section.creditcard_section %h3 クレジットカード一覧 - if @card.present? .container .creditcard-info = image_tag "creditcards/#{@card_image}",width:'34',height:'20', alt:'master-card' %p.creditcard-info__number = "**** **** **** " + @card_info.last4 #クレジットカードの下4桁を表示 %p.creditcard-info__period = @exp_month + " / " + @exp_year = button_to "削除する", creditcard_path(@card), method: :delete, class:"creditcard-info__delete" - else .new-card = link_to new_creditcard_path, class:"new-card-btn" do %i.far.fa-credit-card クレジットカードを追加する購入処理を追加
creditcards_controller.rbdef buy @product = Product.find(params[:product_id]) # すでに購入されていないか? if @product.buyer.present? redirect_back(fallback_location: root_path) elsif @card.blank? # カード情報がなければ、買えないから戻す redirect_to action: "new" flash[:alert] = '購入にはクレジットカード登録が必要です' else # 購入者もいないし、クレジットカードもあるし、決済処理に移行 Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"] # 請求を発行 Payjp::Charge.create( amount: @product.price, customer: @card.customer_id, currency: 'jpy', ) # 売り切れなので、productの情報をアップデートして売り切れにします。 if @product.update(buyer_id: current_user.id) flash[:notice] = '購入しました。' redirect_to controller: 'products', action: 'show', id: @product.id else flash[:alert] = '購入に失敗しました。' redirect_to controller: 'products', action: 'show', id: @product.id end end endあとはボタンを押したら、buyアクションが動くようにすれば、完了です!!
以上です
お疲れ様です。参考リンク
新規登録時にクレジットカード登録
購入処理
テストカード
- 投稿日:2020-02-22T01:41:22+09:00
【Ruby on Rails】gem 'ransack' を利用した検索機能/フォームの実装
検索機能をgemを使って簡単に実装しましょう!
gem 'ransack'を利用した検索機能実装
インストール
Gemfilegem 'ransack'インストールしましょう
ターミナル$ bundle install検索機能の作成
application_controller.rbbefore_action :set_search def set_search @search = Product.ransack(params[:q]) #ransackの検索メソッド @search_products = @search.result(distinct: true).order(created_at: "DESC").includes(:user).page(params[:page]).per(5) # productsの検索結果一覧 # 最終的に、@search_productsを検索結果画面(例:search.html.haml)に挿入します。 # 検索結果の一覧: @search_products = @search.result.order(created_at: "DESC") # distinct: trueは検索結果のレコード重複しないようにします。 # ページネーション: .includes(:user).page(params[:page]).per(5 endapplication_controller.rbで定義します。
products_controller.rbに書くとproducts_controller.rbを使わないページを表示した時にエラーになるため、application_controller.rbで記述。
kaminariでページネーションをつけているのでpage(params[:page])をつけた。
searchメソッドよりransackメソッド推奨なのでransackメソッドを使った。ransackの検索オプション
検索方法 意味(英語) 意味 *_eq equal 等しい *noteq not equal 等しくない *_lt less than より小さい *_lteq less than or equal より小さい(等しいものも含む) *_gt grater than より大きい *_gteq grater than or equal より大きい(等しいものも含む) *_cont contains value 部分一致(内容を含む) 今回使ったのは*_contですね
検索フォームの作成
検索.html.haml.search_wap = search_form_for @search do |f| = f.text_field :name_cont , placeholder: "何かお探しですか?", class: 'input' = button_tag type: 'submit', class: 'search__button' do .icon_wap %i.fas.fa-searchhaml形式でフォームを作成した。
この段階では、url:〇〇_pathはつけていません。あとで付与していきます。
scssのmixinも記載しておきます。mixin_search@mixin input{ border: none; border-radius: 50px; background: #F4F8F9; height: 37px; width: 100%; padding: 10px 20px; } .search_wap{ margin-right: 15px; position: relative; .input{ @include input; margin: 13px 0 0; } .search__button{ background-color: transparent; cursor: pointer; position: absolute; right: 0px; top: 13px; height: 37px; width: 40px; border-style: none; border-radius: 0px 4px 4px 0px; padding: 0px; .icon_wap{ i{ position: absolute; right: 14px; top: 13px; font-size: 13px; font-weight: 900; color: #999999; } } } }検索結果のview作成
あとは、検索画面を作成して、検索結果である@search_productsを挿入してあげれば完成です。
検索結果用の画面に移行するために、route.rbにget :searchを追加します。
route.rbRails.application.routes.draw do resources :products do collection do get :search end end endでは、検索フォームにurlを追加しましょう
検索.html.haml.search_wap = search_form_for @search, url: search_users_path do |f| = f.text_field :name_cont , placeholder: "何かお探しですか?", class: 'input' = button_tag type: 'submit', class: 'search__button' do .icon_wap %i.fas.fa-searchview用のhtml.hmlとコントローラーを記述します。
products_controller.rbclass UsersController < ApplicationController def search end endsearch.html.haml= render 'shared/main-header' .container-fluid .row.py-5.w-100 .jscroll = render partial: 'user', collection: @search_products, as: "product", class: "jscroll" // @search_productはapplication_controller.rbで定義している。@search_productで検索結果のProductを表示させるので、indexで作成したviewに@productsを@search_productに書き換えれば、出来上がりです。
まとめ
検索処理は下記だけ
application_controller.rbbefore_action :set_search def set_search @search = Product.ransack(params[:q]) #ransackの検索処理 @search_products = @search.result # 検索結果 end検索フォームを作って
検索.html.haml.search_wap = search_form_for @search do |f| = f.text_field :name_cont , placeholder: "何かお探しですか?", class: 'input' = button_tag type: 'submit', class: 'search__button' do .icon_wap %i.fas.fa-searchあとはviewに@search_productsを挿入すれば完成。
if文を追加する
if文を追加するなら、下記の3パターンがいいかもしません
例// 検索キーワードなし: 空白だとスペースを入れいてるname全部がヒットしてしまうため - if params[:q]['name_cont'] == "" = "検索キーワードがありません。" // 検索結果ありの場合 - elsif @search_products.present? = "「#{params[:q][:name_cont]}」の検索結果: #{@search_products.count}個" // 検索数0の場合 - else = "検索に一致する商品はありませんでした"では実際に書いていきましょう!
参考リンク
ransackでRailsアプリのヘッダーに検索機能をつける
【Rails】ヘッダーへの検索機能の付け方
Rubyon Rails で検索機能を作ろう(ransack)
[Rails]ransackを利用した色々な検索フォーム作成方法まとめ
Rails 5.1とBootstrapで作るシンプルな検索機能のテンプレ
- 投稿日:2020-02-22T01:37:36+09:00
Railsを用いたアプリケーション開発の流れ
はじめに
Railsを用いてアプリケーション開発をするための流れを簡潔にまとめたいと思います。
概要
以下の流れで実装を行い、トップページにデータベースに保存してあるデータを表示する機能を作成します。
1.アプリケーションの土台となるものを作る
2.データベースを作る
3.ルーティングの設定
4.コントローラの作成
5.ビューの作成
6.モデルの作成
7.ビューにインスタンス変数を埋め込む1.アプリケーションの土台となるものを作る
まずはアプリケーション用のディレクトリを作成してください。
そのディレクトリの直下で以下のコマンドを入力します。x.x.xの部分はrailsのバージョンを入れてください。
rails _x.x.x_ new アプリケーション名 -d 使用するデータベース
これでコマンドを実行したディレクトリの直下に様々なファイルが作成されたはずです。これがアプリケーションの土台となります。2.データベースを作る
以下のコマンドでアプリケーションで使用するデータベースを作成する事ができます。
rails db:create
実際にデータベースを確認して見ましょう。「アプリケーション名_development」と「アプリケーション名_test」というデータベースが作成されているはずです。3.ルーティングの設定
「1.アプリケーションの土台となるものを作る」を実行していれば、/アプリケーション用のディレクトリ/config/app の配下にroutes.rbというファイルが作成されていると思います。このファイルに「root to: "posts#index"」を追記しましょう。
Rails.application.routes.draw do root to: "posts#index" endこれで「root」ディレクトリにリクエストがあった場合、
「posts」コントローラに処理が遷移し、
postsコントローラの「index」アクションの処理を行う、となります。4.コントローラの作成
以下のコマンドでコントローラーを作成できます。「3.ルーティングの設定」でpostsコントローラに処理が遷移するとしたため、今回はコントローラー名をpostsにしましょう。
rails g controller コントローラー名
5.ビューの作成
今回は、postsコントローラのindexアクションに対応するビューであるため、/アプリケーション用のディレクトリ/app/views/posts の配下にindex.html.erbを作成しましょう。コントローラから受け取るインデックス変数などの処理は後ほど記載します。
6.モデルの作成
ターミナルで以下のコマンドを実行しましょう。
これでモデル名をpostにすればpostsテーブルに対応するモデルができます。
rails g model モデル名
7.ビューにインスタンス変数を埋め込む
1~6までの流れでテーブルからデータを抽出する事ができ、@posts(インスタンス変数)にデータが格納されました。あとはビューを修正してインスタンス変数を画面に出力するようにすれば完成です。
# 修正例 <% @posts.each do |post|%> <div> <%= post.text %> </div> <% end %>雑感
3日ほどRubyとRailsについて学び、WEBアプリケーション制作におけるはじめの一歩が踏み出せたのではないかと思います。ちなみに筆者は漫画のはじめの一歩も大好きで、ベストバウトは間柴対木村です。
- 投稿日:2020-02-22T01:25:30+09:00
Devise4.7をRails6+ruby 2.7.0で動かす
はじめに
さっき、「Devise4.7をRails6で動かす」を書いたばかりなのにRuby 2.7.0が使えることに気づき、やり直した。
1. ruby 2.7.0 install
2.7.0があるか確認するとちゃんとあった。
$ rbenv install -l | grep ^2 途中省略 2.7.0-dev 2.7.0-preview1 2.7.0-preview2 2.7.0-preview3 2.7.0-rc1 2.7.0-rc2 2.7.0 2.8.0-dev$ rbenv install 2.7.0 Downloading openssl-1.1.1d.tar.gz... -> https://dqw8nmjcqpjn7.cloudfront.net/1e3a91bc1f9dfce01af26026f856e064eab4c8ee0a8f457b5ae30b40b8b711f2 Installing openssl-1.1.1d... Installed openssl-1.1.1d to /Users/you/.rbenv/versions/2.7.0 Downloading ruby-2.7.0.tar.bz2... -> https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.0.tar.bz2 Installing ruby-2.7.0... ruby-build: using readline from homebrew Installed ruby-2.7.0 to /Users/you/.rbenv/versions/2.7.0 Communized gems for 2.7.0 /usr/local/bin/rbenv-communal-gem-home: line 21: /usr/local/bin/../version_cache/2.7.0: Permission deniedなぜ
Permission denied
? 調べるのは面倒なのでsudoで再実行。$ sudo rbenv install 2.7.0 rbenv: /Users/you/.rbenv/versions/2.7.0 already exists continue with installation? (y/N) y Downloading openssl-1.1.1d.tar.gz... -> https://dqw8nmjcqpjn7.cloudfront.net/1e3a91bc1f9dfce01af26026f856e064eab4c8ee0a8f457b5ae30b40b8b711f2 Installing openssl-1.1.1d... Installed openssl-1.1.1d to /Users/you/.rbenv/versions/2.7.0 Downloading ruby-2.7.0.tar.bz2... -> https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.0.tar.bz2 Installing ruby-2.7.0... ruby-build: using readline from homebrew Installed ruby-2.7.0 to /Users/you/.rbenv/versions/2.7.0 Communized gems for 2.7.0$ rbenv versions system 2.3.3 2.5.1 * 2.5.3 (set by /Users/your/apps/devise-rails6/.ruby-version) 2.6.5 2.7.0 <=ちゃんとインストールされている。 2.7.0-rc1Rubyのバージョンごとにgemをインストールしないといけないらしい。
$ gem install bundler Successfully installed bundler-2.1.2 Parsing documentation for bundler-2.1.2 Installing ri documentation for bundler-2.1.2 Done installing documentation for bundler after 2 seconds 1 gem installed2. Rails newでプロジェクト作成
rbenv rehash
でRubyMineが読み取れるようにインストールしたRubyを反映させておきます。それからRubyMineでプロジェクトを作成します。ここでいろいろエラーが出て、解決するまで時間を使ったが、Railsの問題は本記事のDeviseとは関係ないので割愛する。
ところが、次のエラーが出た。
An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue. Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling.素直に
gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'
を実行したが、またエラー。$ gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/' Building native extensions. This could take a while... ERROR: Error installing mysql2: ERROR: Failed to build gem native extension. 途中省略 linking shared-object mysql2/mysql2.bundle ld: library not found for -lssl <=これが問題だ。 clang: error: linker command failed with exit code 1 (use -v to see invocation) make: *** [mysql2.bundle] Error 1 make failed, exit code 2参考記事 の下の方に下記のヒントがあった。
$> brew install mysql@5.6 $> bundle config build.mysql2 --with-mysql-config=/usr/local/Cellar/mysql\@5.6/5.6.42/bin/mysql_config $> bundle install私はMySQLのバージョンはいじりたくない(他のアプリに影響が出るのを避けたい)し、MySQLは5.7で古くないので、自分のMacのMySQL5.7に合わせて次を実行した。
$ bundle config build.mysql2 --with-mysql-config=/usr/local/Cellar/mysql@5.7/5.7.24/bin/mysql_config $ sudo bundle installこれでbundle installは成功した。
参考記事
mysql2 gem fails to compile with MySQL 5.6.12 on OS X with Homebrew
- 投稿日:2020-02-22T00:31:54+09:00
mac rbenv バージョンの違うRubyをインストールして切り替えて使用する
目的
- home brewを用いてmacにバージョン違いのRubyをインストールする方法をまとめる
実施方法概要
- home brewのインストールリストの更新
- Rubyのインストール
- Rubyバージョンの切り替え
実施方法詳細
home brewのインストールリストの更新
下記コマンドを実行してhome brewのインストールリストを更新する。
$ brew upgrade ruby-buildRubyのインストール
下記コマンドを実行して欲しいバージョンのRubyをrbenv経由でインストールする。(X.X.Xがバージョン)
$ rbenv install X.X.Xインストール確認
下記コマンドを実行して先に指定したバージョンのRubyがインストールされていることを確認する。(
*
が付いているものが現在割り当てられているバージョンになる)$ rbenv versions system 2.3.0 * 2.5.0 (set by /Users/shun/workspace/study/rails/tropical_fish_sns_of_rails/.ruby-version) 2.6.2Rubyバージョンの切り替え
下記コマンドを実行してインストールされているRubyのバージョンを変更する。(X.X.Xはバージョン、
$ rbenv versions
の出力の中から指定する。)$ rbenv global X.X.X
- 投稿日:2020-02-22T00:10:37+09:00
Rails新規アプリケーションの作り方
フレームワークRailsの作成手順(ほぼ備忘録)
ディレクトリ作成からRailsのひな形作成までを簡単にアウトプット練習も兼ねて書いていく。(DB設計は割愛)
開発環境↓
- Railsバージョン5.2.3
- データベース MySQL
ここからはターミナルを実行してひな形を作っていく。
ターミナル# ディレクトリ作成 $ mkdir ~/test
Rails new
コマンドで作成ターミナル$ rails new アプリケーション名 -オプションtest#test-appファイルの作成 $ rails _5.2.3_ new test-app -d mysql
$ cd test-app
でtest-app
ファイルに移動test-app# bundle installしておく $ bundle installこれでRailsフレームワークのひな形が完成。
htmlファイルやcssファイル、routes.rbがあることを確認しておく。
これからDBとモデルを作成する。
以上
- 投稿日:2020-02-22T00:02:24+09:00
[Rails] ActiveRecord::import で NoMethodError: undefined method `raise_record_invalid' が発生したときの対応
はじめに
ActiveRecord
を使ってDBアクセスをしている場合、バルクインサートをやってくれる gem に activerecord-import というのがある。
とても便利なライブラリなのだが、RSpec でActiveRecord::RecordInvalid
の発生を期待するテストを書いた際に当該エラーが発生しなかったので備忘録として残す。環境
バージョン 備考 Ruby v2.5.5p157 Ruby on Rails v5.2.3 activerecord-import v0.14.1 バルクインサートをやってくれる なにが発生したか
前述のとおり
ActiveRecord::RecordInvalid
が発生するテストを書いたのだが、次のエラーが発生した。expected ActiveRecord::RecordInvalid, got #<NoMethodError: undefined method `raise_record_invalid' for結論から
この現象の解決には
activerecord-import
の update を行う。( 下記のバージョンを指定してbundle install
を実行する )gem 'activerecord-import', '~> 0.15.0'発生していた理由
activerecord-import
のバグ。下記で fix の報告があった。https://github.com/zdennis/activerecord-import/pull/294
まとめにかえて
上記のプルリクエストがマージされたのは
on 3 Jul 2016
と古い。そういうわけなので、最近導入した方は本記事で取り上げた現象は発生していないと思われるが、導入したのが古いバージョンのままで更新をかけていないとハマるのでご注意を。
なんせ
ActiveRecord::import
,ActiveRecord::RecordInvalid
,NoMethodError
,undefined method raise_record_invalid
といったキーワードで検索かけても全然糸口が見つからない。
そんななか、検索でひっかかった こちら に前掲のプルリクエストがあって気づけたのは幸いでした。。。参考