20200115のRubyに関する記事は23件です。

Rails modelのjoin戦略についての理解を深める1歩

こちらの記事。よく参考にしてます。
https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58

ここで言っているキャッシュとは何か?
同記事の最後の表についてなんとなくふわっとした理解でした。
特にキャッシュする・しないについて。
それについてまとめました。

Rails modelって

そのmodelに定義されたリレーションに則ってデータを取得できます

class User < ApplicationRecord
  has_many :invoices
  ...

というUserモデルがあったとして

user = User.first
invoice = user.invoices

というふうに取得できますね
その時、invoicesはどのタイミングで取得する(SQLが発行される)かというと

user.invoices

としたタイミングです

結合してみよう

先程のmodelを使って結合してみよう

joins

user = User.joins(:invoices).first
user.invoices

1行目のタイミングで

 SELECT "users".* FROM "users" INNER JOIN "invoices" ON "invoices"."user_id" = "users"."id" ORDER BY "users"."id" ASC LIMIT $1

2行目で

SELECT "invoices".* FROM "invoices" WHERE "invoices"."user_id" = $1 

のようなSQLが発行されました。
invoicesを検索するタイミングは変わってませんね。
なるほど、これがキャッシュされたかどうかってことか。

preload

user = User.preload(:invoices).first
user.invoices

includes

preload/eager_loadをいい感じに判断して使用してくれる

1行目の時点でinvoiceも検索しに行ってます

  User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
  Invoice Load (0.3ms)  SELECT "invoices".* FROM "invoices" WHERE "invoices"."user_id" = $1  [["user_id", 1]]

2行目では特にSQLは発行されていません。
キャッシュされてますね。
そして、SQLの結合は使わずに2回SELECTすることでuser.invoicesを取得してます。

eager_load

user = User.eager_load(:invoices).first
user.invoices

Usersに対してInvoicesをLEFT OUTER JOINで結合してました。
user.invoicesではキャッシュされてるので何も発行しません。

キャッシュしないということ

joinsメソッドでは、結合先のデータを必要としないが、INNER JOINで絞り込みをしたいときに使用する。
なので、

user = User.joins(:invoices).first
user.invoices

のようなコードは無駄があるということ。
includesとwhereで絞り込みとキャッシュを行うべき。

以上

という感じ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでinputのパスワードの黒丸のところを文字で表示させてみた!!

はじめに

某プログラミングスクールで、メ○○リのコピーサイトを作成しました。
チームメンバーが実装していた、ボタンを押すとパスワードの黒丸を文字として表示させる
実装を今日はやっていきたいと思います!と思ったのですが、
すでに下記の参考記事で簡単に作成できました。
パスワード表示時にマスキング有無を選択できるようにする方法

なので、自分のメモ用で記載していきます。
ちなみにこんな感じのものを実装していきます。
a1a64a26379e2a3036884ebbf0b4aa88.gif

解説

1.まずパスワードを文字として表示していきます。

 (1)hamlを記載していきます。(一部のみ表示しています。)

password.html.haml
= form_for @resume do |f|
  = f.password_field :password,placeholder: '検索時に使用するパスワードを入力',class:"content_main9__form9--password",id:'password'
  %input#js-passcheck{type: "checkbox"}
  %label{for: "js-passcheck"} パスワードを表示する

コードはこんな感じです。

 (2)jsを記載していきます。

password.js
$(function(){
  var password = '#password'; #haml内のid:'password'を取得して代入。
  var passcheck = '#js-passcheck'; #haml内のid:'js-passcheck'を取得して代入。

  $(passcheck).change(function(){  #チェックボックスを押した際に発火するイベントを作成
    if ($(this).prop('checked')){   #チェックボックスにチェックが入った場合発火
      $(password).attr('type','text');  #パスワードのタイプをtypeからtextへ変更することで文字表示
    }
  });
});

チェックボックスを押した際に発火するイベントを作成しています。
そして、チェックボックスにチェックが入った時に、
input内のtypeをpasswordからtextへと変更する形となっています。
なので、input内の文字が表示されるようです。

2.パスワードを黒丸に戻します。

password.js
$(function(){
  var password = '#password'; #haml内のid:'password'を取得して代入。
  var passcheck = '#js-passcheck'; #haml内のid:'js-passcheck'を取得して代入。

  $(passcheck).change(function(){  #チェックボックスを押した際に発火するイベントを作成
    if ($(this).prop('checked')){   #チェックボックスにチェックが入った場合発火
      $(password).attr('type','text');  #パスワードのタイプをtypeからtextへ変更することで文字表示
    } else {                            #チェックボックスからチェックがなくなった際に発火
      $(password).attr('type','password');  #パスワードのタイプをpasswordへと変更
    }
  });
});

次に、elseの部分では、チェックボックスからチェックがなくなった時のイベントとなっています。
この文で、inputのtypeをtextからpasswordへと変更しているため、
文字が再び黒丸へと戻るようです。

まとめ

もし間違えている理解が間違っている部分等が、ありましたらコメントをいただけると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GemのソースをTracePointを使って効率的に読む

Rubyには様々な便利なGemがあるので有効活用しない手はないですよね。
ただ使っているGemが予期せぬ挙動をした時やドキュメントに載ってないような詳細仕様を知りたい時などにソースを読みたくなることがあります。

Gemのソースを読みたい場合、GithubなどWeb上に公開されていることが多いのでブラウザでソースを見たり、ローカルにソースを落としてきて見たりすると思います。

ただ愚直にソースを読み始めるとソース量が膨大だったり、メタプロが多用されていたりなどで読解がかなり大変です。
そこでこの記事ではTracePointを使って効率的にソースを読む方法を紹介します。

ソースを読んでみよう

具体例があった方が良いので、今回はrailsのfind_or_create_byを使った場合に呼ばれるソースを探すことにしましょう。
https://github.com/rails/rails

今回はローカルマシンにチェックアウトして読むことにします。
この記事では6-0-stableブランチ(2020/01/14時点)を使っています。

愚直にやってみよう

該当箇所の探し方は人それぞれだと思いますが、私は最初はメソッド名でgrepすることが多いです。
今回はactiverecord配下にあることが明白なのでactiverecord/配下でgit grep find_or_create_byしました。

activerecord % git grep find_or_create_by
CHANGELOG.md:    `ActiveRecord::Base.find_or_create_by`/`!` by leaning on unique constraints in the database.
lib/active_record/associations.rb:      #       def find_or_create_by_name(name)
lib/active_record/associations.rb:      #         find_or_create_by(first_name: first_name, last_name: last_name)
lib/active_record/associations.rb:      #   person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
lib/active_record/associations.rb:      #     def find_or_create_by_name(name)
lib/active_record/associations.rb:      #       find_or_create_by(first_name: first_name, last_name: last_name)
lib/active_record/associations.rb:        #     def find_or_create_by_name(name)
lib/active_record/associations.rb:        #       find_or_create_by(first_name: first_name, last_name: last_name)
lib/active_record/associations.rb:        #     def find_or_create_by_name(name)
lib/active_record/associations.rb:        #       find_or_create_by(first_name: first_name, last_name: last_name)
lib/active_record/querying.rb:      :find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
lib/active_record/relation.rb:    #   User.find_or_create_by(first_name: 'Penélope')
lib/active_record/relation.rb:    #   User.find_or_create_by(first_name: 'Penélope')
lib/active_record/relation.rb:    #   User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
lib/active_record/relation.rb:    #   User.find_or_create_by(first_name: 'Scarlett') do |user|
lib/active_record/relation.rb:    def find_or_create_by(attributes, &block)
lib/active_record/relation.rb:    # Like #find_or_create_by, but calls
lib/active_record/relation.rb:    def find_or_create_by!(attributes, &block)
lib/active_record/relation.rb:    # This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT
lib/active_record/relation.rb:    # * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by,
lib/active_record/relation.rb:    # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new]
lib/active_record/relation/finder_methods.rb:    #   Person.find_or_create_by(name: 'Spartacus', rating: 4)
test/cases/finder_respond_to_test.rb:    assert_not_respond_to Topic, :fail_to_find_or_create_by_title
test/cases/finder_respond_to_test.rb:    assert_not_respond_to Topic, :find_or_create_by_title?
test/cases/finder_test.rb:    assert_raise(NoMethodError) { Topic.fail_to_find_or_create_by_title("Nonexistent Title") }
test/cases/finder_test.rb:    assert_raise(NoMethodError) { Topic.find_or_create_by_title?("Nonexistent Title") }
test/cases/nested_attributes_test.rb:      ).find_or_create_by!(
test/cases/relation/delegation_test.rb:        :find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
test/cases/relations_test.rb:  def test_find_or_create_by
test/cases/relations_test.rb:    bird = Bird.find_or_create_by(name: "bob")
test/cases/relations_test.rb:    assert_equal bird, Bird.find_or_create_by(name: "bob")
test/cases/relations_test.rb:  def test_find_or_create_by_with_create_with
test/cases/relations_test.rb:    bird = Bird.create_with(color: "green").find_or_create_by(name: "bob")
test/cases/relations_test.rb:    assert_equal bird, Bird.create_with(color: "blue").find_or_create_by(name: "bob")
test/cases/relations_test.rb:  def test_find_or_create_by!
test/cases/relations_test.rb:    assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: "green") }
test/cases/statement_cache_test.rb:    def test_find_or_create_by
test/cases/statement_cache_test.rb:      a = Book.find_or_create_by(name: "my book")
test/cases/statement_cache_test.rb:      b = Book.find_or_create_by(name: "my other book")

たくさんヒットしましたが、テストコードやコメントアウトされている行は無視すると下記の2行になります。

lib/active_record/querying.rb:      :find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
lib/active_record/relation.rb:    def find_or_create_by(attributes, &block)

どちらが呼ばれているのでしょうか?
これだけの情報ではわからないのでさらにソースを読む必要がありそうです。

TracePointを使ってみよう

机上でソースを辿るのがツラくなってきたのでTracePointを使って呼び出されたメソッドをトレースしてみます。
動作環境は下記の通り

  • Ruby: 2.6.5
  • Rails: 6.0.2.1

Rails consleで下記を実行しました。

# TracePointのトレース開始
# :callを指定することでメソッド呼び出しをトレースします
trace = TracePoint.trace(:call)  do |tp|
  # 何も指定しないと全Gemがトレースされるのでactiverecordのみ出力するようにしました
  p tp.inspect if tp.path.include?('activerecord-6')
end

# トレースされるようになったのでfind_or_created_byを実行
User.find_or_create_by(name: 'hoge')
# trace情報が大量に出力されます
"#<TracePoint:call `find_or_create_by'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/querying.rb:21>"
"#<TracePoint:call `all'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping/named.rb:26>"
"#<TracePoint:call `current_scope'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping.rb:26>"
"#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping.rb:74>"
"#<TracePoint:call `value_for'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping.rb:79>"
"#<TracePoint:call `raise_invalid_scope_type!'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping.rb:99>"
"#<TracePoint:call `base_class'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:99>"
"#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>"
"#<TracePoint:call `relation'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:295>"
"#<TracePoint:call `create'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/delegation.rb:114>"
"#<TracePoint:call `relation_class_for'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/delegation.rb:120>"
"#<TracePoint:call `relation_delegate_class'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/delegation.rb:8>"
"#<TracePoint:call `arel_table'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:266>"
"#<TracePoint:call `table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:195>"
"#<TracePoint:call `reset_table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:226>"
"#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>"
"#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>"
"#<TracePoint:call `table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:195>"
"#<TracePoint:call `reset_table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:226>"
"#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>"
"#<TracePoint:call `table_name='@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:205>"
"#<TracePoint:call `compute_table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:528>"
"#<TracePoint:call `base_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:113>"
"#<TracePoint:call `base_class'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:99>"
"#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>"
"#<TracePoint:call `full_table_name_prefix'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:236>"
"#<TracePoint:call `undecorated_table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:522>"
"#<TracePoint:call `full_table_name_suffix'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:240>"
"#<TracePoint:call `table_name='@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:205>"
"#<TracePoint:call `type_caster'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:280>"
"#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/type_caster/map.rb:6>"
"#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/arel/table.rb:16>"
"#<TracePoint:call `predicate_builder'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:276>"
"#<TracePoint:call `table_metadata'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:306>"
"#<TracePoint:call `arel_table'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:266>"
"#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/table_metadata.rb:7>"
"#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:7>"
"#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/basic_object_handler.rb:6>"
"#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>"
"#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/base_handler.rb:6>"
"#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>"
"#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/range_handler.rb:8>"
"#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>"
"#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>"
"#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/array_handler.rb:8>"
"#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>"
"#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/array_handler.rb:8>"
"#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>"
"#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation.rb:27>"
"#<TracePoint:call `finder_needs_type_condition?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:86>"
"#<TracePoint:call `descends_from_active_record?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:76>"
"#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>"
"#<TracePoint:call `descends_from_active_record?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:76>"
"#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>"
"#<TracePoint:call `default_scoped'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping/named.rb:57>"
"#<TracePoint:call `build_default_scope'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping/default.rb:103>"
"#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>"
"#<TracePoint:call `find_or_create_by'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation.rb:168>"
・・・(省略)

これで最初に呼ばれているのはlib/active_record/querying.rb#find_or_create_byということがサクッと分かりました。
ちなみに候補に上がっていたlib/active_record/relation.rb#find_or_create_byも58個目に呼ばれていました。

TracePointを使うとこのように速く、そして正確にメソッド呼び出しの順を知ることができます。
この情報をインプットとしてソースを読むとスムーズに処理の流れが読めます。

ちなみに今回は:callを指定して呼び出されたメソッドだけを抽出しましたが、パラメーターや戻り値もトレースできるので、障害の原因調査などでも活躍すると思います。
詳細は"参照"に載せているリンクなどをご覧ください。

参照

以下、TracePointの仕様を知る上でとても参考になったリンクです。

すごく参考になった記事
https://qiita.com/siman/items/9426ff6c113247088f7e

Rubyリファレンス
https://docs.ruby-lang.org/ja/latest/class/TracePoint.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS EC2でCould not find pg-1.1.4 in any of the sources Run `bundle install` to install missing gems.と表示される

こんにちは@yukifreeworld12です。
EC2でエラーが出て解決したのでメモとして、そして後世の同じエラーにハマった人の為にも...

1. エラー文

EC2でGitと連携しクローンしていた終盤

$ rake secret
Could not find pg-1.1.4 in any of the sources
Run `bundle install` to install missing gems.

$ gem install pg
Building native extensions.  This could take a while...
ERROR:  Error installing pg:
    ERROR: Failed to build gem native extension.
<略>

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /home/name/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/extensions/x86_64-linux/2.4.0/pg-1.2.2/mkmf.log

extconf failed, exit code 1

と長いエラー文。

2. 解決策

解決したコマンド
sudo yum install -y postgresql-devel
このコマンドでpostgresql-develをインストールした後
bundle installでちゃんといけました。

3.しかしその後

やりたかったrake secretを実行したところ...

$ rake secret
rake aborted!
Gem::LoadError: You have already activated rake 13.0.1, but your Gemfile requires rake 12.3.3. Prepending `bundle exec` to your command may solve this.

bundle execと書いてあるのでbundle execを付けて
bundle exec rake secretとしたところいけましたとさ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで作ったSNSでユーザの投稿へのコメント作成機能を実装する

・参考URL
https://sadah.github.io/rails-training/ja/004_comments.html

今回紹介するコードは、プログラミングスクールで学んだTwitterクローンを基にしたRailsのアプリ(Micropost)への追加機能であることを前提にするとコードが読みやすくなると思います。

モデルの作成

rails g model Comment content:string user:references micropost:references

コメントはUserとMicropostの多:多の関係性を表すため、中間テーブルが必要になります。
Userがコメントする投稿が複数あって、MicropostにはコメントしたUserが複数いるためです。

マイグレーションファイル

年月日時_create_relationships.rb
class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.string :content
      t.references :user, foreign_key: true
      t.references :micropost, foreign_key: true

      t.timestamps
    end
  end
end

マイグレーションを忘れずに。

rails db:migrate

自動作成されたコメントモデルにバリデーションを付けておきましょう。

コメントモデル

comment.rb
class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :micropost

  validates :content, presence: true, length: { maximum: 255 }
end

コメントを文字なしで投稿できないように設定します。

関連モデル

Userモデル

user.rb
class User < ApplicationRecord
  has_many :microposts
  has_many :comments
end

一つのUserに対して多数のコメントがあるためこの関係になります。

Micropostモデル

micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
end

一つのMicropostに対して多数のコメントがあるためこの関係になります。

Micropostモデルにdependent: :destroyを追記したことで、投稿が削除されるとそれに紐づいたコメントが削除されるようになります。
dependent: :destroyがないと、コメントの付いた投稿を削除するときにエラーが起きます。

micropost.rb
has_many :commenters, through: :comments, source: :user

Micropost側から見ると、コメントしてくるユーザが多数いるので、正確にモデル作成するなら以下のコードの追記が必要かもしれません。(今回使うことはありませんでしたが、間違っていたらごめんなさい)

ルーティング

routes.rb
Rails.application.routes.draw do
  resources :microposts, only: [:create, :destroy, :show] do
    resources :comments, only: [:create, :destroy]
  end
end

どのMicropostに対してコメントなのかを明らかにするためにルーティングのネスト(入れ子構造)をします。

コントローラー作成

$ rails g controller comments create destroy

コメントで必要な機能は作成機能(のちに削除機能)の2つだけなのでターミナルへの入力はこのようになります。

comments_controller.rb
class CommentsController < ApplicationController
  before_action :require_user_logged_in

  def create
    @micropost = Micropost.find(params[:micropost_id]) 
    @comment = @micropost.comments.build(comment_params)
    @comment.user_id = current_user.id
    if @comment.save
      flash[:success] = '投稿にコメントしました。'
      redirect_back(fallback_location: root_path)
    else
      @micropost = Micropost.find(params[:micropost_id]) 
    @comments = @micropost.comments.includes(:user)
      flash.now[:danger] = '投稿へのコメントに失敗しました。'
      render 'microposts/show'
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:content)
  end
end

コメントはmicropostと紐づいているため@micropostを頭につけます。
コメントとuserを紐づけるために@comment.user_id = current_user.idが必要です。
これがないとコメントからuserデータを得られません。

else文が実行されるときrenderするファイルに@micropost@comments
2つのインスタンス変数を渡すのは、micropostのshowファイルを表示するために必要だからです。
片方でも欠けるとエラーになります。

関連コントローラー

コメント作成を投稿の詳細ページ(show)で実行できるようにします。

microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :require_user_logged_in

  def show
    @micropost = Micropost.find(params[:id])
    @comments = @micropost.comments.includes(:user)
    @comment = @micropost.comments.build(user_id: current_user.id) if current_user # form_with 用
  end
end

showページの@micropost = Micropost.find(params[:id])で特定の一つのmicropostを表示できるように設定します。

@comments = @micropost.comments.includes(:user)により、対応する投稿へのコメントを一覧表示することができます。
includes(:user)はコメントしたuserを表示するためにuserカラムを取得するメソッドです。これがないとgravatar_urlなどを表示する時にuserがnilになりエラーが起こります。

@commentには、コメント入力フォームの表示エラーを避けるために空のコメントを入れておきます。

comments_controller.rb
class CommentsController < ApplicationController
  before_action :require_user_logged_in

  def create
    @micropost = Micropost.find(params[:micropost_id]) 
    @comment = @micropost.comments.build(comment_params)
    @comment.user_id = current_user.id
    if @comment.save
      flash[:success] = '投稿にコメントしました。'
      redirect_back(fallback_location: root_path)
    else
      @micropost = Micropost.find(params[:micropost_id]) 
      @comments = @micropost.comments.includes(:user)
      flash.now[:danger] = '投稿へのコメントに失敗しました。'
      render 'microposts/show'
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:content)
  end
end

基本的にはmicroposts_controllerのcreateと同じようにします。(microposts_controllerに関してはRails Tutorialなどを参考にしてください)
コメントはmicropostに紐づくように作成したいので、対応するmicropost
インスタンスを記入しておきます。
@comment.user_id = current_user.idがないとコメントにuser情報が入らないため必須です。
@micropost@comennts両方のインスタンスを代入しないとrender先のshowファイルで
コメント失敗メッセージを表示できずにエラーが起こります。

ルーティング

投稿詳細ページに飛ぶためのルーティングをターミナルを使って確認します。

$ rails routes

microposts#showページに飛ぶために必要なPrefixを確認します

micropost GET    /microposts/:id(.:format)                                                                microposts#show

投稿一覧のパーシャルに以下の1行を追加して、投稿詳細ページに移動できるようにしましょう。

microposts一覧のパーシャルファイルの一部
<% microposts.each do |micropost| %><%= link_to 'Comments', micropost_path(micropost), class: 'btn btn-link btn-sm' %><% end %>

microposts#showのPrefixがmicropostなのでmicropost_pathと記入します。
micropost一覧から特定の|micropost|のページに飛びたいので、
micropost_pathの()内には||内のmicropostを代入します。

Viewファイル

投稿詳細ページ

コメントしたい特定のmicropostを表示するファイルです。
micropostを表示させるためには、controllerのdef showで定義した
@micropostのインスタンスをshowファイルに渡せるように注意しましょう。

microposts/show.html.erb
<ul class="list-unstyled">
  <li class="media mb-3">
    <img class="mr-2 rounded" src="<%= gravatar_url(@micropost.user, { size: 50 }) %>" alt="">
    <div class="media-body">
      <div>
        <%= link_to @micropost.user.name, user_path(@micropost.user) %> <span class="text-muted">posted at <%= @micropost.created_at %></span>
      </div>
      <div>
        <p><%= @micropost.content %></p>
      </div>
      <div class="btn-group">
        <% if current_user == @micropost.user %>
          <%#=詳細ページで削除するとid見つからないエラー発生 link_to "Delete", @micropost, method: :delete, data: { confirm: "You sure?" }, class: 'btn btn-danger btn-sm' %>
        <% end %>
      <%= render 'favorites/favorite_button', micropost: @micropost %>
      </div>
    </div>
  </li>
</ul>

<%# コメント入力フォームのパーシャル %>
<%= render 'comments/form', micropost: @micropost %>

<%# コメント一覧のパーシャル %>
<%= render 'comments/comments', micropost: @micropost %>

投稿詳細ページでmicropostを削除すると、micropostの削除自体はできても
ルーティングエラーが起きてしまうため、現在は削除ボタンを使えないようにしています。

コメント入力フォーム

form_withのmodelに@micropost@commentが入るようにしないとエラーが起こります。

comments/_form.html.erb
<div class="col-sm-8">
<%= form_with(model: [@micropost, @comment], local: true) do |f| %>
  <div class="form-group">
    <%= f.text_area :content, class: 'form-control', rows: 3 %>
  </div>
  <%= f.submit 'Comment', class: 'btn btn-primary btn-block' %>
<% end %>
</div>

コメント一覧

comments/_comments.html.erb
<ul class="list-unstyled">
  <% @comments.each do |comment| %>
    <li class="media mb-3">
      <img class="mr-2 rounded" src="<%= gravatar_url(comment.user, { size: 50 }) %>" alt="">
      <div class="media-body">
        <div>
          <%= link_to comment.user.name, user_path(comment.user) %> <span class="text-muted">posted at <%= comment.created_at %></span>
        </div>
        <div>
          <p><%= comment.content %></p>
        </div>
        <div class="btn-group">
          <% if current_user == comment.user %>
            <%#= link_to "Delete", micropost_comment_path(comment), method: :delete, data: { confirm: "You sure?" }, class: 'btn btn-danger btn-sm' %>
          <% end %>
        </div>
      </div>
    </li>
  <% end %>
  <%#= paginate comments %>
</ul>

コメント削除機能は正しいルーティングができていないので、まだ未実装です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで作ったSNSアプリでユーザの投稿へのコメント作成機能を実装する

・参考URL
https://sadah.github.io/rails-training/ja/004_comments.html

今回紹介するコードは、プログラミングスクールで学んだTwitterクローンを基にしたRailsのアプリ(Micropost)への追加機能であることを前提にするとコードが読みやすくなると思います。

モデルの作成

rails g model Comment content:string user:references micropost:references

コメントはUserとMicropostの多:多の関係性を表すため、中間テーブルが必要になります。
Userがコメントする投稿が複数あって、MicropostにはコメントしたUserが複数いるためです。

マイグレーションファイル

年月日時_create_relationships.rb
class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.string :content
      t.references :user, foreign_key: true
      t.references :micropost, foreign_key: true

      t.timestamps
    end
  end
end

マイグレーションを忘れずに。

rails db:migrate

自動作成されたコメントモデルにバリデーションを付けておきましょう。

コメントモデル

comment.rb
class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :micropost

  validates :content, presence: true, length: { maximum: 255 }
end

コメントを文字なしで投稿できないように設定します。

関連モデル

Userモデル

user.rb
class User < ApplicationRecord
  has_many :microposts
  has_many :comments
end

一つのUserに対して多数のコメントがあるためこの関係になります。

Micropostモデル

micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
end

一つのMicropostに対して多数のコメントがあるためこの関係になります。

Micropostモデルにdependent: :destroyを追記したことで、投稿が削除されるとそれに紐づいたコメントが削除されるようになります。
dependent: :destroyがないと、コメントの付いた投稿を削除するときにエラーが起きます。

micropost.rb
has_many :commenters, through: :comments, source: :user

Micropost側から見ると、コメントしてくるユーザが多数いるので、正確にモデル作成するなら以下のコードの追記が必要かもしれません。(今回使うことはありませんでしたが、間違っていたらごめんなさい)

ルーティング

routes.rb
Rails.application.routes.draw do
  resources :microposts, only: [:create, :destroy, :show] do
    resources :comments, only: [:create, :destroy]
  end
end

どのMicropostに対してコメントなのかを明らかにするためにルーティングのネスト(入れ子構造)をします。

コントローラー作成

$ rails g controller comments create destroy

コメントで必要な機能は作成機能(のちに削除機能)の2つだけなのでターミナルへの入力はこのようになります。

comments_controller.rb
class CommentsController < ApplicationController
  before_action :require_user_logged_in

  def create
    @micropost = Micropost.find(params[:micropost_id]) 
    @comment = @micropost.comments.build(comment_params)
    @comment.user_id = current_user.id
    if @comment.save
      flash[:success] = '投稿にコメントしました。'
      redirect_back(fallback_location: root_path)
    else
      @micropost = Micropost.find(params[:micropost_id]) 
    @comments = @micropost.comments.includes(:user)
      flash.now[:danger] = '投稿へのコメントに失敗しました。'
      render 'microposts/show'
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:content)
  end
end

コメントはmicropostと紐づいているため@micropostを頭につけます。
コメントとuserを紐づけるために@comment.user_id = current_user.idが必要です。
これがないとコメントからuserデータを得られません。

else文が実行されるときrenderするファイルに@micropost@comments
2つのインスタンス変数を渡すのは、micropostのshowファイルを表示するために必要だからです。
片方でも欠けるとエラーになります。

関連コントローラー

コメント作成を投稿の詳細ページ(show)で実行できるようにします。

microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :require_user_logged_in

  def show
    @micropost = Micropost.find(params[:id])
    @comments = @micropost.comments.includes(:user)
    @comment = @micropost.comments.build(user_id: current_user.id) if current_user # form_with 用
  end
end

showページの@micropost = Micropost.find(params[:id])で特定の一つのmicropostを表示できるように設定します。

@comments = @micropost.comments.includes(:user)により、対応する投稿へのコメントを一覧表示することができます。
includes(:user)はコメントしたuserを表示するためにuserカラムを取得するメソッドです。これがないとgravatar_urlなどを表示する時にuserがnilになりエラーが起こります。

@commentには、コメント入力フォームの表示エラーを避けるために空のコメントを入れておきます。

comments_controller.rb
class CommentsController < ApplicationController
  before_action :require_user_logged_in

  def create
    @micropost = Micropost.find(params[:micropost_id]) 
    @comment = @micropost.comments.build(comment_params)
    @comment.user_id = current_user.id
    if @comment.save
      flash[:success] = '投稿にコメントしました。'
      redirect_back(fallback_location: root_path)
    else
      @micropost = Micropost.find(params[:micropost_id]) 
      @comments = @micropost.comments.includes(:user)
      flash.now[:danger] = '投稿へのコメントに失敗しました。'
      render 'microposts/show'
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:content)
  end
end

基本的にはmicroposts_controllerのcreateと同じようにします。(microposts_controllerに関してはRails Tutorialなどを参考にしてください)
コメントはmicropostに紐づくように作成したいので、対応するmicropost
インスタンスを記入しておきます。
@comment.user_id = current_user.idがないとコメントにuser情報が入らないため必須です。
@micropost@comennts両方のインスタンスを代入しないとrender先のshowファイルで
コメント失敗メッセージを表示できずにエラーが起こります。

ルーティング

投稿詳細ページに飛ぶためのルーティングをターミナルを使って確認します。

$ rails routes

microposts#showページに飛ぶために必要なPrefixを確認します

micropost GET    /microposts/:id(.:format)                                                                microposts#show

投稿一覧のパーシャルに以下の1行を追加して、投稿詳細ページに移動できるようにしましょう。

microposts一覧のパーシャルファイルの一部
<% microposts.each do |micropost| %><%= link_to 'Comments', micropost_path(micropost), class: 'btn btn-link btn-sm' %><% end %>

microposts#showのPrefixがmicropostなのでmicropost_pathと記入します。
micropost一覧から特定の|micropost|のページに飛びたいので、
micropost_pathの()内には||内のmicropostを代入します。

Viewファイル

投稿詳細ページ

コメントしたい特定のmicropostを表示するファイルです。
micropostを表示させるためには、controllerのdef showで定義した
@micropostのインスタンスをshowファイルに渡せるように注意しましょう。

microposts/show.html.erb
<ul class="list-unstyled">
  <li class="media mb-3">
    <img class="mr-2 rounded" src="<%= gravatar_url(@micropost.user, { size: 50 }) %>" alt="">
    <div class="media-body">
      <div>
        <%= link_to @micropost.user.name, user_path(@micropost.user) %> <span class="text-muted">posted at <%= @micropost.created_at %></span>
      </div>
      <div>
        <p><%= @micropost.content %></p>
      </div>
      <div class="btn-group">
        <% if current_user == @micropost.user %>
          <%#=詳細ページで削除するとid見つからないエラー発生 link_to "Delete", @micropost, method: :delete, data: { confirm: "You sure?" }, class: 'btn btn-danger btn-sm' %>
        <% end %>
      <%= render 'favorites/favorite_button', micropost: @micropost %>
      </div>
    </div>
  </li>
</ul>

<%# コメント入力フォームのパーシャル %>
<%= render 'comments/form', micropost: @micropost %>

<%# コメント一覧のパーシャル %>
<%= render 'comments/comments', micropost: @micropost %>

投稿詳細ページでmicropostを削除すると、micropostの削除自体はできても
ルーティングエラーが起きてしまうため、現在は削除ボタンを使えないようにしています。

コメント入力フォーム

form_withのmodelに@micropost@commentが入るようにしないとエラーが起こります。

comments/_form.html.erb
<div class="col-sm-8">
<%= form_with(model: [@micropost, @comment], local: true) do |f| %>
  <div class="form-group">
    <%= f.text_area :content, class: 'form-control', rows: 3 %>
  </div>
  <%= f.submit 'Comment', class: 'btn btn-primary btn-block' %>
<% end %>
</div>

コメント一覧

comments/_comments.html.erb
<ul class="list-unstyled">
  <% @comments.each do |comment| %>
    <li class="media mb-3">
      <img class="mr-2 rounded" src="<%= gravatar_url(comment.user, { size: 50 }) %>" alt="">
      <div class="media-body">
        <div>
          <%= link_to comment.user.name, user_path(comment.user) %> <span class="text-muted">posted at <%= comment.created_at %></span>
        </div>
        <div>
          <p><%= comment.content %></p>
        </div>
        <div class="btn-group">
          <% if current_user == comment.user %>
            <%#= link_to "Delete", micropost_comment_path(comment), method: :delete, data: { confirm: "You sure?" }, class: 'btn btn-danger btn-sm' %>
          <% end %>
        </div>
      </div>
    </li>
  <% end %>
  <%#= paginate comments %>
</ul>

コメント削除機能は正しいルーティングができていないので、まだ未実装です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Web系開発周りの自分用まとめ:学習:Ruby

0. 概要

Progateを使ってRubyを学んでみての感想をまとめてみた。
他言語と比較しての感想とかも混ざってる。
今後Rubyを書いてみて、あれ?これどうするんだっけ?といったときに見返す用。

1. 感想

  • 演算子
    • インクリメント演算子++が使えない
  • 条件式
    • elseifじゃなくてelsifなのにびっくり
  • 括弧
    • くくらない言語なのでちょっと違和感(VBAとかもだけど)
    • 加えて、多言語での中括弧終わりに相当するのがend
  • 文字列内""くくりでの変数展開が#{}なのが他でみたことない感じ
    • 文字列と数値の+での結合ができない言語なので上をつかわないとダメ
  • each doに違和感
  • 慣習(と思われるもの「)
    • 変数名はスネークケース
    • 配列は複数形、each doでのループ回す時のループ変数は単数形
    • タブ(インデント)は半角スペース2つ
    • bool値を返すメソッド名の末尾は?をつける
    • クラス名は大文字開始(というか言語仕様的に大文字以外できない)
    • メソッドはキーワード引数で定義して、呼び出し側の可読性を向上させるっぽい
  • ハッシュ(連想配列)
    • 文字列と同じような(とはいえ異なる)シンボル(:<任意の文字列>)というものをキーにすることもできるがよくわからん・・・
    • シンボルはハッシュのキーとしてよく使い、通常は{"key1" => value1, "key2" => value2}と書かないといけないのを、省略して{key1: value1, key2: value2}と書くことができて楽。これは便利!
  • nil
    • オブジェクトが存在しない、はRubyではnullではなくnil
    • 条件式でnilはfalseと扱われ、それ以外の値はtrueと扱われる仕様。へぇー
  • メソッド
    • 定義するときには、defを頭につける。なんかCのマクロと混同しちゃう。
    • 定義時に引数の名前をref:のように:で終わらせると、呼び出し側でその引数をつけて呼び出させることができる。キーワード引数と呼ぶ。呼び出し側のコードの可読性があがりそう!
    • ちなみにメソッド内でのキーワード引数へのアクセスは:が取れてるバージョン。へぇ・・・
    • 引数なしの場合は呼び出し時に()は不要。つけてもうごくけど
  • クラス
    • クラス名
      • 大文字縛り。コーディング規約的に一般的なのでいいね。
    • メンバ
      • インスタンス変数と呼ぶ.変な感じ
      • attr_accessor :member1のようにシンボル的な書き方をする.変な感じ。attr_accessorもなんか長ったらしー
      • インスタンス変数へのアクセスは.member1のようにする。えぇ・・・:member1じゃないんかーい
      • ※attr_accessor :member1はメンバ変数の宣言ではないらしい。こう書くと、@member1というインスタンス変数への以下のpublicなgetter, setterが定義される特殊な書き方らしい。だから、インスタンス化した後に.member1で外部からアクセスすると、C#やJavaのような言語でpublicなmember1というインスタンス変数へ直接アクセスするかのような使い方ができる、というものらしい。
        • def member1
        • def member1=(引数)
    • メソッド
      • メンバでattr_accessorだったからattr_methodとか必要なのかと思いきや、ふつーにdef <メソッド名>ええ・・一貫性がなーい
      • クラスに定義したメソッドはインスタンスメソッドという。なんでなんや・・・クラスメソッドでいいじゃん。メンバの方もインスタンス変数だったけどクラス変数でいいじゃん・・・
        • と思いきや!クラスにはインスタンス化した後に呼び出せるメソッドとインスタンス化前に呼び出せるメソッドがあり、まだインスタンス化していないときに使えるメソッドのことをクラスメソッドと呼ぶ!なるほど!
      • 自身のインスタンス変数を使うときにはself.<インスタンス変数名>なんでthis.<インスタンス変数名>じゃないんや・・・
        • 厳密
      • コンストラクタはinitialize()。クラス自身の名前じゃないんですねぇ・・・・
    • 継承
      • 子クラス < 親クラス で定義する.:じゃなくて<なんですね。へぇー
      • 子クラスのメソッドの中でsuper()とすると親クラスの同名のメソッドを呼び出すことができる。子クラス側で親クラスと同じ処理になる場合は、こうしておくと処理の修正をしなければならないときに親クラス側を修正するだけで済みそう
  • コンソールからの入力
    • gets.chompでやる。へぇー
    • gets.chompでうけとった値は絶対に文字列。なので、文字列以外の値として扱いたい場合は、メソッドを実行する。たとえばget.chomp.to_iみたいな。なるほどねー
  • 日付を使いたい
    • Rutyで用意されているDateクラスを使う
    • require "date"が必要。require "./date"ではダメ。へぇー
    • コンストラクタは(年, 月, 日)になっている。
    • Date.new()でなく、Date.todayとすると今日の日付でインスタンス化される。へぇー
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails(Ruby)アプリからスプレッドシートに書き込み

GCP側のでやること

GCPのコンソール(https://cloud.google.com)から、
・「Google Drive API」と「Google Sheets API」を有効化
スクリーンショット 2020-01-15 15.49.43.png
スクリーンショット 2020-01-15 15.48.27.png

・OAuth Client ID取得
スクリーンショット 2020-01-15 15.50.54.png

注意:WEBとかiOSとかではなく、「Other」を選ぶ

Rails(Ruby)側

gemを追加

Gemfile
gem 'google_drive'

認証情報を記載するconfig.jsonを作成

touch config.json
config.json
{
  "client_id": "xxxxxxx-xxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
  "client_secret": "xxxxxxxXXxxxxxxxxxxxxxxxxx"
}

キーからスプレッドシートを取得。スプレッドシートのURLのうち、https://docs.google.com/spreadsheets/d/xxxこのxxxの部分がスプレッドシートのキーになる。

require "google_drive"

session = GoogleDrive::Session.from_config("config.json")
sheet = session.spreadsheet_by_key("xxxxxxxxxxxxxxxxxxxxxxxxxxx").worksheets[0]

# 書き込み
sheet[1,1] = "From API"
sheet.save

実行すると、認証が始まるので表示されるURLに対象アカウントでログインして認証。

書き込まれるかどうか確認

スクリーンショット 2020-01-15 16.12.15.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】Rubyで超簡単ドリンク注文アプリケーション(リファクタリングver)

@scivola さんよりアドバイスいただき、DRYな部分を整理しました。

こちらの記事は
https://qiita.com/pontarou194/items/9e91b40dcc2da608dd24
の続きです。

もともとは、メソッドが4つありました。。。。

otyaメソッド
coffeeメソッド
beerメソッド
saidaメソッド

これを一つにまとめられました。
もともとはこんな感じ。

これがメソッドの多いアプリケーション(進化前)

def otya(otya)
  puts "あなたが選んだのはお茶"
  puts "お茶の値段は100円です"
  puts "いくつ注文しますか?"
  order_quantity = gets.to_i
  total_price = order_quantity * otya
  puts "合計#{total_price}円です"
end

def coffee(coffee)
  puts "あなたが選んだのはコーヒー"
  puts "コーヒーの値段は200円です"
  puts "いくつ注文しますか?"
  order_quantity = gets.to_i
  total_price = order_quantity * coffee
  puts "合計#{total_price}円です"

end

def beer(beer)
  puts "あなたが選んだのはビール"
  puts "ビールの値段は300円です"
  puts "いくつ注文しますか?"
  order_quantity = gets.to_i
  total_price = order_quantity * beer
  puts "合計#{total_price}円です"
end

def saida(saida)
  puts "あなたが選んだのは三ツ矢サイダー"
  puts "サイダーの値段は300円です"
  puts "いくつ注文しますか?"
  order_quantity = gets.to_i
  total_price = order_quantity * saida
  puts "合計#{total_price}円です"
end

otya = 100
coffee = 200
beer = 300
saida = 400

  puts "何を飲みたいですか?"
    drink_menu = ["お茶","コーヒー","ビール","三ツ矢サイダー"]
    drink_menu.each.with_index(1) do |drink_name, number|
      puts "#{number}:#{drink_name}"
    end

  case gets.to_i
    when 1
      otya(otya)
    when 2
      coffee(coffee)
    when 3
      beer(beer)
    when 4
      saida(saida)
    else
      puts "無効な入力値です"
    end

結論、メソッドを一つにまとめると、下記のようなコードとなりました(進化後)

def order_drink(drink_name, drink_price)
  puts "あなたが選んだのは#{drink_name}"
  puts "#{drink_name}の値段は#{drink_price}円です"
  puts "いくつ注文しますか?"
  order_quantity = gets.to_i
  total_price = order_quantity * drink_price
  puts "合計#{total_price}円です"
end

otya = 100
coffee = 200
beer = 300
saida = 400

  puts "何を飲みたいですか?"
    drink_menu = ["お茶","コーヒー","ビール","三ツ矢サイダー"]
    drink_menu.each.with_index(1) do |drink_name, number|
      puts "#{number}:#{drink_name}"
    end

  case gets.to_i
    when 1
      order_drink("お茶", otya)
    when 2
      order_drink("コーヒー", coffee)
    when 3
      order_drink("ビール", beer)
    when 4
      order_drink("三ツ矢サイダー", saida)
    else
      puts "無効な入力値です"
    end

@scivola さん完璧です。アドバイスいただきありがとうございました。
なんと、36行もコードを圧縮できました!

@scivola さんがおっしゃるお通り、ポイントは以下の通りです。

・DRYの原則にあてこむ(繰り返しは避ける、似た表現が繰り返される場合もなるべく避ける)
・メソッド名をわかりやすく


(自分で見返したとき用に)もっとわかりやすくします。

擬似コードで書いててわかるように、

def 
①お茶が選ばれた場合、値段を表示
②注文数を入力
③合計額を表示
end

def 
①コーヒーが選ばれた場合、値段を表示
②注文数を入力
③合計額を表示
end

def 
①ビールが選ばれた場合、値段を表示
②注文数を入力
③合計額を表示
end

def
①サイダーが選ばれた場合、値段を表示
②注文数を入力
③合計額を表示
end

この①〜③とメソッド名がかなり似ている。
これ、まとめたほうがすっきりしてわかりやすい。

特に
①のあなたが選んだのは○○
→共通化できる

②○○の値段は○○円です
→共通化できる

③合計○○円です
→共通化できる

あとは、いくつ注文しますかとか、gets.to_iとかを一緒に入れて完成。

def order_drink(drink_name, drink_price)
  puts "あなたが選んだのは#{drink_name}"
  puts "#{drink_name}の値段は#{drink_price}円です"
  puts "いくつ注文しますか?"
  order_quantity = gets.to_i
  total_price = order_quantity * drink_price
  puts "合計#{total_price}円です"
end

otya = 100
coffee = 200
beer = 300
saida = 400

引数の使い方について

今回勉強になったポイントは、引数の使い方
複数の引数を使用するケースをあまり経験したことがなかった・・・・・・

なので、いざ自分でコードを組み立てていくとなると、

何がどうなってるのやら??
っという状況でした。

下記のコードでは、drink_nameが第一引数、drink_priceが第二引数ですね。
コードにポイントをコメントで記述します。

def order_drink(drink_name, drink_price) #このdrink_nameとdrink_priceが仮引数
  puts "あなたが選んだのは#{drink_name}"
  puts "#{drink_name}の値段は#{drink_price}円です"
  puts "いくつ注文しますか?"
  order_quantity = gets.to_i
  total_price = order_quantity * drink_price
  puts "合計#{total_price}円です"
end
  case gets.to_i
    when 1
      order_drink("お茶", otya) #お茶とotyaが実引数
    when 2
      order_drink("コーヒー", coffee) #上記に習って同じ
    when 3
      order_drink("ビール", beer) #上記に習って同じ
    when 4
      order_drink("三ツ矢サイダー", saida) #上記に習って同じ
    else
      puts "無効な入力値です"
    end

仮引数と実引数は一致しなくても良いというのがありましたね。なので
これでも下記のようにcase文の実引数をいじって、otya = 100 等の変数宣言を省略することもできます。

  case gets.to_i
    when 1
      order_drink("お茶", 100)
    when 2
      order_drink("コーヒー", 200)
    when 3
      order_drink("ビール", 300)
    when 4
      order_drink("三ツ矢サイダー", 400)
    else
      puts "無効な入力値です"
    end

メソッド名をわかりやすく。

order_drinkというメソッド名はドリンクをオーダーするためだけなのか、計算もしてくれるのかがわかりずらいですよね。
なので、注文管理メソッドという名前をつけてみました。
グーグル翻訳すると、order_managementとなってましたので、そのまま名付けました。

最後に、コードをまとめました、こんな感じです。

def order_management(drink_name, drink_price)
  puts "あなたが選んだのは#{drink_name}"
  puts "#{drink_name}の値段は#{drink_price}円です"
  puts "いくつ注文しますか?"
  order_quantity = gets.to_i
  total_price = order_quantity * drink_price
  puts "合計#{total_price}円です"
end

  puts "何を飲みたいですか?"
    drink_menu = ["お茶","コーヒー","ビール","三ツ矢サイダー"]
    drink_menu.each.with_index(1) do |drink_name, number|
      puts "#{number}:#{drink_name}"
    end

  case gets.to_i
    when 1
      order_management("お茶", 100)
    when 2
      order_management("コーヒー", 200)
    when 3
      order_management("ビール", 300)
    when 4
      order_management("三ツ矢サイダー", 400)
    else
      puts "無効な入力値です"
    end

引数や変数は便利だなぁと改めて感じました。
また、DRYを原則とすることで、「保守しやすっ!」っと思いました。

ヒアドキュメントはもう少し勉強必要でした、、、

@scivola さんありがとうございました。

@scivola から、さらにアドバイスいただきました。

二重配列を使用して、case文を省略し、コードを短くできるよう見直しました。

def order_management(drink_name, drink_price)
  puts "あなたが選んだのは#{drink_name}"
  puts "#{drink_name}の値段は#{drink_price}円です"
  puts "いくつ注文しますか?"
  order_quantity = gets.to_i
  total_price = order_quantity * drink_price
  puts "合計#{total_price}円です"
end

  puts "何を飲みたいですか?"
    drink_menu = ["お茶","コーヒー","ビール","三ツ矢サイダー"]
    drink_menu.each.with_index(1) do |drink_name, number|
      puts "#{number}:#{drink_name}"
    end

    drinks = [
      ["お茶", 100],
      ["コーヒー", 200],
      ["ビール", 300],
      ["三ツ矢サイダー", 400]
    ]

    input = gets.to_i
    drink_name = drinks[input - 1][0]
    drink_price = drinks[input -1][1]
  order_management(drink_name, drink_price)

(おそらく合っている?
動作は問題なく動きました)

2次元配列について参考とさせていただきました。
https://pikawaka.com/ruby/array

case文使わない方法があるとは・・・・
実際かいてみると、単純構造でした。

@scivola さん、アドバイスいただきありがとうございます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでレコードに保存している「連続した改行」を改行を保持したままviewに表示する

環境

Rails 5.2.3
Ruby 2.6.3

実現したいこと

例えばArticleモデルのcontentカラムに保存している、
「あいうえお

かきこけこ」
をviewに表示したい。

解決策

safe_join関数を利用し、改行文字をbrに変換する。
simple_format 関数もあるのだが連続改行が認識されないため、
safe_join関数を利用する。

show.html.haml
= safe_join(@article.content.split("\n"), tag(:br))

参照

https://api.rubyonrails.org/classes/ERB/Util.html#method-c-html_escape

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby初心者向け】打順の組み方を変えて得点の入り方をシミュレーションして比較してみた

概要

野球が好きなので、打順の最適な組み方をついつい考えてしまいます。

ある検証で「強打者と弱い打者を交互に並べる」よりも「強打者を下位打線に固める」方が得点が入りやすいという結果が出たというのを知り、単純化したモデルで検証してみようと思い試してみました。

本格的な検証ではないので、Rubyを使って何か簡単な検証や計算を行ってみたいという人に大雑把な流れだけわかってもらえれば嬉しいです。

前提条件

  • 3割打者5人と2割打者4人の2タイプを用意
  • 打者は凡打か単打のみを打つ
  • 凡打においてランナーは進塁せず、3連打で1点が入る
  • 3連打以上では安打のたびに1得点を追加
  • エラーや四死球は簡略化のため考慮しない
  • 盗塁や送りバントはセイバーメトリクスで推奨されないので行わない
  • 1万試合回して合計得点を競う

この超簡略化した野球で2タイプの打者の打順を入れ替えて、どれだけ得点が入るかを検証します。ただ四死球と長打を全て排除するのはあまりにも酷なので、あとでそれらを考慮したコードも紹介していきます。

打順の組み方は

  • 上位5人に3割打者を固める
  • 3割打者と2割打者を交互におく
  • 3割打者を5-9番の下位に固める

の3種類とします。

変数を宣言

baseball.rb
#1-9番の打者の打順を記録した配列
average=[0.3,0.3,0.3,0.3,0.3,0.2,0.2,0.2,0.2]

#合計得点
sumScore=0
#打席に立っている打者の打順
batter=1
#アウトカウント
outCount=0
#塁上のランナーの数
runner=0
#アウトカウント
iningCount=0

1イニングを終わらせてみる

baseball.rb
loop {
    hit = rand(1..100)
    hit = hit/100.0
    #乱数を使って単打か凡打を記録

    if hit <= average[batter-1]
      #走者を一人増やす
      runner+=1
      if runner>=3
        #走者が2人以上たまっていたらランナーを本塁に返す
        runner=2
        sumScore+=1
      end
    else
        #凡打なのでアウトカウントを増やすだけ
        outCount+=1
        if outCount == 3
            #3アウトチェンジしてループを抜ける
            runner=0
            outCount=0
            break
        end
     end
 #次のバッターに交代
 batter+=1
 #9番打者を打ち終わったら一番に戻る
 batter = (batter-1)%9 + 1
}

1万試合を回してみる

1万試合なので90000イニング繰り返して、何点入るかを検証します。ただ9万回回すのではなく、9回が終わったら打順を1番に戻す処理を忘れないようにします。

baseball.rb
90000.times {
  iningCount+=1
  loop {
    hit = rand(1..100)
    hit = hit/100.0
    #乱数を使って単打か凡打を記録

    if hit <= average[batter-1]
      #走者を一人増やす
      runner+=1
       if runner>=3
         #走者が2人以上たまっていたらランナーを本塁に返す
         runner=2
         sumScore+=1
       end
    else
        #凡打なのでアウトカウントを増やすだけ
        outCount+=1
        if outCount == 3
            #3アウトチェンジしてループを抜ける
            runner=0
            outCount=0
            break
        end
     end
    #次のバッターに交代
    batter+=1
    #9番打者を打ち終わったら一番に戻る
    batter = (batter-1)%9 + 1
   }

 if iningCount == 9 
    #9回が終了したらイニングと打順をリセットする
    iningCount=0
    batter=1
 end
}

##最後に合計得点を出力
puts sumScore

結果

打線の組み方 上位打線に強打者 下位打線に強打者 交互に並べる
1試合平均得点 1.57 1.52 1.50

という結果になりました。ものすごく簡単な検証でしたが、強打者は分散して配置するよりも、下位打線でも良いから連続させて打順を組む方が効果的ということがわかります。

今回のルールでは連打が出ないと点が入らないので、チャンスでいかに打てるかが大事です。強打者が作ったチャンスを平凡な打者が潰してしまうのを減らすためには、強打者は集中させるのがいいという直感と合致しますね。

おまけ 得点が少なすぎるので調整

上のルールにおける検証結果では強打者を上位に並べても1試合あたり1,2点しか入らないということになり、あまりにも少なすぎるので、もう少し実際の野球に近づけてみました。

近年の野球において特に重視されるのは、出塁率と長打率です。野球は27個のアウトを取られるまでは永遠に攻撃を続けられるスゴロクゲームと捉えることができます。

スゴロクに置いて重要なのは

  • いかにアウトにならず塁に出られるか
  • 長打によって一度にたくさん駒を進められるか

の2点です。実際の野球ではヒットでなくても四球で塁に出ることができますし、本塁打が出ればその時点で得点が入ります。

プロ野球で打率3割を残せるのはごく一部ですが、出塁率3割はレギュラークラスなら超えておかなければならない数字です。

コードで本塁打と四球を反映させてみた

ここからは打線の組み方というよりも本物の野球の点の入り方に近づけることを目標とします。四球は単打と同じ扱いとし、全員の出塁率を一律に3割2分とします。3回に1回塁に出る計算になります。

また、出塁のうちの10本の1本はホームランが出て、ランナーを全員返すと仮定しましょう。(この辺りの仮定は実際のプロ野球のパリーグのデータを用いています)

baseball.rb
#チーム全員の出塁率を宣言する
average=[0.32,0.32,0.32,0.32,0.32,0.32,0.32,0.32,0.32]

sumScore=0
batter=1
outCount=0
runner=0
iningCount=0

90000.times {
    iningCount+=1
     loop {
        hit = rand(1..100)
        hit = hit/100.0

        if hit <= average[batter-1]
            runner+=1
            if rand(1..10) == 1
                #10回に一回はホームランが出て全員が帰りランナーを0に戻す
                sumScore+=runner
                runner=0
            elsif runner>=3
                runner=2
                sumScore+=1
            end
        else
            outCount+=1
            if outCount == 3
                runner=0
                outCount=0
                break
            end
        end

        batter+=1
        batter = (batter-1)%9 + 1
    }

    if iningCount == 9 
        iningCount=0
        batter=1
    end

}

puts sumScore

四球とホームランを考慮した結果、1試合あたりの平均得点は4.4点となりました。DH制を利用しているパリーグの昨年の平均得点が4.31点だったので、大雑把なモデルの割にはかなり近い感じになりました。

まとめと展望

出塁率と長打率さえ調整すれば、実際の野球と近い平均得点を出せることがわかりました。ここからさらに実際の野球に近づけるには、以下のことをする必要があるでしょう。

  • ホームランバッターを上位におく
  • 3連続出塁で1点も入らない場合も想定する
  • 打者の出塁率の差を考慮する
  • 2塁打を考慮する(3塁打は少なすぎて誤差の範囲)

バント、盗塁はセイバーメトリクスでは推奨されていませんし、エラーも出塁と同値と考えて良いのかもしれません。コードが簡潔に保てる範囲内で色々試してみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリにLINE Pay決済を導入する簡易手引き(LINE Pay API - v3)

株式会社OKANで「おかんPay」の開発(フロント/バックエンド)に携わっているsuzu_Dと申します。
このアプリを簡単に説明すると、「オフィスおかん」という置型の社食サービスで、小銭を使わずキャッシュレス決済ができるアプリとなっております。
今まで当アプリではクレジットカードを登録し支払う、という方法でのみの決済でしたが、
2019年12月の目玉アップデートとして、当アプリ経由でLINE Pay決済ができる機能をリリースしました。

今回は、技術的なお話というよりLINE Payを組み込む際に、今現在ネットの情報だけだと自分的には不足している部分があり
「そもそもLINE Pay APIに通信できない・・・」といった事が多発し、LINE Payの技術スタッフの方と何回かやり取りさせていただいたので、
その中で改めて知った、LINE Payの知見を、自分なりに纏めてアウトプットし、
もし皆さんが関わっているプロダクトで「LINE Pay決済」を導入する機会がありましたら
少しでも参考になればなと思い、本記事を寄稿しました。

本文献の購読対象者と、目的・出来るようになること

購読対象者

・開発中のネイティブアプリやWEBアプリでLINE Pay決済を組み込もうとしている方
・実際にいまLINE Payを組込中で「そもそもLINE Pay APIと通信がうまくいかない!」と嘆いている方

目的・出来るようになること

・自プロダクトでLINE Payで決済ができる仕組みが、最低限なんとなく理解できて、LINE Pay APIとREST通信出来るようになる。

LINE Pay APIのバージョンと参考ガイドについて

当記事で対象としているLINE Pay APIのバージョン

当記事で対象としているLINE Pay APIのバージョンは
v3(2019/12 時点) 
の情報になります、v3以前のバージョンや、これ以降APIのバージョンが上がった際
参考にならない記述が出る可能性があることを、ご了承願います。

当記事で参考にしているLINE Pay API ガイド

https://pay.line.me/jp/developers/apis/onlineApis?locale=ja_JP

LINE Pay APIの環境について

API通信に使う環境として、主に使うのは以下2つになると思います。

  • 本番環境
  • テスト加盟店環境

API通信に使う環境について

本番環境

加盟店申込後に審査が完了してから利用可能になる環境です。
実際のお金のやりとりをする環境になります。
加盟店申込後にLINE Pay側から連絡があり、申請したアカウント(メールアドレス)にて
LINE PayのMy Pageからログインを行うと、取引の履歴を見たり、決済のキャンセル・返金を行うことが出来ます。
また、本番のAPI通信に必要なキーの情報を見ることが出来ます。

テスト加盟店環境

Sandbox生成後利用可能になる環境です。
このページにて登録したメールアドレス宛に、テスト加盟店環境にログインできるアカウントが通知されます。
通知されたアカウントでLINE PayのMy Pageからログインを行うと、テスト加盟店環境で行った取引の履歴を見たり、決済のキャンセル・返金を行うことが出来ます。また、テスト加盟店環境にてAPI通信をするために必要なキーの情報を見ることが出来ます。
こちらも、本番環境同様、決済処理の通信を行った場合、実際のお金のやり取りが発生する(LINE Pay残高が減る)のですが、毎日23:55と2:55に支払いを行った決済に対して自動的に決済のキャンセル・返金処理が行われます。
アプリに決済機能を実装し、LINE Pay決済をテスト的につかう場合はこちらの環境を使うことをおすすめします。

各環境にAPI通信するために必要なエンドポイントとキーについて

API通信する際のエンドポイント

エンドポイント(本番、テスト加盟店環境 共通)
https://api-pay.line.me
例 - Request APIをコールする場合のエンドポイント
https://api-pay.line.me/v3/payments/request

各環境にAPI通信するためのキー、Channel IDとChannel SecretKeyについて

LINE Pay API通信する際、必要な認証情報としてChannel IDとChannel SecretKeyを使用することになります。
このキーが本番環境、もしくはテスト加盟店環境の違いになるので、本番環境用とテスト用で接続先の設定ファイルを作る際は間違えないようにしてください(テスト用の設定ファイルに本番用のChannel IDとChannel SecretKeyを登録してしまった等の間違いをしないようにしてください。)
確認方法は以下になります。

①本番環境、もしくはテスト加盟店環境のアカウントにてLINE PayのMy Pageからログインを行う

②ログイン後画面にて「決済連動管理」>「連動キー管理」と進み、ログイン用のパスワードを入力する
スクリーンショット 2019-12-23 14.27.41.png (168.7 kB)

③Channel ID とChannel Secret Keyを確認することが出来る(このChannel ID とChannel Secret Keyは、絶対外部に漏らさないようにしてください。)
スクリーンショット 2019-12-23 14.57.13.png (605.5 kB)

このChannel IDとChannel SecretKeyによって、どの加盟店での決済かを判別しているようです、APIのリクエストを投げる際、共通ヘッダーにキーを設定することによって、通信が可能となります、このキーの設定が間違っている場合にhttps://api-pay.line.meに通信を行うと以下のようなレスポンスが返ってきます、

{
    "returnCode": "1106",
    "returnMessage": "Header information error. request verification Failed"
}

このキーを共通ヘッダーに設定する際、少々特別な処理が必要になります、詳しい説明は後述する共通ヘッダーの設定方法を参照してください。

※SandBoxについて

本番環境、テスト加盟店環境 の双方どちらも「SandBox」で通信できる環境があります。
スクリーンショット 2019-10-30 11.40.56.png (190.5 kB)
こちらの環境は以下のエンドポイントで通信が行えます

https://sandbox-api-pay.line.me

テスト的に使うのであれば、この環境を使いたいところですが、
SandBox環境だと決済の流れが、実際アプリではなくエミュレータのようなもので動き、
本物の決済の流れが体感的に分かりにくいのと、お試しでREST通信をしたい場合(Postman等を使用して疎通確認したい場合)
何らかの原因でエラーがでてしまい、通信が成功しないなどあるので、テスト的に使う場合はhttps://sandbox-api-pay.line.meのエンドポイントは使わず、少額の決済であれば、実際のお金を幾らかLINE Payに入金しておき、テスト加盟店環境https://api-pay.line.meをコールするようにした方がよいと考えます。
(PostmanがSandbox環境にて使えない原因をLINE Payの技術スタッフの方に問い合わせしたところ、通信時に特殊なパラメータが付与されている為のことだそうです。)

環境まとめ

本番環境 テスト加盟店環境 Sandbox(本番・テスト加盟店環境)
利用制限 加盟店に申込後、審査完了後利用可能 なし、無料利用可能 なし、無料利用可能(テスト加盟店環境の場合)
利用料 加盟店契約により変動 無料 無料 (テスト加盟店環境の場合)
利用方法 加盟店申込をする Sandbox環境を作成する(ややこしいが、この環境がテスト加盟店環境という)
接続先 https://api-pay.line.me https://api-pay.line.me https://sandbox-api-pay.line.me
決済処理時の動き LINEアカウントの残高を利用 LINEアカウントの残高を利用 Sandboxの架空残高を利用(この利用に関しては実際に試していないのでどんな感じで決済されるかについてはよく分かっていない)
返金方法 MyPageから手動 MyPageから手動 or 毎日23:55と2:55に自動的に返金 架空残高を使うので特に無し

LINE Pay APIにPOST通信する為の共通ヘッダーについて

共通ヘッダーについて

共通ヘッダーに設定する値

リクエストを送る時の、最低限の必要な共通ヘッダーが下記になります。

キー データの型 設定する値
Content-Type String 'application/json'
X-LINE-ChannelId String LINE PayのMy Pageで確認したChannel ID
X-LINE-Authorization-Nonce String 1回限りのランダムな値を設定する、UUIDを生成したり、現在日時のミリ秒等を生成して設定する。
X-LINE-Authorization String ※後述

X-LINE-Authorization-Nonceに設定する値としては、一度切りに使う値を生成し使ってください、(例えばJavaScriptでヘッダーを実装する場合var nonce = (new Date()).getTime();と現在のミリ秒を生成しnonceの値に設定する、といった実装です。)
X-LINE-Authorizationに関しては少々ややこしいので、次項に詳細を書きます。

X-LINE-Authorization に設定する値について

X-LINE-Authorizationに設定する値はガイドによるとこう記述されています。

HTTP Method : GET

Signature = Base64(HMAC-SHA256(Your ChannelSecret, (Your ChannelSecret + URL Path + Query String + nonce))) Query String : ?を除いたクエリ文字列(例 : Name1=Value1&Name2=Value2...)

HTTP Method : POST

Signature = Base64(HMAC-SHA256(Your ChannelSecret, (Your ChannelSecret + URL Path + RequestBody + nonce)))

今回は「POST」の時のX-LINE-Authorizationについて説明します。
POST時にX-LINE-Authorizationを設定するために、以下の4つの値が必要です。
①Your ChannelSecret - LINE PayのMy Pageで確認したChannel SecretKey
②URL Path - POSTするURLのパス(例えば https://api-pay.line.me/v3/payments/request 等)
③RequestBody - POSTする時のRequest Body
④nonce - X-LINE-Authorization-Nonceに設定した値
そして上記の値を文字列連結し、ハッシュ メッセージ認証コード (HMAC) で計算します。
(HMACとは?)
この計算に使う、秘密鍵とメッセージ(データ)とハッシュ関数は以下になります
秘密鍵: LINE PayのMy Pageで確認したChannel SecretKey
メッセージ(データ):上記①〜④を文字列連結した値
ハッシュ関数:SHA256

そして、ハッシュ化した値をBase64 でエンコードし、
エンコードした値をX-LINE-Authorizationに設定します。

この処理をJAVAで書いた例はガイドに記載されてあります。
JavaScriptで書く場合は以下のようになるかと思います。

var crypto = require('crypto-js');// crypto-jsという暗号化、復号化のライブラリを使う

var channelSecret = '上記①の値';
var path = '上記②の値';
var body = '上記③の値';
var nonce = (new Date()).getTime(); // nonceに使うランダムな値の生成をする

var message = channelSecret + path + body + nonce ;
var hash = crypto.HmacSHA256(message,channelSecret); // channelSecretの値をキーにSHA256ハッシュ関数を使用してハッシュ (HMAC) を計算
var hashHeader = CryptoJS.enc.Base64.stringify(hash); // ここの値がX-LINE-Authorizationに設定する値になる

また、今回自分が実際実装した言語はrubyを使ったので
rubyで実装する場合は下記のように①〜④の値を受けとって処理するようなメソッドを実装すればよいと思います。

    def authorization(channel_secret, params, url, nonce)
      message = "#{channel_secret}#{url}#{params.to_json}#{nonce}"
      hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), channel_secret, message)
      Base64.strict_encode64(hash)
    end

ヘッダーの実装例(ruby)

rubyでヘッダー部分を実装するとしたら、以下のようになります。

    def headers(params, url)
      # ヘッダーを作る処理をする時にPOSTするURLのパスとPOSTする時のRequest Bodyを渡す
      nonce = authorization_nonce
      {
        'Content-Type' => 'application/json',
        'X-LINE-ChannelId' => channel_id,
        'X-LINE-Authorization-Nonce' => nonce,
        'X-LINE-Authorization' => authorization(params, url, nonce),
      }
    end

    def channel_id
      Rails.application.credentials.dig(# ここはcredentials.yml.encに設定した値を取り出す).to_s
    end

    def channel_secret
      Rails.application.credentials.dig(# ここはcredentials.yml.encに設定した値を取り出す).to_s
    end

    def authorization_nonce
      Time.zone.now.to_s(:db_jst)
    end

    def authorization(params, url, nonce)
      message = "#{channel_secret}#{url}#{params.to_json}#{nonce}"
      hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), channel_secret, message)
      Base64.strict_encode64(hash)
    end

決済するためのAPIについて

「決済」を行うためのAPI

商品の決済をする為に必要最低限のAPIは以下の2つのみです。
・Request API (POST /v3/payments/request)
https://pay.line.me/documents/online_v3_ja.html#request-api

・Confirm API (POST /v3/payments/{transactionId}/confirm)
https://pay.line.me/documents/online_v3_ja.html#confirm-api

その他決済取り消し等のAPIありますが、本記事では取り上げません。
また、上記のAPIを毎回コールし決済するパターンを「一般決済」と言い、
上記APIを初回のみコールし、以降上記APIをコールしなくても決済が出来るパターンを「自動決済」と言うのですが
今回は一般決済のみの事を記述します。

決済の処理をアプリに組み込む際のざっくりな流れ(一例)

①Request API (POST /v3/payments/request)をコールする

②成功時のレスポンスにLINE Pay取引番号(transactionId)と
 決済用のURL(info.paymentUrl.web 又は info.paymentUrl.web.app) が返ってくる

③アプリまたはWEB上で ②のレスポンス内にある決済用のURL(info.paymentUrl.web 又は info.paymentUrl.app)
 に遷移する処理をする。

④(LINEが端末にインストールされている状態でinfo.paymentUrl.appのURLに遷移した場合)
  LINEアプリが立ち上がり LINE Payの支払画面に遷移するので、LINEアプリ内で決済の操作を行う

⑤決済が完了すると ①のRequest APIをコール時に設定した画面に遷移する(設定の仕方は後述)

⑥ ⑤にて遷移した瞬間にConfirm API (POST /v3/payments/{transactionId}/confirm)をコールする処理をする、
 {transactionId} には②のレスポンスに含まれていたLINE Pay取引番号(transactionId)を設定する。
 設定例: /v3/payments/2019010100000000000/confirm

⑦決済が完了する。

Request APIについて

Request API のざっくりとした説明

「こんな商品を買うから、購入するために整理券ちょうだい」的なAPIです。
「商品の値段は○○で、個数が○○で〜」といった情報をRequest Bodyに詰め込んで送ると
「整理券あげるよ、整理番号(LINE Pay取引番号)はこれだよ、この場所(決済用のURL)で買ってね」
というようなレスポンスが返ってきます。
返ってきた、整理番号(LINE Pay取引番号)は次にコールするConfirm APIで使う事になるので
どこかに保持しておく必要があります。

Request API のRequest Bodyの例とレスポンス

実際通信する時のRequest Bodyの例は以下のようになっています。
必要最低限のことだけ記述してあるので、「商品の送料、配送先」等、詳細にせってしたい場合は公式ドキュメントを参照してください。
Request Bodyの例-コピペ用

{
    "amount": 300,
    "currency": "JPY",
    "orderId": "20190101",
    "packages": [
        {
            "id": "1",
            "amount": 300,
            "name": "LINE Pay商品",
            "products": [
                {
                    "name": "豚の角煮(赤みそ)",
                    "imageUrl": "https://exmple.jpg",
                    "quantity": 1,
                    "price": 100
                },
                {
                    "name": "れんこんサラダ", 
                    "imageUrl": "https://exmple.jpg", 
                    "quantity": 2, 
                    "price": 100
                }
            ]
        }
    ],
    "redirectUrls": {
        "confirmUrl": "myapp://confirm",
        "cancelUrl": "myapp://cancel"
    }
}

Request Bodyの例-各パラメータの簡単な説明

{
    "amount": 300, // 合計金額を設定、この金額はpackages[].amountの合計金額と一致していないとエラーになる。
    "currency": "JPY", // 決済通貨の種類 日本であれば"JPY"で問題ないかと
    "orderId": "20190101", // 実装者側で発行するユニークなID、特に決まっていなければ、ランダムなUUIDを生成したり、現在日付のTIME_STAMP等設定すれば良い
    "packages": [
        {
            "id": "1", // 特に決まっていなくても設定する必要あり
            "amount": 300, // products[].priceの合計金額、一致してないとエラーになる
            "name": "LINE Pay商品", // 購入する商品の全体的な名前、特に決まっていなければ、商品と同じ名前でよいと思う。
            "products": [
                {
                    "name": "豚の角煮(赤みそ)", // 商品の名前 ここの値がLINE Payの決済画面で表示される。
                    "imageUrl": "https://linepay.exmple.image.jpg", // 商品の画像のサムネイル LINE Payの決済画面で表示される。
                    "quantity": 1, // 商品の個数
                    "price": 100 // 商品の値段
                },
                {
                    "name": "れんこんサラダ", 
                    "imageUrl": "https://linepay.exmple.image.jpg", 
                    "quantity": 2, 
                    "price": 100
                }
            ]
        }
    ],
    "redirectUrls": {
        "confirmUrl": "myapp://confirm", // LINE Payで商品決済したときに遷移するURL、遷移させたいのがWEBページならhttp://~ ネイティブアプリならアプリのDeepLinkを設定する
        "cancelUrl": "myapp://cancel" // LINE Payで商品決済をキャンセルしたときに遷移するURL
    }
}

※"imageUrl" に設定している値は一例なので、実際にサムネイルとして何か画像を表示してみたい場合は、実在するURLを入れてください、若しくは設定しないでください。

上記のリクエストを飛ばし、成功すると以下のようなレスポンスが返ってきます(~の部分は可変します)

{
    "returnCode": "0000",
    "returnMessage": "Success.",
    "info": {
        "paymentUrl": {
            "web": "https://web-pay.line.me/web/payment/wait?~~~~~",
            "app": "line://pay/payment/~~~~~"
        },
        "transactionId": ~~~~~~~~,
        "paymentAccessToken": "~~~~~~~~"
    }
}

端末にLINEがインストールされており、LINE Payが使える設定にしている場合に、上記のレスポンスの「info.paymentUrl.app」に遷移すると以下のような画面に遷移します。
Screenshot_20191031_124159_jp.naver.line.android.jpg (22.7 kB)
※上記の画像にサムネイルが表示されていますがRequest BodyのimageUrlで設定した「linepay.exmple.image.jpg」とは異なる画像を使っています。「linepay.exmple.image.jpg」というURLはあくまで一例であって実際には存在しませんのでご了承ください。
※上記の画像で決済やキャンセルを行うとRequest Bodyで設定したconfirmUrlやcancelUrlに遷移します。

Confirm APIのRequest Bodyの例とレスポンス

Confirm API のざっくりとした説明

「さっき教えてもらった場所で決済したよ、承認してね。」的なAPIです。
LINE Payのアプリで決済処理をせずにコールするとエラーになるので注意が必要です。

Request Bodyの例-コピペ用

{
    "amount": 300,
    "currency": "JPY"
}

Request Bodyの例-各パラメータの簡単な説明

{
    "amount": 300, //商品の合計金額 Request APIの時と違う金額だとエラーになるので注意
    "currency": "JPY" // 決済通貨の種類
}

上記のリクエストを飛ばし、成功すると以下のようなレスポンスが返ってきます(~の部分は可変します)

{
    "returnCode": "0000",
    "returnMessage": "Success.",
    "info": {
        "transactionId": ~~~~~~~~,
        "orderId": "20190101",
        "payInfo": [
            {
                "method": "BALANCE",
                "amount": 300
            }
        ],
        "packages": [
            {
                "id": "1",
                "amount": 300,
                "name": "LINE Pay商品",
                "products": [
                    {
                        "name": "豚の角煮(赤みそ)",
                        "imageUrl": "https://exmple.jpg",
                        "quantity": 1,
                        "price": 100
                    },
                    {
                        "name": "れんこんサラダ", 
                        "imageUrl": "https://exmple.jpg", 
                        "quantity": 2, 
                        "price": 100
                    }
                ]
            }
        ],
    }
}

上記のようなレスポンスが返ってきた場合、実際にLINE Payの残高が減り、LINE PayのMy Pageにて取引の履歴が残っていることを確認できるかと思います。
LINE PayのMy Page
[取引管理]>[取引内訳]
スクリーンショット 2019-12-23 18.05.47.png (198.1 kB)

以上で、LINE Pay APIにて一般決済する時の処理の流れになります。

(おまけ)Postmanで疎通確認を行う

まだ実際にはアプリには組み込まないけど、LINE Pay APIを叩いたらどんなレスポンスが返ってくるか見たい場合は
Postman」を使うのが便利です、ここではPostmanを使ってLINE PayのAPIをコールするときの設定を明記します。(Postmanの導入方法についてはここでは説明しませんので、各自で調べていただくよう、宜しくお願いします。)

各種設定

環境ファイルの設定(Channel ID とChannel SecretKeyの設定)

①赤枠部分の歯車のアイコンをクリックします。
スクリーンショット 2019-12-25 11.35.13 2.png (141.6 kB)

②赤枠部分の[Add]をクリックします。
スクリーンショット 2019-12-25 11.38.10.png (182.5 kB)

③以下のように設定し[Add]をクリックします。

VARIABLE INITIAL VALUE CURRENT VALUE
channelId LINEのMyPageで確認したChannel ID ←INITIAL VALUEで設定した値と同じ
channelSecret LINEのMyPageで確認したChannel SecretKey ←INITIAL VALUEで設定した値と同じ

スクリーンショット 2019-12-25 11.40.19.png (197.0 kB)

④赤枠部分のように環境ファイルを先程追加したものに設定しておきます。
スクリーンショット 2019-12-25 11.35.13.png (141.4 kB)

Headersの設定

以下のように設定します。

KEY VALUE
Content-Type application/json
X-LINE-ChannelId {{channelId}}
X-LINE-Authorization-Nonce {{nonce}}
X-LINE-Authorization {{authorization}}

Content-Typeは文字列で'application/json'と設定しておきます。
その他の設定はPostmanの環境ファイルから取得するようにします。
{{channelId}}等、中括弧で囲むと、先程設定した設定ファイルのkey参照し、その値を参照するようになります。
スクリーンショット 2019-12-25 11.35.13.png (141.4 kB)

Bodyの設定

rawを選択し、以下のように設定します。

{
    "amount": 300,
    "currency": "JPY",
    "orderId": "20190101",
    "packages": [
        {
            "id": "1",
            "amount": 300,
            "name": "LINE Pay商品",
            "products": [
                {
                    "name": "豚の角煮(赤みそ)",
                    "imageUrl": "",
                    "quantity": 1,
                    "price": 100
                },
                {
                    "name": "れんこんサラダ", 
                    "imageUrl": "", 
                    "quantity": 2, 
                    "price": 100
                }
            ]
        }
    ],
    "redirectUrls": {
        "confirmUrl": "myapp://confirm",
        "cancelUrl": "myapp://cancel"
    }
}

スクリーンショット 2019-12-25 11.37.06.png (224.2 kB)

Pre-request Scriptの設定

ヘッダーの「X-LINE-Authorization」に設定するための処理を記述します。

var crypto = require('crypto-js');
var time = (new Date()).getTime();
pm.environment.set("nonce", time);
var path = pm.request.url.getPath();
var body = pm.request.body.toString();
var message = pm.environment.get("channelSecret") + path + body + pm.environment.get("nonce") ;
var hash = crypto.HmacSHA256(message,pm.environment.get("channelSecret"));
var hashHeader = CryptoJS.enc.Base64.stringify(hash);
pm.environment.set("authorization",hashHeader);

スクリーンショット 2019-12-25 11.56.47.png (171.9 kB)
pm.〜で始まる記述はPostman独自の処理で、リクエストBodyを取得したり、環境ファイルの設定を読み込んだり書き込んだりするための記述です。

レスポンス

Sendした結果、以下のようなレスポンスが返ってきたら成功です。
以下の結果はRequest API (POST /v3/payments/request)をコールした際のレスポンスです。
スクリーンショット 2019-12-25 11.57.49.png (240.9 kB)

returnCodeが'0000'以外の場合はLINE APIへの疎通は成功しているが、処理が成功していません
各APIのReturn Codesの欄を参考にどこがミスをしているか探してみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

あっさり読むrails③(クラス)

はじめに

前回の記事にて、
もちろんこのままだと味気ないので、実際にはclassを設定したり、他のメソッドを使用することになりますと書きました。
本記事では、そのclassを設定するの部分を実行しようと思います。

実行

前回使用したコードを改良しようと思います。今の所、コードは下記の通りです。

- @products.each do |product|
  = product.name
  = product.price
  = image_tag(product.image)

まずは、name,price,imageをひとまとめにするクラスを作ります。

index.html.haml
- @products.each do |product|
  .product
    = product.name
    = product.price
    = image_tag(product.image)

これで、テーブルに登録してあるだけの、name,price,imageの3要素を持ったproductclassのオブジェクトが作られます。
この状態では、namepriceでフォントを変えたり、imageの大きさを変更したり、といったことが難しいので、要素毎にclassを設定します。

index.html.haml
- @products.each do |product|
  .product
    = product.name, class: "product__name"
    = product.price, class: "product__price"
    = image_tag(product.image, class: "product__image")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6 のちょい足しな新機能を試す 116(MySQL データベース存在チェック編)

はじめに

Rails 6 に追加された新機能を試す第116段。 今回は、MySQL データベース存在チェック 編です。
Rails 6 では、MySQLのデータベースを存在するかどうかをチェックする方法が少し変わりました。
データベースが存在しないときに、 bin/rails db:migrate を実行した場合、 MySQLのエラーメッセージが英語以外でも、 ActiveRecord::NoDatabaseError が発生するようになりました。

Ruby 2.6.5, Rails 6.0.2.1, Rails 5.2.4.1 MySQL 8.0.16 で確認しました。 (Rails 6.0.0 でこの修正が入っています。)

$ rails --version
Rails 6.0.2.1

今回は、MySQL のエラーメッセージを ja_JP にして起動して、 bin/rails db:migrate コマンドを使って Rails 6.0.2.1 と Rails 5.2.4.1 の違いを確認してみます。

今回、MySQL のエラーメッセージを日本語に切り変えるために mysqld コマンドに --lc_messages_dir=/usr/share/mysql-8.0 --lc_messages=ja_JP オプションを追加しています。

MySQL 側で直接、データベースが存在しないことを確認する

mysql コマンドでデータベースが存在しないことを確認します。
ここで、メッセージの先頭が 1049 となっていることに注意してください。

$ mysql -u root app_development
ERROR 1049 (42000): 'app_development' は不明なデータベースです。

db:migrate を実行する

エラーを確認するために、 db:create しないで db:migrate を実行すると ActiveRecord::NoDatabaseError が発生します。

$ bin/rails db:migrate
rails aborted!
ActiveRecord::NoDatabaseError: 'app_development' は不明なデータベースです。
/usr/local/bundle/gems/activerecord-6.0.2.1/lib/active_record/connection_adapters/mysql2_adapter.rb:28:in `rescue in mysql2_connection'

ちなみに MySQL のエラーメッセージが英語の場合も ActiveRecord::NoDatabaseError となります。

$ bin/rails db:migrate
rails aborted!
ActiveRecord::NoDatabaseError: Unknown database 'app_development'
/usr/local/bundle/gems/activerecord-6.0.2.1/lib/active_record/connection_adapters/mysql2_adapter.rb:28:in `rescue in mysql2_connection'

Rails 5 では

MySQL のエラーメッセージが日本語の場合は、 Mysql2::Error となります。 英語の場合は、 ActiveRecord::NoDatabaseError になります。

日本語の場合:

$ bin/rails db:migrate
rails aborted!
Mysql2::Error: 'app_development' は不明なデータベースです。
/usr/local/bundle/gems/mysql2-0.5.2/lib/mysql2/client.rb:90:in `connect'

英語の場合

$ bin/rails db:migrate
rails aborted!
ActiveRecord::NoDatabaseError: Unknown database 'app_development'
/usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/connection_adapters/mysql2_adapter.rb:26:in `rescue in mysql2_connection'

何が変わったのか

Rails 5 までは、 エラーメッセージに Unknown database が含まれた場合に ActiveRecord::NoDatabaseError を発生させていました。
Rails 6 では、 エラーメッセージではなく、エラーの番号が 1049 (最初に mysqlコマンドでデータベースが存在しないことを確認したときの値)であるときに、 ActiveRecord::NoDatabaseError を発生させるようにしています。

試したソース

https://github.com/suketa/rails_sandbox/tree/try116_mysql_locale

参考情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby 行と列を入れ替えて配列の配列を作成する方法

paizaのスキルチェックで使った内容を投稿しました

array = [[1 ,2, 3], [4,5,6], [7,8,9]]
p array.transpose
>> [[1,4,7],[2,5,8],[3,6,9]]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

変数展開

変数展開とは

変数の値を文字列の中に含める方法

"#{変数}"

注意

シングルクオーテーションはそのまま文字列として出力される

変数の良いところ

・同じ要素を繰り返し使える
・変更に対応しやすい
・なんの要素かわかりやすい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

配列から複数の要素を取り出す方法

paizaのスキルチェックに使った内容を投稿しました。

array.each_cons(引数) {|arr| block }

引数で指定した数の要素を繰り返し取り出して、ブロックを実行

ブロック引数には取り出した要素が配列で入る

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】タイムゾーンの設定を日本時間に変更する方法

タイムゾーンの設定を日本時間に変更する方法です。

設定方法

configの中のapplication.rb内に下記のコードを追加。

config/application.rb
config.time_zone = 'Tokyo'
config.active_record.default_timezone = :local

これだけです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails MySQL データ型をdecimalにしているのに小数点を扱ってくれなくて困った話

目的

  • DBに格納されている数値に少数の値を足そうとして詰まりまくったところをまとめる

困った箇所

  1. viewファイルで表している入力formの少数の値が入力できない。
  2. 入力してDBのカラムに保存しようとしてもカラムに反映されない。saveでのエラーは出ない。
  3. rails consoleで当該カラムに少数を入力しsaveするとDBのカラムに反映されるが、formから少数を入力しても足し算されない、整数の足し算はできる。

「viewファイルで表している入力formの少数の値が入力できない」の解決方法

「入力してDBのカラムに保存しようとしてもカラムに反映されない。saveでのエラーは出ない。」の原因と解決方法

調査と原因

  • 数値を保存するカラムstady_timeのデータ型は少数を扱うためにdecimalにしている。
  • 下記にカラムstady_timeを作成したときのマイグレーションファイルを下記に記載する。
class AddStudyTimeHashTagToPosts < ActiveRecord::Migration[6.0]
  def change
    add_column :posts, :study_time, "decimal"
  end
end
  • 原因はカラム作成時にデータ型"decimal"を設定するときにオプションで格納する数値の全桁数と少数部分の桁数のを指定する必要があったようである。

解決方法

  • カラムstudy_timeを削除して再度オプション付きで作成し直すか、現在存在するstudy_timeカラムのデー型を設定し直すかの二通りがある。
  • 今回は個人プロダクトであるが、デプロイして使用されているサービスで重要なカラムを一旦削除することはありえないため、データ型を設定し直す方法をとる。
  • 下記の修正を加える

    修正前 修正後 修正可否
    カラム名 study_time study_time 修正しない
    データ型 decimal decimal 修正しない
    データ型のオプション 指定なし 全桁数12桁 少数点以下桁数2桁 修正する
  • 下記にカラムのデータ型設定を修正する手順を記載する。

    1. 下記コマンドを実行してマイグレーションファイル(DBの仕様変更を命令するファイル)を作成する。

      $ rails g migration change_study_time
      
    2. db/migrate/に存在するマイグレーションファイル20200112011439_change_study_time.rbに下記の記載を行う(#の行はコメントなので実際はなくても良い)

      class AddPasswordToUsers < ActiveRecord::Migration[6.0]
        def change
          #カラムの変更なのでchenge_column :テーブル名, :カラム名, :データ型, :オプション(数値の全桁数), :オプション(数値の小数点以下の桁数、第二位まで欲しいので2とした)
          change_column :posts, :study_time, :decimal, precision: 12, scale: 2
        end
      end
      
    3. 下記コマンドを実行してマイグレート(DBにマイグレーションファイル通りの仕様変更を行う命令)を行う。

      $ rails db:migrate
      
    4. もしマイグレーションファイルの記載を間違えてマイグレートしてしまったら下記の方法でマイグレート前の状態に戻す。

    5. 下記方法でカラムの方が正常に設定されているか確認する。

rails consoleで当該カラムに少数を入力しsaveするとDBのカラムに反映されるが、formから少数を入力しても足し算されない、整数の足し算はできる。」の原因と解決方法

調査と原因

  • rails consoleでの確認と切り分けにより足し算を行なっているコントローラファイルに問題があると感じた。
  • 少数は表示できるのに足されないということはformから送られてきた値を数値に変換している箇所に間違いがあると予想した。
  • 下記にpostコントローラの当該処理のメソッドを抜粋して記載する。

    def update
      @post = Post.find_by(id: params[:id])
      @post.study_time += params[:study_time].to_i
      @post.save
      redirect_to("/posts/#{@post.id}")
    end
    
  • params[:study_time]にはフォームで入力した少数値が入っている。.to_iで入力した文字列を数値に変更している。

  • 整数を表現する場合は.to_iでも良いが、少数を扱い際は別の表現にしないといけない。

解決方法

  • コントローラでフォームで入力した値を数値をして扱う処理で整数に変換してしまっているので少数も含む数値に変換してあげる記載をする。
  • .to_i.to_fとする。
  • 下記に正しいコードを記載する。

    def update
      @post = Post.find_by(id: params[:id])
      @post.study_time += params[:study_time].to_f
      @post.save
      redirect_to("/posts/#{@post.id}")
    end
    
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像の複数枚投稿と編集とプレビューと私

以前の記事にて

初投稿につき緊張ながら投稿したのをよく覚えています。
たくさん見てくださってありがとうございました。
画像の複数投稿??プレビュー表示??え??

しかし......

「投稿のみやん。editは?はよ。」と多数ご指摘をいただきました。

しかし.......

何の成果もあげられませんでした。

編集することができませんでした。

藁にもすがる思いでメンターさんにコードを見てもらい、アドバイスを仰ぎましたが、

「僕こんな実装したことないし、こんなやり方見たことない。
メンテナンス性も悪いし可読性もよろしくない。
他に効率のいい方法あるから.....やり直そうか」

と、ありがたくも残酷なご指摘をいただきました。

参考にしてくださった方々ごめんなさい。
メンテナンス性が悪く可読性もよくないお粗末なコードを書いてしまいました......

なので今回改良版をやります。意地で。

仕様

  1. 画像は5枚投稿できる
  2. 投稿した画像は1枚ずつプレビューされる
  3. 5枚目を投稿すると投稿欄が消える
  4. ドラッグ&ドロップは非実装
  5. 削除を押すとプレビューが消える

前回の反省

前回は新規投稿に注力しすぎて編集する時のことを考えられていなかったことが敗因でした。
新規投稿が完成した時点でやりきった感がありました。
スプリントレビューで「編集は?」と言われて「Oh.......」ってなったものの対応できず、
「1枚画像が投稿できる状態にする」というレビューOKの最低ラインに合わせるべくJSファイルを消去しました。悲しい。

「後のことを考えて実装する」というもっとも重要な設計思想が抜けていました。
大いに反省するきっかけとなったのでよしとしましょう。

しかし僕は諦めが悪いので、
今回は編集機能の実装を前提として考えていきたいと思います。
ではLET'S GO.

編集するには....

まず、編集ページへアクセスすると、
登録済みの写真についてはプレビューが表示されている状態にしなければいけません。
Image from Gyazo

前回は、画像登録の際にdataTransferというデータの箱を使って、1つのfile_fieldにデータを追加していく形で複数投稿を実装していました。
しかし、いざeditを実装するとなった際に、dataTransferにデータを追加することができず断念する結果となりました......
1つのfile_fieldを酷使する実装にはやはり無理があったようです。無理させてごめんねfile_field。

そこで今回は別々のidを持ったfile_fieldを5つ作成し、そのそれぞれにデータを入れていく形で実装することにしました。
これなら画像の編集も削除もできそうです。
では画像投稿機能から実装していきましょう。

プレビュー表示と削除

前回も利用したsample_appを使用します。

items/new.haml
.main
  %section.main__block
    = form_with model:@item, local:true do |f|
      %h2.sell__block__head
        商品の情報を入力
      .sell__block__form
        .sell__block__form__upload
          %h3.sell__block__form__upload__head
            出品画像
            %span.require 必須
          %p 最大5枚までアップロードできます
          .post__drop__box__container
            .prev-content
            .label-content
              %label{for: "item_images_attributes_0_image", class: "abel-box", id: "label-box--0"}
                %pre.label-box__text-visible クリックしてファイルをアップロード
            .hidden-content
              = f.fields_for :images do |i|
                = i.file_field :image, class: "hidden-field"
                %input{class:"hidden-field", type: "file", name: "item[images_attributes][1][image]", id: "item_images_attributes_1_image" }
                %input{class:"hidden-field", type: "file", name: "item[images_attributes][2][image]", id: "item_images_attributes_2_image" }
                %input{class:"hidden-field", type: "file", name: "item[images_attributes][3][image]", id: "item_images_attributes_3_image" }
                %input{class:"hidden-field", type: "file", name: "item[images_attributes][4][image]", id: "item_images_attributes_4_image" }
        .sell__block__form__name
          .form-group__name
            %label
              商品名
              %span.require 必須
            %div
              = f.text_field :name, placeholder:"商品名(必須 40文字まで)",class: "form__group__name"

        .sell__block__form__btn
          %div
            = f.submit "出品する",class: "btn-default__btn-red"
items.scss
//投稿欄以外のCSSは省略します

//image投稿欄のCSS
.post__drop__box__container{
  display: block;
  margin: 16px auto 0;
  display: flex;
  flex-wrap: wrap;

  //プレビュー表示欄のCSS
  .prev-content {
    display: flex;
    .preview-box {
      height: 162px;
      width: 112px;
      margin: 0 15px 10px 0;
      .upper-box {
        height: 112px;
        width: 100%;
        img{
          width: 112px;
          height: 112px;
        }
      }
      .lower-box {
        display: flex;
        text-align: center;
        .update-box {
          color: #00b0ff;
          width: 50%;
          height: 50px;
          line-height: 50px;
          border: 1px solid #eee;
          background: #f5f5f5;
          cursor: pointer;
        }
        .delete-box {
          color: #00b0ff;
          width: 50%;
          height: 50px;
          line-height: 50px;
          border: 1px solid #eee;
          background: #f5f5f5;
          cursor: pointer;
        }
      }
    }
  }

  //投稿クリックエリアのCSS
  .label-content{
    margin-bottom: 10px;
    width: 620px;
    .label-box {
      display: block;
      border: 1px dashed #ccc;
      position: relative;
      background: #f5f5f5;
      width: 100%;
      height: 162px;
      cursor: pointer;
      &__text-visible {
        position: absolute;
        top: 50%;
        left: 16px;
        right: 16px;
        text-align: center;
        font-size: 14px;
        line-height: 1.5;
        font-weight: bold;
        -webkit-transform: translate(0, -50%);
        transform: translate(0, -50%);
        pointer-events: none;
        white-space: pre-wrap;
        word-wrap: break-word;
      }
    }
  }

  //file_fieldのcss
  .hidden-content {
    .hidden-field {
      display: none;
    }
    .hidden-checkbox {
      display: none;
    }
  }
}

今回は画像を5枚投稿するので、file_fieldを5つ作りました。
全てdisplay: none;で隠しています。
スクリーンショット 2020-01-14 23.36.14.png
こんな感じ。
スクリーンショット 2020-01-14 23.43.16.png

5つのfile_fieldには別のidが振ってあります。
したがって、file_fieldに画像が入るたびにlabel側のforを変更していけば1枚ずつ複数の画像を投稿することができますね。

では、JSで操作してきましょう。

item_new.js
$(document).on('turbolinks:load', function(){
  $(function(){

    //プレビューのhtmlを定義
    function buildHTML(count) {
      var html = `<div class="preview-box" id="preview-box__${count}">
                    <div class="upper-box">
                      <img src="" alt="preview">
                    </div>
                    <div class="lower-box">
                      <div class="update-box">
                        <label class="edit_btn">編集</label>
                      </div>
                      <div class="delete-box" id="delete_btn_${count}">
                        <span>削除</span>
                      </div>
                    </div>
                  </div>`
      return html;
    }

    // ラベルのwidth操作
    function setLabel() {
      //プレビューボックスのwidthを取得し、maxから引くことでラベルのwidthを決定
      var prevContent = $('.label-content').prev();
      labelWidth = (620 - $(prevContent).css('width').replace(/[^0-9]/g, ''));
      $('.label-content').css('width', labelWidth);
    }

    // プレビューの追加
    $(document).on('change', '.hidden-field', function() {
      setLabel();
      //hidden-fieldのidの数値のみ取得
      var id = $(this).attr('id').replace(/[^0-9]/g, '');
      //labelボックスのidとforを更新
      $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`});
      //選択したfileのオブジェクトを取得
      var file = this.files[0];
      var reader = new FileReader();
      //readAsDataURLで指定したFileオブジェクトを読み込む
      reader.readAsDataURL(file);
      //読み込み時に発火するイベント
      reader.onload = function() {
        var image = this.result;
        //プレビューが元々なかった場合はhtmlを追加
        if ($(`#preview-box__${id}`).length == 0) {
          var count = $('.preview-box').length;
          var html = buildHTML(id);
          //ラベルの直前のプレビュー群にプレビューを追加
          var prevContent = $('.label-content').prev();
          $(prevContent).append(html);
        }
        //イメージを追加
        $(`#preview-box__${id} img`).attr('src', `${image}`);
        var count = $('.preview-box').length;
        //プレビューが5個あったらラベルを隠す 
        if (count == 5) { 
          $('.label-content').hide();
        }

        //ラベルのwidth操作
        setLabel();
        //ラベルのidとforの値を変更
        if(count < 5){
          //プレビューの数でラベルのオプションを更新する
          $('.label-box').attr({id: `label-box--${count}`,for: `item_images_attributes_${count}_image`});
        }
      }
    });

    // 画像の削除
    $(document).on('click', '.delete-box', function() {
      var count = $('.preview-box').length;
      setLabel(count);
      //item_images_attributes_${id}_image から${id}に入った数字のみを抽出
      var id = $(this).attr('id').replace(/[^0-9]/g, '');
      //取得したidに該当するプレビューを削除
      $(`#preview-box__${id}`).remove();
      console.log("new")
      //フォームの中身を削除 
      $(`#item_images_attributes_${id}_image`).val("");

      //削除時のラベル操作
      var count = $('.preview-box').length;
      //5個めが消されたらラベルを表示
      if (count == 4) {
        $('.label-content').show();
      }
      setLabel(count);

      if(id < 5){
        //削除された際に、空っぽになったfile_fieldをもう一度入力可能にする
        $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`});
      }
    });
  });
})

プレビュー表示の流れとしては、
(1) id = 0 〜 5のfile_fieldと、for = 0のlabelがある
(2) id = 0のfile_fieldに画像を放り込む
(3) labelのforがJS側で for = 1に変更される
(4) label部をクリックするとid = 1のfile_fieldに画像が入るようになる
(5) 繰り返し 5枚投稿するとlabelが消える
って感じです。
GIFで見るとわかりやすいかも。
Image from Gyazo
......7秒は短い。
しかし、1枚目を投稿したあとでfile_fieldの判定が2つ目に移動しているのがわかりますね。

また、プレビュー削除の流れとしては
(1) プレビューが2つある。それぞれが格納されているfile_fieldのidは0, 1。
(2) id = 0の方の削除ボタンを押す
(3) プレビューが消え、id = 0のfile_fieldの中身が消える
(4) labelのforオプションを0として更新
(5) id = 0のfile_fieldが再度入力可能になる
って感じで実装しています。
GIFで見ると......
Image from Gyazo
削除したfile_fieldが再度入力可能になっているのがわかりますね。
プレビュー表示側ではプレビュー数をもとにlabelのforオプションを更新するよう設定しているので、この後ファイルを追加する際にはちゃんと次の空のfile_fieldに判定が移動する実装になっています。

controller側はというと......

items_controller.rb
class ItemsController < ApplicationController

  def new
    @item = Item.new
    @images = @item.images.build
  end

  def create
    @item = Item.new(item_params)
    @item.save
  end

  private

    def item_params
      params.require(:item).permit(
        :name,
        [images_attributes: [:image]])
    end
end

こんな感じです。サンプルなのでめちゃくちゃシンプルに書いてます。お許しを。
また、DBのカラム名ですが
imagesテーブルに関しては「image」カラムにファイル名が保存される形で実装しています。

これで画像の複数投稿ができるようになりました。良かったね。

編集機能

さて今回の本題です。
編集ページへのアクセス時にどうやってプレビューを出すか......ですが、
わかりやすくeach文で表示することにしました。

edit.haml
.main
  %section.main__block
    = form_with model:@item, local:true do |f|
      %h2.sell__block__head
        商品の情報を入力
      .sell__block__form
        .sell__block__form__upload
          %h3.sell__block__form__upload__head
            出品画像
            %span.require 必須
          %p 最大5枚までアップロードできます
          .post__drop__box__container
            .prev-content

              //JSで挿入したhtmlと同じ形 each文でのプレビュー表示
              - @item.images.each do |image|
                .preview-box
                  .upper-box
                    = image_tag image.image.url, width: "112", height: "112", alt: "preview"
                  .lower-box
                    .update-box
                      %label.edit-btn 編集
                    .delete-box
                      .delete-btn
                        %span 削除
            .label-content

              //プレビューの数に合わせてforオプションを指定
              = f.label :"images_attributes_#{@item.images.length}_image", class: "label-box", id: "label-box--#{@item.images.length}" do
                %pre.label-box__text-visible クリックしてファイルをアップロード
            .hidden-content
              = f.fields_for :images do |i|

                //プレビューが出ている分のfile_fieldとdelete用のチェックボックスを設置
                = i.file_field :image, class: "hidden-field"
                = i.check_box:_destroy, class: "hidden-checkbox"

                //5つのfile_fieldを準備するに当たって、足りない分を表示
              - @item.images.length.upto(4) do |i|
                %input{name: "item[images_attributes][#{i}][image]", id: "item_images_attributes_#{i}_image", class:"hidden-field", type:"file"}
        .sell__block__form__name
          .form-group__name
            %label
              商品名
              %span.require 必須
            %div
              = f.text_field :name, placeholder:"商品名(必須 40文字まで)",class: "form__group__name"

        .sell__block__form__btn
          %div
            = f.submit "出品する",class: "btn-default__btn-red"

スクリーンショット 2020-01-15 1.45.42.png

file_field関連のdisplay: none;を外すとこんな感じになります。
スクリーンショット 2020-01-15 1.46.53.png
ちょっとわかりにくいですが、現在投稿済みのプレビューに対応するfile_fieldの横に削除用のチェックボックスが追加されています。

編集の流れとしては、
(1) id = 0, 1のfile_fieldが投稿済みのものと対応しているものとする
(2) id = 0の削除ボタンを押す
(3) プレビューが削除されるとともに、id = 0のfile_fieldに対応する削除用チェックボックスにチェックが入る
(4) 投稿時と同様、labelをクリックすると削除済みのid = 0のfile_fieldがアクティブになる
(5) 再度id = 0に新しい画像が入ったら、削除用チェックボックスのチェックを外す

こんな感じで実装していきたいと思います。
(4)を見るに、「item_new.js」に追記、場合分けする形でeditも一緒に実装できそうですね。

完成コードはこちら。

item_new.js
$(document).on('turbolinks:load', function(){
  $(function(){

    //プレビューのhtmlを定義
    function buildHTML(count) {
      var html = `<div class="preview-box" id="preview-box__${count}">
                    <div class="upper-box">
                      <img src="" alt="preview">
                    </div>
                    <div class="lower-box">
                      <div class="update-box">
                        <label class="edit_btn">編集</label>
                      </div>
                      <div class="delete-box" id="delete_btn_${count}">
                        <span>削除</span>
                      </div>
                    </div>
                  </div>`
      return html;
    }

    // 投稿編集時
    //items/:i/editページへリンクした際のアクション=======================================
    if (window.location.href.match(/\/items\/\d+\/edit/)){
      //登録済み画像のプレビュー表示欄の要素を取得する
      var prevContent = $('.label-content').prev();
      labelWidth = (620 - $(prevContent).css('width').replace(/[^0-9]/g, ''));
      $('.label-content').css('width', labelWidth);
      //プレビューにidを追加
      $('.preview-box').each(function(index, box){
        $(box).attr('id', `preview-box__${index}`);
      })
      //削除ボタンにidを追加
      $('.delete-box').each(function(index, box){
        $(box).attr('id', `delete_btn_${index}`);
      })
      var count = $('.preview-box').length;
      //プレビューが5あるときは、投稿ボックスを消しておく
      if (count == 5) {
        $('.label-content').hide();
      }
    }
    //=============================================================================

    // ラベルのwidth操作
    function setLabel() {
      //プレビューボックスのwidthを取得し、maxから引くことでラベルのwidthを決定
      var prevContent = $('.label-content').prev();
      labelWidth = (620 - $(prevContent).css('width').replace(/[^0-9]/g, ''));
      $('.label-content').css('width', labelWidth);
    }

    // プレビューの追加
    $(document).on('change', '.hidden-field', function() {
      setLabel();
      //hidden-fieldのidの数値のみ取得
      var id = $(this).attr('id').replace(/[^0-9]/g, '');
      //labelボックスのidとforを更新
      $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`});
      //選択したfileのオブジェクトを取得
      var file = this.files[0];
      var reader = new FileReader();
      //readAsDataURLで指定したFileオブジェクトを読み込む
      reader.readAsDataURL(file);
      //読み込み時に発火するイベント
      reader.onload = function() {
        var image = this.result;
        //プレビューが元々なかった場合はhtmlを追加
        if ($(`#preview-box__${id}`).length == 0) {
          var count = $('.preview-box').length;
          var html = buildHTML(id);
          //ラベルの直前のプレビュー群にプレビューを追加
          var prevContent = $('.label-content').prev();
          $(prevContent).append(html);
        }
        //イメージを追加
        $(`#preview-box__${id} img`).attr('src', `${image}`);
        var count = $('.preview-box').length;
        //プレビューが5個あったらラベルを隠す 
        if (count == 5) { 
          $('.label-content').hide();
        }

        //プレビュー削除したフィールドにdestroy用のチェックボックスがあった場合、チェックを外す=============
        if ($(`#item_images_attributes_${id}__destroy`)){
          $(`#item_images_attributes_${id}__destroy`).prop('checked',false);
        } 
        //=============================================================================

        //ラベルのwidth操作
        setLabel();
        //ラベルのidとforの値を変更
        if(count < 5){
          $('.label-box').attr({id: `label-box--${count}`,for: `item_images_attributes_${count}_image`});
        }
      }
    });

    // 画像の削除
    $(document).on('click', '.delete-box', function() {
      var count = $('.preview-box').length;
      setLabel(count);
      var id = $(this).attr('id').replace(/[^0-9]/g, '');
      $(`#preview-box__${id}`).remove();

      //新規登録時と編集時の場合分け==========================================================

      //新規投稿時
      //削除用チェックボックスの有無で判定
      if ($(`#item_images_attributes_${id}__destroy`).length == 0) {
        //フォームの中身を削除 
        $(`#item_images_attributes_${id}_image`).val("");
        var count = $('.preview-box').length;
        //5個めが消されたらラベルを表示
        if (count == 4) {
          $('.label-content').show();
        }
        setLabel(count);
        if(id < 5){
          $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`});

        }
      } else {

        //投稿編集時
        $(`#item_images_attributes_${id}__destroy`).prop('checked',true);
        //5個めが消されたらラベルを表示
        if (count == 4) {
          $('.label-content').show();
        }

        //ラベルのwidth操作
        setLabel();
        //ラベルのidとforの値を変更
        //削除したプレビューのidによって、ラベルのidを変更する
        if(id < 5){
          $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`});
        }
      }
      //=============================================================================
    });
  });
});
items_controller.rb
class ItemsController < ApplicationController

  #省略

  def edit
    @item = Item.find(params[:id])
  end

  def update
    @item = Item.find(params[:id])
    @item.update(item_update_params)
  end

  private
  def item_params
    params.require(:item).permit(
      :name,
      [images_attributes: [:image]])
  end

  def item_update_params
    params.require(:item).permit(
      :name,
      [images_attributes: [:image, :_destroy, :id]])
  end
end

JSに関しては、コメントアウトの「============================」で囲んであるところが追記箇所です。
わかりにくかったらすみません。

動作はこのようになります。
Image from Gyazo
削除を押すとチェックボックスにチェックが入ります。
このまま『編集する』ボタンを押すと、画像が削除されます。
画像を選択した場合、チェックボックスからチェックが外れます。
この状態で『編集する』ボタンを押すと、画像が差替わります。
Image from Gyazo
編集後のリンク先を指定していないので微妙な感じですが、
each文で引っ張り出される画像が更新されているのがわかりますね。ね??

一旦これで完成です。

はぁ、疲れた。

最後に

とりあえず形はなんとかできました。
まだエラーは残っていると思うので、完璧なものは保証できません。
自分で試した中ではエラー起こらなかったですが、
もし見落としがあったら教えていただけると嬉しいです。

僕ができるのはここまでです。参考にして頂けましたら幸いです。

あとドラッグ&ドロップやら10枚投稿への対応やら2段目のドロップボックス出現やら削除やら色々あると思いますが、
皆さんの手でよしなに実装してください。

また気が向いたら何か書きます。

おわり。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ruby で公開鍵の文字列から public key オブジェクトを作成する方法

概要

公開鍵の文字列から public key オブジェクトを生成する方法について少しはまったので、共有します。

コード

下記のような公開鍵があるときは

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0bXcnrheJ2snfq1wv6Qz 8+TEPDGKHCM0SsrQjxEFpXSEycL2/A+oW1ZGUzCuhz4HH4wkvc4CDJl25johSIUT Vyo4mrFrJ0ab0QAhrWE7gMyWFIfraj9cksPAGyVAiXLCN9Ly2xuoJxFjCAZXw1VO 8i7RTYK8ZP6dhcosiyzdhYt7C/65B5ikmCS4AymXIa83QQanCtjoGiwy4Cf2pLnn 9zXMZEnqQ+wwSoGn32YExmap7GAtjOwHNWU5zpW3dwNMq+zkcln3ICEBwxDpWJhE ZHZPBpPWgN+dQZDR2FiGHJgUFE3EM+CIcwxekrRBP+R3xEUeMFf5z1HeQNK8sjZe RwIDAQAB
-----END PUBLIC KEY-----

「-----BEGIN PUBLIC KEY-----」の後と「-----END PUBLIC KEY-----」の前に改行記号を入れた文字列を作ります。改行記号の前後にスペースがあるとエラーが出ます。

str = 
"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0bXcnrheJ2snfq1wv6Qz 8+TEPDGKHCM0SsrQjxEFpXSEycL2/A+oW1ZGUzCuhz4HH4wkvc4CDJl25johSIUT Vyo4mrFrJ0ab0QAhrWE7gMyWFIfraj9cksPAGyVAiXLCN9Ly2xuoJxFjCAZXw1VO 8i7RTYK8ZP6dhcosiyzdhYt7C/65B5ikmCS4AymXIa83QQanCtjoGiwy4Cf2pLnn 9zXMZEnqQ+wwSoGn32YExmap7GAtjOwHNWU5zpW3dwNMq+zkcln3ICEBwxDpWJhE ZHZPBpPWgN+dQZDR2FiGHJgUFE3EM+CIcwxekrRBP+R3xEUeMFf5z1HeQNK8sjZe RwIDAQAB\n-----END PUBLIC KEY-----"

その文字列を OpenSSL::PKey::RSA に渡します。

public_key_object = OpenSSL::PKey::RSA.new(str)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】ハッシュとJSONの変換メモ

前提

  • Ruby 2.7
  • Hash -> JSON は JSONモジュールの to_json を使用
  • JSON -> Hash は JSON.parse を使用

メモ

JSONはキーが文字列限定。Rubyのハッシュはキー、値ともに任意のオブジェクト。
ということは、RubyのハッシュからJSONに変換するときは、何らかのルールで、元のハッシュオブジェクトにJSONがサポートしていない型があった場合は変換されることとなる。

以前のメモ

JSONの形式の仕様ざっくりまとめとサンプル集 - Qiita https://qiita.com/kure/items/df04ca760290fd51b904

Rubyのハッシュの小さなサンプルメモ - Qiita https://qiita.com/kure/items/4f104bf4af4a5e243e61

サンプル

JSON -> Hash

キーは文字列のまま、値もそのまま変換される。null は nil へ。

require "json"

# JSON -> Hash
json1 = '{
  "name1": 0
}'

json2 = '{
  "name4": {
      "name5": 0
  }
}'

json3 = '{
  "name2": "value2"
}'

json4 = '{
  "name4": null
}'

pp JSON.parse(json1) # => {"name1"=>0}
pp JSON.parse(json2) # => {"name4"=>{"name5"=>0}}
pp JSON.parse(json3) # => {"name2"=>"value2"}
pp JSON.parse(json4) # => {"name4"=>nil}

Hash -> JSON

  • キー
    • 強制的に文字列に変換される。nil は空文字列として変換される。- - 値
    • シンボルやBoolean等はその文字のまま文字列として変換される。 Rubyのクラスやオブジェクト等、そのままではJSONに無い型は文字列として変換されるもよう。
# Hash -> JSON

class Test; end

hash1 = { 0 => 1 }
pp hash1.to_json # => "{\"0\":1}"

hash2 = { "name" => "value" }
pp hash2.to_json # => "{\"name\":\"value\"}"

hash3 = { true => "false_str" }
pp hash3.to_json # => "{\"true\":\"false_str\"}"

hash4 = { name: "value" }
pp hash4.to_json # => "{\"name\":\"value\"}"

hash5 = { nil => nil }
pp hash5.to_json #=> "{\"\":null}"

hash6 = { "class" => Class }
pp hash6.to_json # => "{\"class\":\"Class\"}"

hash7 = { "class" => Test }
pp hash7.to_json # => "{\"class\":\"Test\"}"

hash8 = { "bool" => true }
pp hash8.to_json # => "{\"bool\":true}"

hash9 = {Class => Class}
pp hash9.to_json # => "{\"Class\":\"Class\"}"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unknown action ,The action 'create' could not be found for MessagesControllerの一例

1.どんなエラー

MessagesControllerにcreateが定義されていませんよという内容です。
ちなみに筆者は下記の通りcreateは定義していました。

<エラー文>

スクリーンショット 2020-01-14 21.03.34.png

<エラーに該当するファイル>

messages_controller.rb
class MessagesController < ApplicationController
  before_action :set_group

  def index
    @message = Message.new
    @messages = @group.messages.includes(:user)
  end
end

  def create
    @message = @group.messages.new(message_params)
    if @message.save
      redirect_to group_messages_path(@group), notice: 'メッセージが送信されました'
    else
      @messages = @group.messages.includes(:user)
      flash.now[:alert] = 'メッセージを入力してください。'
      render :index
    end
  end

  private

  def message_params
    params.require(:message).permit(:content, :image).merge(user_id: current_user.id)
  end

  def set_group
    @group = Group.find(params[:group_id])
  end

2.原因

今回の場合は'class MessagesController'の'end'の位置が'create'の定義の前で記入してしまっていたため、定義されていないという状態となっていました。
なので八行目の'end'を一番下にカットアンドペーストすることで解決します。

3.類似ケース

ちなみに下記のurlでは変数名を間違っていたため、同様なエラーとなっていますが、筆者も変数'message'を'messages'としているというミスがあったため、そちらも確認していただければいいのかなと思います
<参考にした記事>
https://teratail.com/questions/83406

<筆者が変数で間違えていた箇所>

_message.html.haml
.messages   ←この変数が複数形になっていました
  .chat-main__message-list
    .chat-main__message-list__name
      =message.user.name
      .chat-main__message-list__name__date
        =message.created_at.strftime("%y年%m月%d日%H時%M分")
    .chat-main__message-list__comment
      - if message.content.present?
        %p.chat-main__message-list__comment__content
          = message.content
          = image_tag message.image.url, class: 'chat-main__message-list__comment__content__image' if message.image.present?

ご参考になればと思います

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む