1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

セレクトボックスから複数のデータを取り出すまで

Last updated at Posted at 2020-09-11

Ruby on Railsにおいてセレクトボックスから複数のデータを取り出す作業が意外と複雑な為、これまで試行錯誤した過程をこちらで整理しています。
まだ解決済みではありませんので、進捗が有り次第更新していきます(追記:問題解決しました。)

バージョン情報

ruby 2.6.4p104
RubyGems 3.0.3
Rails 5.2.3

やりたいこと

現在語学学習系SNSサイトを制作しています。今回はプロフィール編集画面で、ユーザーが学習中の言語を複数選択出来るセレクトボックスを作成したいと思います。
最終的には世界中の言語が網羅されているgemを使おうと思うのですが、まずはテスト段階として「日本語、英語、中国語、スペイン語」の4言語が入ったセレクトボックスを作っていきます。

多対多の関係性

「ユーザー」と「言語」は多対多の関係にあると思いますので、「user_language」と言う中間テーブルを用い「user」と「language」を多対多の関係で結びました。

Userモデル

models/user.rb
class User < ApplicationRecord
 has_secure_password
 has_many :languages, through: :user_language
 has_many :user_language  

 def language_name
   ::LanguageSelect::LANGUAGES[language]
 end
end

Languageモデル

models/language.rb
class Language < ApplicationRecord
  has_many :users, through: :user_language
  has_many :user_language  
end

UserモデルとLanguageモデルの中間テーブル

models/user_language.rb
class Language < ApplicationRecord
  belongs_to :user
  belongs_to :language 
end

schema

db/schema.rb
ActiveRecord::Schema.define(version: 2020_05_21_034306) do

  create_table "languages", force: :cascade do |t|
    t.string "description"
    t.boolean "done"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["user_id"], name: "index_languages_on_user_id"
  end

  create_table "user_languages", force: :cascade do |t|
    t.integer "user_id"
    t.integer "language_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["language_id"], name: "index_user_languages_on_language_id"
    t.index ["user_id"], name: "index_user_languages_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "image_name"
    t.string "password_digest"
    t.string "cover_image_name"
    t.string "sex"
    t.string "country"
    t.string "language"
    t.text "introduction"
  end

プロフィール編集ページは以下の様に記述。

views/users/edit.html.erb
  <div class="col-1">Spoken language:</div>
  <div class="col-2">
      <select name="language[]" multiple="multiple" class="form-control">
        <option>日本語</option>
        <option>英語</option>
        <option>中国語</option>
        <option>スペイン語</option>
      <input type="button" value="+" class="add pluralBtn">
      <input type="button" value="-" class="del pluralBtn">
  </div>

コントローラは以下の様に記述。

controllers/users_controller.rb
  def update
    @user = User.find_by(id: params[:id])    
    @user.languages = params[:language]
    if @user.save
      flash[:notice] = "ユーザー情報を編集しました"
      redirect_to("/users/#{@user.id}")
    else
      render("users/edit")
    end
  end

最後にプロフィールページは以下の様に記述。

views/users/show.html.erb
    <div class="language">
    <% @user.languages.each do |language| %>
      <%= @user.language %>
    <% end %>
    </div>

HasManyThroughOrderのエラー

準備は整ったので複数の言語を選択し保存しようとすると、以下のエラーが発生しました。

ActiveRecord::HasManyThroughOrderError in UsersController#update
Cannot have a has_many :through association 'User#languages' which goes through 'User#user_language' before the through association is defined.
Extracted source (around line #125):
 
    @user.languages = params[:language]

多対多の関係を記述する際に、中間テーブルに関するコード(ここではuser_language)を先に書かないと上記のエラーが発生するようです。
こちらの記事を参考にして、リレーションについて以下の様に記述し直しました。

Userモデル

models/user.rb
class User < ApplicationRecord
 has_secure_password
 has_many :user_language
 has_many :languages, through: :user_language

 def language_name
   ::LanguageSelect::LANGUAGES[language]
 end
end

Languageモデル

models/language.rb
class Language < ApplicationRecord
  has_many :user_language
  has_many :users, through: :user_language
end

AssociationTypeMissmatchのエラー

書き直す事でHasManyThroughOrderのエラーは表示されなくなりましたが、今度は以下のエラーが表示されました。

ActiveRecord::AssociationTypeMismatch in UsersController#update
Language(#135869460) expected, got "英語" which is an instance of String(#21954020)
    @user.languages = params[:language]

Languageのインスタンスを受け取るつもりが"英語"と言う文字列を受け取ってしまった、と言う内容のようです。
なのでそれを防ぐために、プロフィール編集ページにおける選択肢の言語それぞれにvalue属性を付与する事にしました。

views/users/edit.html.erb
  <div class="col-1">Spoken language:</div>
  <div class="col-2">
      <select name="language[]" multiple="multiple" class="form-control">
        <option>日本語</option>
        <option>英語</option>
        <option>中国語</option>
        <option>スペイン語</option>
      <input type="button" value="+" class="add pluralBtn">
      <input type="button" value="-" class="del pluralBtn">
  </div>

こうだった物を、以下の様に変更。

views/users/edit.html.erb
  <div class="col-1">Spoken language:</div>
  <div class="col-2">
      <select name="language[]" multiple="multiple" class="form-control">
        <option value="1">日本語</option>
        <option value="2">英語</option>
        <option value="3">中国語</option>
        <option value="4">スペイン語</option>
      <input type="button" value="+" class="add pluralBtn">
      <input type="button" value="-" class="del pluralBtn">
  </div>

再度AssociationTypeMissmatchのエラー

これで解決したかと思いきや、再び同じエラー文が。

ActiveRecord::AssociationTypeMismatch in UsersController#update
Language(#128573540) expected, got "2" which is an instance of String(#21954020)
    @user.languages = params[:language]

今回こそはlanguage_idを渡したつもりが、ここでもダブルクォーテーション付きの"2"と言う「文字列」として渡ってしまっているようです。

実はこれは、Railsのvalueで送れるデータは文字列限定であると言う事が恐らくの原因でした。

中々上手くいかないとラジオボタンなどで済ませてしまいたくなる気持ちもありますが、最終的には100個以上の言語を選択肢に入れたいので、やはりここはセレクトボックスにこだわって頑張っていきます。

今後

取りあえず今はここで躓いている状態です。
細かく整理しますと、コントローラにおいてユーザーが学習中の言語の情報を以下の様に「@user.languages」と定義した場合はAssociationTypeMismatchのエラーが表示されます。

controllers/users_controller.rb
  def update
    @user = User.find_by(id: params[:id])    
    @user.languages = params[:language]
    if @user.save
      flash[:notice] = "ユーザー情報を編集しました"
      redirect_to("/users/#{@user.id}")
    else
      render("users/edit")
    end
  end

また以下の様に「@user.language」と単数形で定義した場合はエラーは表示されないものの、["1", "2"]のように配列の形のまま(文字列として)出力されます。

controllers/users_controller.rb
  def update
    @user = User.find_by(id: params[:id])    
    @user.language = params[:language]
    if @user.save
      flash[:notice] = "ユーザー情報を編集しました"
      redirect_to("/users/#{@user.id}")
    else
      render("users/edit")
    end
  end

日本語 中国語のように、[]や""を含まない形でデータを個別に取り出し表示させるべく、解決方法を考えていきます。
今考えている方法は2つ:

方法その1
["1", "2"]と言った文字列を受け取った後に数値型に変換する。

方法その2
データを受け取る前に数値型を指定出来るのなら指定する。

2つともどのように記述していけば良いか分からず現在試行錯誤している状況です。
解決策を期待して読んで下さった方には申し訳ありませんが、進捗が有り次第更新していきます。

追記(問題解決しました)

その後ご助言を頂けたので、コントローラの記述を変える所から始めました。

controllers/users_controller.rb
  def update
    @user = User.find_by(id: params[:id])    
    @user.languages = params[:language]
    if @user.save
      flash[:notice] = "ユーザー情報を編集しました"
      redirect_to("/users/#{@user.id}")
    else
      render("users/edit")
    end
  end

上記記述だった物を、以下の様に変更しました(言語のデータを受け取る箇所)。
またその前後でのデータの受け渡しを確認する為、pryの記述も加えました。

controllers/users_controller.rb
binding.pry
@user.language_ids = params[:language]
@user.language = params[:language]
binding.pry

_idsの記述に関してはこちらで参照できますが、まだ完全な理解には至っておりません。)
https://railsguides.jp/association_basics.html#has-many%E3%81%A7%E8%BF%BD%E5%8A%A0%E3%81%95%E3%82%8C%E3%82%8B%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89-collection-singular-ids-ids

RecordNotFoundのエラー

上記のコードで試してみるも、再度エラーが表示されました。
今回複数選択した言語は2つだったので、「2つ分のidをLanguagesから取得しようとしたが、実際見つかったidは0個だった。3と4と言うidを持つLanguagesは見つかりません」と言う意味の様です。

ActiveRecord::RecordNotFound in UsersController#update
Couldn't find all Languages with 'id': (3, 4) (found 0 results, but was looking for 2). Couldn't find Languages with ids 3, 4.
    @user.country = params[:country]   
    binding.pry 
    @user.language_ids = params[:language]
    binding.pry 

しかしここで、languagesテーブルにそもそも言語のレコードが入っていない事が発覚しました。
「性別」や「国籍」などの項目と同じ様に、プロフィール編集ページ(views/users/edit.html.erb)にオプションを羅列すれば良いのだと思っていましたが、「言語」は多対多でテーブルが別にあるため、そちらでのレコードの事前準備が必要なのだと言う事を知りました…。

pry(main)> Language.all
+----+-------------+------+---------+-------------------------+-------------------------+
| id | description | done | user_id | created_at              | updated_at              |
+----+-------------+------+---------+-------------------------+-------------------------+
| 1  | Japanese    |      |         | 2020-09-29 07:05:30 UTC | 2020-09-29 07:05:30 UTC |
| 2  | English     |      |         | 2020-09-29 07:06:11 UTC | 2020-09-29 07:06:11 UTC |
| 3  | Chinese     |      |         | 2020-09-29 06:20:27 UTC | 2020-09-29 06:20:27 UTC |
| 4  | Spanish     |      |         | 2020-09-29 06:21:00 UTC | 2020-09-29 06:21:00 UTC |
+----+-------------+------+---------+-------------------------+-------------------------+

上記のようにレコードを準備した上で、再度ビューにて言語を複数選択しました。するとpryからは以下の様に返ってきました。

  UserLanguage Destroy (4.0ms)  DELETE FROM "user_languages" WHERE "user_languages"."user_id" = ? AND "user_languages"."language_id" IN (?, ?)  [["user_id", 13], ["language_id", 4], ["language_id", 3]]
  ↳ app/controllers/users_controller.rb:126
  UserLanguage Create (0.6ms)  INSERT INTO "user_languages" ("user_id", "language_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["user_id", 13], ["language_id", 1], ["created_at", "2020-09-29 07:07:38.908759"], ["updated_at", "2020-09-29 07:07:38.908759"]]
  ↳ app/controllers/users_controller.rb:126
  UserLanguage Create (0.2ms)  INSERT INTO "user_languages" ("user_id", "language_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["user_id", 13], ["language_id", 2], ["created_at", "2020-09-29 07:07:38.917779"], ["updated_at", "2020-09-29 07:07:38.917779"]]

language_id 4と3を選択していたユーザーの言語情報が、language_id 1と2にしっかりと上書きされたようです。更にはビューで以前出ていた「idが見つからないよ」と言うエラーも表示されなくなりました。

viewの記述

しかし今度はプロフィール画面に飛んでも、今度は言語の情報が上書きされずに以前のまま表示されるようになりました。以前保存していた["1", "3", "4"]がそのまま表示されると言った具合です。

これは、viewで誤った記述をしていた事が原因でした。

<% @user.languages.each do |language| %>
  <%= @user.language %>
<% end %>

上記の記述ですと、恐らくLanguageのインスタンスを配列で受け取ってしまいます。
先ほどレコードに載せたように、languageテーブルにはiddescriptionと言ったカラムがあるので、そこを指定する必要がありました。

ですので以下の様に記述する事で、日本語 中国語のように、[]や""を含まない形でデータを個別に取り出す事に成功しました!

<% @user.languages.each do |language| %>
  <%= language.description %>
<% end %>

おわりに

選んだ言語を表示するだけのシンプルな作業だと始める前は思っていましたが、実は多対多やデータベース等に関して様々な知識が必要で、デバッグツールやERD図なども使って考えていかないと解決が難しい題材でした。

今後このように複雑な問題に出くわした時は、デバッグツールを使うなどして、どの時点でおかしくなったのか問題を切り分けながら考えられるようになればと思います。

また欲を言えば便利なgemなども探していけたらと思いますが、取りあえず最低限の機能の実装が出来れば一先ずは良いので、追々変更があれば追記致します。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?