Search code examples
arraysruby-on-railsenumsmodel

Rails map array of word or enum to integer or float value


I'm not sure what this question is even called, but is it possible to have a list in a model via:

array of word

equipment %w[foo bar kimi etc...]
equipcost %i[10 35 85 etc...] 

or enum

enum equipment: { foo: 10,
                  bar: 35,
                 kimi: 83,
                 etc...
               }

Then save multiple in an array i.e.

 t.string "equipment_list", default: [], array: true

{ equipment_list => ["foo", "bar"] } or { equipment_list => [10, 35] }

Then when the object is called via @object.equipment_list, it references the the array of words or enum.

So in the view I say <%= @object.equipment_list %> and have the foo and bar display. Or if I'm in the model I can have a method that adds the values to get a total cost:

def cost
   e = self.equipment_list
   e.value
   e.inject(:+)
end

Is there a ruby or rails way of doing this? The array of words way seems wrong and the emun way is only for a single value.

I found this but like they say it is an index not an actual value.


Solution

  • Hardcoding that data into your application is just not a good idea unless you want to change the code every time the buisness logic demands trivial changes or you like being called up at 3 am. Using an array/JSON/HSTORE/Serialized text column isn't actually a good approach either:

    • No data normalization
    • Bad type support (no Decimal type)
    • No Assocations
    • Violates First Normal Form (1NF)
    • Querying the data is both harder and less performant.

    Instead create a join table where you can store the relation between a store and equipment as well as any additional data that describes the relation between the two buisness entities:

    class Equipment < ApplicationRecord
      has_many :store_equipments
      has_many :stores, through: :store_equipments
    end
    
    class Store
      has_many :store_equipments
      has_many :equipments, through: :store_equipments
    end
    
    # rails g model store_equipment store:belongs_to equipment:belongs_to cost:decimal
    class StoreEquipment < ApplicationRecord
      belongs_to :store
      belongs_to :equipment
    end
    

    Naming the table/model after the two things it joins is just a very lazy convention - use a more appropriate name that fits the buisness logic of what that thing actually represents when possible.

    The exact implementation here will depend on your buisness logic and you'll have to consider stuff like how you want to handle quantities. If you wanted to get the sum of the cost of the equipment the naive implementation is:

    # for a single record
    store = Store.find(1)
    equipment_cost = @store.store_equipments.sum(:cost)
    
    # fetch it for a group of records in a single query
    stores = Store.left_joins(:store_equipments)
                   .select(
                     Store.arel_table[Arel.star],
                     StoreEquipment.arel_table[:cost].sum.as('equipment_cost')
                   )
                   .group(:id)