I have two models with "has_many :through" association. It’s working good, but I need to add unique ordering for each of combination.
Let’s say we have model Train and Carriages (Railway Carriage) and each train has unique combination of carriages
# /models/train.rb
class Train < ActiveRecord::Base
has_many :carriages, through: :combinations
end
# /models/carriage.rb
class Carriage < ActiveRecord::Base
has_many :trains, through: :combinations
end
# /models/combination.rb
class Combination < ActiveRecord::Base
belongs_to :train
belongs_to :carriage
end
# /controllers/trains_controller.rb
class TrainsController < ApplicationController
def shortcut_params
params.require(:train).permit(:name, :description, carriage_ids: [])
end
end
# /views/trains/_form.html.erb
<div class="field">
<%= f.label :carriage_ids, 'Choose Carriages' %><br>
<%= f.select :carriage_ids, Carriage.all.collect { |x| [x.name, x.id] }, {}, multiple: true, size: 6 %>
</div>
For example:
train_1 = carriage_5, carriage_4, carriage_1, carriage_3, carriage_2, carriage_6
train_2 = carriage_6, carriage_5, carriage_3, carriage_1, carriage_2, carriage_4
train_3 = carriage_1, carriage_2, carriage_3, carriage_4, carriage_6, carriage_5
In this example carriage_5 have:
first place in train_1,
second place in train_2,
last place in train_3.
It’s mean I can’t use solution like this https://stackoverflow.com/a/19138677/4801165, because I don’t have parameter to order carriages.
In database I see that carriage_ids saving from 1 to 5 (from lowest to highest id), so may be there is solution to add ids one by one?
I hope there is easy solution to get correct ordering of carriage for each Train.
Thanks
You can add a position attribute to your Combination model denoting the position of the Carriage within the Train.
I tried to use Fred Willmore advice but this additional column not need if you use nested forms to adding manually each element.
You can find Gem here https://rubygems.org/gems/cocoon and use this nice guide with Standard Rails Forms https://github.com/nathanvda/cocoon/wiki/ERB-examples.
Related
In my application I have a search_volume.rb model that looks like this:
search_volume.rb:
class SearchVolume < ApplicationRecord
# t.integer "keyword_id"
# t.integer "search_engine_id"
# t.date "date"
# t.integer "volume"
belongs_to :keyword
belongs_to :search_engine
end
keyword.rb:
class Keyword < ApplicationRecord
has_and_belongs_to_many :labels
has_many :search_volumes
end
search_engine.rb:
class SearchEngine < ApplicationRecord
belongs_to :country
belongs_to :language
end
label.rb:
class Label < ApplicationRecord
has_and_belongs_to_many :keywords
has_many :search_volumes, through: :keywords
end
On the label#index page I am trying to show the sum of search_volumes for the keywords in each label for the last month for the search_engine that the user has cookied. I am able to do this with the following:
<% #labels.each do |label| %>
<%= number_with_delimiter(label.search_volumes.where(search_engine_id: cookies[:search_engine_id]).where(date: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).sum(:volume)) %>
<% end %>
This works well but I have the feeling that the above is very inefficient. With the current approach I also dind it difficult to do operations on search volumes. Most of the time I just want to know about last month's search volume.
Normally I would create a counter_cache on the keywords model to keep track of the latest search_volume, but since there are dozens of search_engines I would have to create one for each, which is also inefficient.
What's the most efficient way to store last month's search volume for all the different search engines separately?
First of all, you can optimize your current implementation by doing one single request for all involved labels like so:
# models
class SearchVolume < ApplicationRecord
# ...
# the best place for your filters!
scope :last_month, -> { where(date: 1.month.ago.beginning_of_month..1.month.ago.end_of_month) }
scope :search_engine, ->(search_engine_id) { where(search_engine_id: search_engine_id) }
end
class Label < ApplicationRecord
# ...
# returns { label_id1 => search_volumn_sum1, label_id2 => search_volumn_sum2, ... }
def self.last_month_search_volumes_per_label_report(labels, search_engine_id:)
labels.
group(:id).
left_outer_joins(:search_volumes).
merge(SearchVolume.last_month.search_engine(search_engine_id)).
pluck(:id, 'SUM(search_volumes.volume)').
to_h
end
end
# controller
class LabelsController < ApplicationController
def index
#labels = Label.all
#search_volumes_report =
Label.last_month_search_volumes_per_label_report(
#labels, search_engine_id: cookies[:search_engine_id]
)
end
end
# view
<% #labels.each do |label| %>
<%= number_with_delimiter(#search_volumes_report[label.id]) %>
<% end %>
Please note that I have not tested it with the same architecture, but with similar models I have on my local machine. It may work by adjusting a few things.
My proposed approach still is live requesting the database. If you really need to store values somewhere because you have very large datasets, I suggest two solutions:
- using materialized views you could refresh each month (scenic gem offers a good way to handle views in Rails application: https://github.com/scenic-views/scenic)
- implementing a new table with standard relations between models, that could store your calculations by ids and months and whose you could populate each month using rake tasks, then you would simply have to eager load your calculations
Please let me know your feedbacks!
I have a polymorphic association (belongs_to :resource, polymorphic: true) where resource can be a variety of different models. To simplify the question assume it can be either a Order or a Customer.
If it is a Order I'd like to preload the order, and preload the Address. If it is a customer I'd like to preload the Customer and preload the Location.
The code using these associations does something like:
<%- #issues.each do |issue| -%>
<%- case issue.resource -%>
<%- when Customer -%>
<%= issue.resource.name %> <%= issue.resource.location.name %>
<%- when Order -%>
<%= issue.resource.number %> <%= issue.resource.address.details %>
<%- end -%>
Currently my preload uses:
#issues.preload(:resource)
However I still see n-plus-one issues for loading the conditional associations:
SELECT "addresses".* WHERE "addresses"."order_id" = ...
SELECT "locations".* WHERE "locations"."customer_id" = ...
...
What's a good way to fix this? Is it possible to manually preload an association?
You can do that with the help of ActiveRecord::Associations::Preloader class. Here is the code:
#issues = Issue.all # Or whatever query
ActiveRecord::Associations::Preloader.new.preload(#issues.select { |i| i.resource_type == "Order" }, { resource: :address })
ActiveRecord::Associations::Preloader.new.preload(#issues.select { |i| i.resource_type == "Customer" }, { resource: :location })
You can use different approach when filtering the collection. For example, in my project I am using group_by
groups = sale_items.group_by(&:item_type)
groups.each do |type, items|
conditions = case type
when "Product" then :item
when "Service" then { item: { service: [:group] } }
end
ActiveRecord::Associations::Preloader.new.preload(items, conditions)
You can easily wrap this code in some helper class and use it in different parts of your app.
This is now working in Rails v6.0.0.rc1: https://github.com/rails/rails/pull/32655
You can do .includes(resource: [:address, :location])
You can break out your polymorphic association into individual associations. I have followed this and been extremely pleased at how it has simplified my applications.
class Issue
belongs_to :order
belongs_to :customer
# You should validate that one and only one of order and customer is present.
def resource
order || customer
end
end
Issue.preload(order: :address, customer: :location)
I have actually written a gem which wraps up this pattern so that the syntax becomes
class Issue
has_owner :order, :customer, as: :resource
end
and sets up the associations and validations appropriately. Unfortunately that implementation is not open or public. However, it is not difficult to do yourself.
You need to define associations in models like this:
class Issue < ActiveRecord::Base
belongs_to :resource, polymorphic: true
belongs_to :order, -> { includes(:issues).where(issues: { resource_type: 'Order' }) }, foreign_key: :resource_id
belongs_to :customer, -> { includes(:issues).where(issues: { resource_type: 'Customer' }) }, foreign_key: :resource_id
end
class Order < ActiveRecord::Base
belongs_to :address
has_many :issues, as: :resource
end
class Customer < ActiveRecord::Base
belongs_to :location
has_many :issues, as: :resource
end
Now you may do required preload:
Issue.includes(order: :address, customer: :location).all
In views you should use explicit relation name:
<%- #issues.each do |issue| -%>
<%- case issue.resource -%>
<%- when Customer -%>
<%= issue.customer.name %> <%= issue.customer.location.name %>
<%- when Order -%>
<%= issue.order.number %> <%= issue.order.address.details %>
<%- end -%>
That's all, no more n-plus-one queries.
I would like to share one of my query that i have used for conditional eager loading but not sure if this might help you, which i am not sure but its worth a try.
i have an address model, which is polymorphic to user and property.
So i just check the addressable_type manually and then call the appropriate query as shown below:-
after getting either user or property,i get the address to with eager loading required models
###record can be user or property instance
if #record.class.to_s == "Property"
Address.includes(:addressable=>[:dealers,:property_groups,:details]).where(:addressable_type=>"Property").joins(:property).where(properties:{:status=>"active"})
else if #record.class.to_s == "User"
Address.includes(:addressable=>[:pictures,:friends,:ratings,:interests]).where(:addressable_type=>"User").joins(:user).where(users:{is_guest:=>true})
end
The above query is a small snippet of actual query, but you can get an idea about how to use it for eager loading using joins because its a polymorphic table.
Hope it helps.
If you instantiate the associated object as the object in question, e.g. call it the variable #object or some such. Then the render should handle the determination of the correct view via the object's class. This is a Rails convention, i.e. rails' magic.
I personally hate it because it's so hard to debug the current scope of a bug without something like byebug or pry but I can attest that it does work, as we use it here at my employer to solve a similar problem.
Instead of faster via preloading, I think the speed issue is better solved through this method and rails caching.
I've come up with a viable solution for myself when I was stuck in this problem. What I followed was to iterate through each type of implementations and concatenate it into an array.
To start with it, we will first note down what attributes will be loaded for a particular type.
ATTRIBS = {
'Order' => [:address],
'Customer' => [:location]
}.freeze
AVAILABLE_TYPES = %w(Order Customer).freeze
The above lists out the associations to load eagerly for the available implementation types.
Now in our code, we will simply iterate through AVAILABLE_TYPES and then load the required associations.
issues = []
AVAILABLE_TYPES.each do |type|
issues += #issues.where(resource_type: type).includes(resource: ATTRIBS[type])
end
Through this, we have a managed way to preload the associations based on the type. If you've another type, just add it to the AVAILABLE_TYPES, and the attributes to ATTRIBS, and you'll be done.
So I am having some problems with strong parameters and nested forms (suprise! Nobody's ever had that problem before lol) and I've been looking at several threads, trying different solutions but still can't get a nested attribute to work so I turn to you, fellow programmers. Firstly lets look at the code, shall we? If you find anything fishy, let me know!
Aventure.rb
class Adventure < ActiveRecord::Base
belongs_to :user
has_many :photos
has_many :reservations
has_many :activity_dates
accepts_nested_attributes_for :activity_dates
...
end
Activity_date.rb
class ActivityDate < ActiveRecord::Base
belongs_to :adventure
has_many :time_slots
accepts_nested_attributes_for :time_slots
end
Timeslot.rb
class TimeSlot < ActiveRecord::Base
belongs_to :activity_date
end
_form.hmtl.erb
<%= form_for #adventure, html: {multipart: true} do |f| %>
...
<%= f.fields_for :activity_dates, #adventure.activity_dates.new do |a| %>
<%= a.date_field :date %>
<%= a.hidden_field :adventure_id, value: #adventure.id %>
<% end %>
adventure_controller.erb
def adventure_params
params.require(:adventure).permit(:activity_name,:description,:leader,
:company_name,:adress,:max_perticipants,
:price,:currency,{:activity_dates=>[:dates]})
end
Right, so when I inspect the params like below I get these
hashes (see img link):
render json: { p: params.inspect, ad:adventure_params.inspect }
I have concluded that activity_dates shows up in params, but NOT in ad:adventure_params. Using params[:activity_dates] gives a ForbiddenAttributesError, which was expected. That's not a good way of going about it, as it is not permitted in the strong params. I would however like to get :activity_dates with it's attribute date, and later on even it's nested attribute for :timeslots. But no matter how many solutions I have looked at, I have not been getting the desired results. What am I doing wrong? Help me Obi-Wan, you are my only hope!
For nested attributes you need to add '_attributes' to the end of field name when you are adding them to your strong parameters, so you need to permit activity_dates as activity_dates_attributes as follows:
params.require(:adventure).permit(:activity_dates_attributes=>[:dates])
or as follows with your other permitted parameters:
params.require(:adventure).permit(:activity_name,:description,:leader,
:company_name,:adress,:max_perticipants,
:price,:currency, :activity_dates_attributes=>[:dates])
For more information on whitelisting strong parameters here are some useful links here: http://edgeapi.rubyonrails.org/classes/ActionController/StrongParameters.html
https://github.com/rails/strong_parameters/
I have a functioning, self built e-com web app, but right now the app assumes we have infinite quantity.
It uses line_items and product models.
I am going to add stock_QTY as an attribute to the product
For items that don't have any variants (sizes, colors etc.), the line_item will be created if and stock_QTY is greater than one.
I'm not sure how to deal with sizes though.
Should I create different Products? IE:
Shirt.create (name:"small green shirt", color:"green", size:S, stock_QTY:4)
Shirt.create (name:"medium green shirt", color:"green", size:M, stock_QTY:6)
Shirt.create (name:"large green shirt", color: "green", size:L, stock_QTY:1)
This seems repetitive, but at least the stock QTY can have some independence. Is there a way to create only one shirt record, with variants, and allow them to have different sizes?
Ideally I'd like
Shirt.create(name:"shirt", colors:['red', 'blue', 'green'], sizes: ['s','m',l'])
and then be able to do
Shirt.where(color => "green").where(size => "L").stock_QTY
=> X number
Shirt.where(color => "green").where(size => "M").stock_QTY
=> Y number
This way I have one model, but it can store different quantities depending on the scope of the variants.
Let me know if this is unclear.
Thanks!
Update
Product.rb
require 'file_size_validator'
class Product < ActiveRecord::Base
has_many :line_items
before_destroy :ensure_not_referenced_by_any_line_item
mount_uploader :image, ImageUploader
validates :price, :numericality => {:greater_than_or_equal_to => 0.01}
validates :title, :uniqueness => true
def to_param
"#{id}_#{permalink}"
end
private
# ensure that there are no line items referencing this product
def ensure_not_referenced_by_any_line_item
if line_items.empty?
return true
else
errors.add(:base, 'Line Items present')
return false
end
end
end
Here is my Product as it is now.
from seeds.rb
Product.create!([
{
:title => "Liaisons Shirt Green",
:description => "",
:has_size => true,
:price => 24.99,
:permalink => "shirt",
:weight => 16.00,
:units => 1.00,
:image => (File.open(File.join(Rails.root, "app/assets/images/dev7logo.png")))
}
])
So, my advice is to improve the DB schema to make it more flexible and scalable ;)
Define the Size and Color models (2 new tables), make your actual Product model the BaseProduct model (just renaming the table) and finally create the Product model (new table) which will have 3 external keys (base_product_id, color_id and size_id) and of course the stock_qty field to define all possible configurations with the minimal repetition of information :)!
Just a little help, you're final classes schema should be like:
class Color < ActiveRecord::Base
end
class Size < ActiveRecord::Base
end
class BaseProduct < ActiveRecord::Base
# This will have almost all fields from your actual Product class
end
class Product < ActiveRecord::Base
# Ternary table between Color, Size and BaseProduct
end
I'm omitting all associations because I like the idea you succeed on your own, but if you need, please just ask :)
This will allows you to do BaseProduct queries like:
base_product.colors
base_product.sizes
product.base_product # retrieve the base_product from a product
and to keep trace of the quantities:
product.stock_qty
product.color
product.size # size and color are unique for a specific product
You can also create some helper method to make the creation process similar to the one you'd like to have (as shown in your question).
Well I understand the approaches you wanted to deal with. Pretty easy business logic if I understand correctly. So you wanted the following things If I get you correctly:
You have so many products
You want to add stock count record
You wanted to validate the product for selling (line items for cart) if the product available
You need to ensure if the product is already in customer's cart when you are deleting that.
So I assumed you already added the stock_qty columns.
Now you need to ensure if the product is available to be added in your cart.
So you need to write your validation in your line_item modem.
class LineItem < ActiveRecord::Base
# other business logics are here
belongs_to :product
before_validation :check_if_product_available
def check_if_product_available
# you will find your product from controller, model should be responsible to perform business
# decision on them.
if !self.try(:product).nil? && self.product.stock_qty < 1
errors.add(:product, 'This product is not available in the stock')
return false
end
end
end
This is the approach I believe is the valid way to do. And moreover, rather saving variants in same product model, I would suggest consider designing your model more efficiently with separate variant model or you can utilize the power of self association.
I hope this will help you. Let me know if I miss anything or miss interpret your problem.
Thanks
I have a model called Person that the user selects five personality Traits for. However, the order they pick them for matters (they are choosing most descriptive to least descriptive).
I know how to create a join table with a poison an do ordering that way. I'm using acts_as_list as well.
But I can't seem to find any help on, is how to create a way for the user of my app to set the order of the traits. That is I want to have say five select boxes on in the HTML and have them pick each one, and use something like jQuery UI Sortable to allow them to move them around if they like.
Here is a basic idea of my models (simplified for the purpose of just getting the concept).
class Person < ActiveRecord::Base
has_many :personalizations
has_many :traits, :through => :personalizations, :order => 'personalizations.position'
end
class Personalization < ActiveRecord::Base
belongs_to :person
belongs_to :trait
end
class Trait < ActiveRecord::Base
has_many :persons
has_many :persons, :through => :personalizations
end
I just have no idea how to get positioning working in my view/controller, so that when submitting the form it knows which trait goes where in the list.
After a lot of research I'll post my results up to help someone else encase they need to have list of records attached to a model via many-to-many through relationship with being able to sort the choices in the view.
Ryan Bates has a great screencast on doing sorting with existing records: http://railscasts.com/episodes/147-sortable-lists-revised
However in my case I needed to do sorting before my Person model existed.
I can easily add an association field using builder or simple_form_for makes this even easier. The result will be params contains the attribute trait_ids (since my Person has_many Traits) for each association field:
#view code (very basic example)
<%= simple_form_for #character do |f| %>
<%= (1..5).each do |i| %>
<%= f.association :traits %>
<% end %>
<% end %>
#yaml debug output
trait_ids:
- ''
- '1'
- ''
- '2'
- ''
- '3'
- ''
- '4'
- ''
- '5'
So then the question is will the order of the elements in the DOM be respected whenever the form is submitted. Specially if I implement jQuery UI draggable? I found this Will data order in post form be the same to it in web form? and I agree with the answer. As I suspected, too risky to assume the order will always be preserved. Could lead to a bug down the line even if it works in all browsers now.
Therefore after much looking I've concluded jQuery is a good solution. Along with a virtual attribute in rails to handle the custom output. After a lot of testing I gave up on using acts_as_list for what I am trying to do.
To explain this posted solution a bit. Essentially I cache changes to a virtual property. Then if that cache is set (changes were made) I verify they have selected five traits. For my purposes I am preserving the invalid/null choices so that if validation fails when they go back to the view the order will remain the same (e.g. if they skipped the middle select boxes).
Then an after_save call adds these changes to the database. Any error in after_save is still wrapped in a transaction so if any part were to error out no changes will be made. It was easiest therefore to just delete all the endowments and save the new ones (there might be a better choice here, not sure).
class Person < ActiveRecord::Base
attr_accessible :name, :ordered_traits
has_many :endowments
has_many :traits, :through => :endowments, :order => "endowments.position"
validate :verify_list_of_traits
after_save :save_endowments
def verify_list_of_traits
return true if #trait_cache.nil?
check_list = #trait_cache.compact
if check_list.nil? or check_list.size != 5
errors.add(:ordered_traits, 'must select five traits')
elsif check_list.uniq{|trait| trait.id}.size != 5
errors.add(:ordered_traits, 'traits must be unique')
end
end
def ordered_traits
list = #trait_cache unless #trait_cache.nil?
list ||= self.traits
#preserve the nil (invalid) values with '-1' placeholders
list.map {|trait| trait.nil?? '-1' : trait.id }.join(",")
end
def ordered_traits=(val)
#trait_cache = ids.split(',').map { |id| Trait.find_by_id(id) }
end
def save_endowments
return if #trait_cache.nil?
self.endowments.each { |t| t.destroy }
i = 1
for new_trait in #trait_cache
self.endowments.create!(:trait => new_trait, :position => i)
i += 1
end
end
Then with simple form I add a hidden field
<%= f.hidden :ordered_traits %>
I use jQuery to move the error and hint spans to the correct location inside
the div of five select boxes I build. Then I had a submit event handler on the form and convert the selection from the five text boxes in the order they are in the DOM to an array of comma separated numbers and set the value on the hidden field.
For completeness here is the other classes:
class Trait < ActiveRecord::Base
attr_accessible :title
has_many :endowments
has_many :people, :through => :endowments
end
class Endowment < ActiveRecord::Base
attr_accessible :person, :trait, :position
belongs_to :person
belongs_to :trait
end