0

I got a variable in a nested model (account) belonging the user model in which there is a column for :skills. in my edit_user_path, i would like to be able to save multiple skills into that column via checkboxes. For doing that I permitted the skills in the controller as array, even though it is saved as string into the database.

My controller:

    def user_params
        params.require(:user).permit(:email, :password, :password_confirmation, :account, 
            account_attributes:[:id, :username, {:skills => []}, :description, :location, :avatar, :tags, :tag_list])
    end

If i save multiple values via checkboxes into this variable inside of a nested form, i do it like this:

        <% skills = ["Coding", "Design", "Petting Cats"] %>
        <% skills.each do |skill| %>

        <div class="form-check form-check-inline">
          <div class="custom-control custom-checkbox">
            <%= form.check_box :skills, { multiple: true, class:"custom-control-input",id: skill }, skill, false %>
            <%= form.label skill, class:"custom-control-label", for: skill %>
          </div>
        </div>

        <% end %>

This works and the values are getting saved as an array, but oddly the array once saved into the database looks like this:

"[\"Artist\", \"Mixing\", \"Mastering\"]" 

instead of this:

["Artist", "Mixing", "Mastering"] 

Which leads to troubles, since i would like to iterate through all users later "filtering" for certain skills, like User.account.where(skills: "Petting Cats") if a user has Petting Cats saved somewhere inside of the array.

For Development i am using SQLite, for production PostgresQL. How do i save multiple strings into a string variable as clean array without mess, and how to iterate through the array later with a where query method?

5
  • 3
    This seems like a uniquely bad idea. You should, IMO, have a Skill model and create a m:m relationship between User and Skill using a UserSkill join model (using has_many :through). Also, you're asking for trouble by developing with SQLite and deploying with PostgreSQL. There are subtle differences that can pop up and cause you heartache. Best to keep development as close to production as possible. Commented Apr 14, 2020 at 18:01
  • Is that column set serialize? Commented Apr 14, 2020 at 18:13
  • @tadman no i dont think so. i havent configured it though. Commented Apr 14, 2020 at 19:25
  • @jvillian good point. i have thought of this already but thought it would be an overkill since i only want to store a tiny bit of content. isnt it possible instead of creating a join model, just to go for a one:many relationship between skill and user? also how would i realize this in a form? since most of the users will have multiple skills i would have to create almost every time two records for skill with one form Commented Apr 14, 2020 at 19:30
  • It would be good if you maintain a separate skill table. Tomorrow you may need to add few more fileds related to skill, (like skill version , level of expertise in that skill) Commented Apr 23, 2020 at 15:18

1 Answer 1

4

You're falling into the common double whammy noob trap of using an array column and serialize.

The reason you are getting "[\"Artist\", \"Mixing\", \"Mastering\"]" stored in is that you are using serialize with a native array column. serialize is an old hack to store stuff in varchar/text columns and the driver actually natively converts it to an array. When you use serialize with a native array/json/hstore column your actually casting the value into a string before the driver casts it. The result is:

["[\"Artist\", \"Mixing\", \"Mastering\"]"]

Removing serialize will fix the immediate problem but you're still left with a substandard solution compared to actually doing the job right:

# rails g model skill name:string:uniq
class Skill < ApplicationRecord
  validates :name, uniqueness: true, presence: true
  has_many :user_skills, dependent: :destroy
  has_many :users, through: :user_skills
end
# rails g model user_skill user:belongs_to skill:belongs_to
# use `add_index :user_skills, [:user_id, :skill_id], unique: true` to ensure uniqueness
class UserSkill < ApplicationRecord
  validates_uniqueness_of :user_id, scope: :skill_id
  belongs_to :user
  belongs_to :skill
end
class User < ApplicationRecord
  has_many :user_skills, dependent: :destroy
  has_many :skills, through: :user_skills
end
<%= form_with(model: @user) do |form| %>
  <div class="field"> 
    <%= form.label :skill_ids %>
    <%= form.collection_select :skill_ids, Skill.all, :id, :name %>
  </div>
<% end %>
def user_params
  params.require(:user).permit(
    :email, :password, :password_confirmation, :account, 
    account_attributes: [
      :id, :username, :description, :location, :avatar, :tags, :tag_list,
    ],
    skill_ids: [] 
 )
end

This gives you:

  • Queryable data
  • Referential integrity
  • Normalization
  • Encapsulation
  • ActiveRecord Assocations!
  • The pleasure of not looking like an idiot in a review/audit
Sign up to request clarification or add additional context in comments.

8 Comments

lul this made my day. also thank you for the in detail explaination
One question. where do you put the add_index in the migration or in the model?
In the migration. In the def change method.
i see. can you explain why you chose putting the param inside of the account_attributes, since there is a relation between the UserSkill model and the user ? Also I am getting an undefined method error for skill_ids :S
ah yeah tack min vän
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.