In my Rails 4 app Project (model) has_many Videos (model). I have a named scope in the videos model:
scope :live, where( is_deleted: 0, sent_to_api: 1 )
In one of my project views, I do this (project is an instance of Project):
project.videos.live.size
What I expect to get is the number of projects in that specific project but instead I get the number of videos in any project. It's as if .live is not returning a subset from .videos but rather replacing it.
I see it explained here that chaining named scopes with one another should be combined with logical AND but when applied to an "association method" [<--not sure the proper terminology for .videos in this context] that doesn't seem to be happening.
What's the right way to do this?
I believe it should read like this in Rails 4:
scope :live, -> { where(is_deleted: 0, sent_to_api: 1) }
The rails 4 docs and all examples in it show you passing in a callable object to the scope to ensure it gets called each time. If it doesn't work like this try implementing it as a class method and see how that works out for you.
http://api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/ClassMethods.html
I would just go for class methods and leave scopes behind. The syntax is much simpler because it's just like any other class method, including passing parameters to it.
Try:
def self.live
where( is_deleted: 0, sent_to_api: 1 )
end
And then:
project.videos.live.size
and see if it helps.
For more info, read here.
Related
I have 4 clases, with an STI on Instance.
Workspace, Project, Task, Instance, (type1 < Instance) and (type2 < Instance).
With proper associations. (Workspace has_many projects, has_many task through projects, so on)
And I have this nested create (worked before implementing STI):
if (%w(type1 type2).include?(params[:type]))
sti_class = params[:type].classify.constantize
workspaces.find_by_name(name: w_name).
projects.where( name: p_name).first_or_create!.
tasks.where(name: t_name).first_or_create!.
sti_class.create()
now, that doesn't work, I can't figure out a way.
However, the following works, but I want to keep the nested create.
task= workspaces.find_by_name(name: w_name).
projects.where( name: p_name).first_or_create!.
tasks.where(name: t_name).first_or_create!
sti_class.create(task_id: task.id)
How can I keep the nested create?
The problem I could immediately deduce is that the sti_class method isn't defined in your Task model, as you're adding it to the method chain.
Don't really think you're following the best practice here, but to immediately resolve the issue, you should probably do something like:
if (%w(type1 type2).include?(params[:type]))
# depending on the association between the type(s) and the tasks,
# you'd need to either singularize or pluralize here, I'd assume
# task has many types, therefore pluralize
sti_class = params[:type].pluralize
# if you're already calling `find_by_name`, you don't need to pass
# the name option here anymore, but the name argument
workspaces.find_by_name(w_name).
projects.where(name: p_name).first_or_create!.
tasks.where(name: t_name).first_or_create!.
send(sti_class).create
I added a variable in config/application.rb:
config.available_account_types = %w(SystemAccount CashAccount DemandAccount LiabilityAccount CreditCardAccount)
And generated some scopes in model account.rb:
for t in Mypurse::Application.config.available_account_types
scope t.underscore.pluralize.to_sym, -> {where(type: t)}
end
But when I try all of them, Account.system_accounts, Account.cash_accounts, etc, I got this sql for every account type:
where type = 'CreditCardAccount'
That is, all of the generated scope are pointed to the {where(type: 'CreditCardACcount')}
I don't know why.
here is the source file:
https://github.com/chylli/mypurse/blob/add_cash_demand_liability_credit_card/config/application.rb
https://github.com/chylli/mypurse/blob/add_cash_demand_liability_credit_card/app/models/account.rb
I think this is caused because a scope is given a Proc which is only executed when called, and so t will always be the last element of the loop.
A solution is to define methods instead of scopes (which work exactly the same) :
MyPurs::Application.config.available_account_types.each do |account_type|
define_singleton_method(account_type.underscore.pluralize.to_sym) do
where(type: "#{account_type}")
end
end
But since this does not declare a proc, this should work as expected.
Also the for .. in is rarely used in ruby, I personally prefer to use the more idiomatic .each (but of course you are free to use whatever you want, programmer happiness is key in ruby :) :)
Now as an aside, while meta-programming is really cool, you should really ask yourself if just listing the scopes is not way more readable. I understand: meta-programming is more DRY, but personally, in most cases where I did this, I reverted to the explicit definitions because of readability.
I am not sure why you have defined 'config.available_account_types', as this is business logic. this should belong to Account modal. so I would do something like this
class Account < ActiveRecord::Base
ACCOUNT_TYPES = %w(SystemAccount CashAccount DemandAccount LiabilityAccount CreditCardAccount)
ACCOUNT_TYPES.each do |acccount_type|
define_singleton_method(account_type.underscore.pluralize.to_sym) do
where(type: "#{account_type}")
end
end
end
The code snippet has three methods: lambda, scope and class method.
All of them returns the same results.
Questions:
Is there any best practice in Ruby/Rails when it is preferred to use one over the other ?
In what cases would you use lambda, scope or class method ( best practices ).
class Cars < ActiveRecord::Base
attr_accessible :manufacturer, :price, :used
#one
scope :used_and_cheap_lambda, lambda { where('used = ?', true ).where('price >= ?',30000) }
#two
scope :used_and_cheap_scope, where('used = ?', true ).where('price >= ?',30000)
#three
def self.used_and_cheap_class
where('used = ?', true ).where('price >= ?',30000)
end
end
Cars.used_and_cheap_lambda.count
=> #24
Cars.used_and_cheap_class.count
=> #24
Cars.used_and_cheap_scope.count
=> #24
It's best to avoid using option 2. That code gets run immediately when your Rails app loads which is bad since it will always return the same value for any Time argument you use in it. That's because it isn't reevaluated every time it's called.
Option 1, as pointed out by musicnerd47, are lazy loaded and it is advisable that you pass lambdas to scopes in Rails 4 rather than doing option 2 since they are reevaluated every time called so they will return updated values.
So the only options would be 1 and 3. This is usually a matter of style that your team adheres to. In our company, we use option 1 when the code we pass to it is going to be an ActiveRecord query and we want it to output a query that can be chained. This is to ensure that an ActiveRecord::Relation object is returned every time we do our queries for multiple records. That would mean that they are always going to be chainable with other ActiveRecord::Relation methods and our other defined scopes.
We use option 3 if it's for behavior that doesn't need to be chained with other scopes.
Here's a good read on the matter of scopes and class_methods where he goes into detail and provides examples on the difference between scopes and class methods.
http://blog.plataformatec.com.br/2013/02/active-record-scopes-vs-class-methods/
Starting in Rails 4 you have to use a lambda - in general it is a better practice because it is lazily loaded and prevents a lot of traps, especially when dealing with dates and times.
I think for simple scopes that deal with a single where call or something, using scope is okay. When it is more complex, then moving to a class method is better (for example, when you need to be calling other methods or setting local variables before you return the scope).
I would use the lambda. The function you're describing is sufficiently simple. Using the lambda initializes lazily as well. I direct you here to the rails style guide.
Unfortunately there is no golden rule. Scopes are designed for this exact application. When the application of logic comfortably fits into a scope, I think that's the best bet. When things start to get too complex, it's usually best to move the logic into a class method.
I feel like I'm missing something rather important in both circumstances, but I can't seem to figure either out:
1) I have a model named TestCase -
class TestCase < ActiveRecord::Base
belongs_to :test_suite
scope :queued, lambda { where("test_cases.suite_id IS NOT NULL") }
scope :assigned_to, lambda { |sid| where(:suite_id => sid) }
end
The controller can interact with it perfectly fine. When trying to display information from it in either the view or via the view helper such as TestCase.all, I get a NoMethodError (undefined method 'all') If I call it with ::TestCase.all, that works. I had a theory that it has something to do with the fact that it's associated to another model (belongs_to ...), I just can't find anything to confirm that or tell me why that happens.
2) On another project I have yet another model named Artwork. Again, it has associations (belongs_to). In this case, I can access it just fine in the view, and all the methods within it work fine for the controller except if I try to do dynamic method calls. In this case I have a simple toggle for -
#artwork = Artwork.find(params[:id])
value = params[:value].to_sym
#artwork.update_attributes(value => !#artwork.method(value).call)
That gives me a NoMethodError. However, if I add - if #artwork.respond_to?(value) - then it works as expected. Again, I can't figure out why.
Both items I get working using the mentioned methods, but again, I feel like I'm really missing something important here.
Re: problem 1 -- Don't call your model "TestCase". That conflicts with the Rails TestCase class.
Re: problem 2 -- That's an odd way of doing things. You might get it working by using
#artwork.send(value)
but keep in mind that a rogue user could pass in any method name through the form and wreak havoc.
I have a basic has_many :through relationship that is bi-directional:
calendars have many calendar_calendar_events
calendars have many events through calendar_calendar_events
events have many calendar_calendar_events
events have many calendars through calendar_calendar_events
I'm wanting to assign calendars to an event with the basic calendar_ids= function that has_many :through sets up, however, I want to override this function to add some extra magic. I've had a look through the rails source and can't find the code for this function. I'm wondering if someone could point me to it. I'll then override it for this class to add the stuff that I want :)
You can find the source code in the file lib/active_record/associations.rb at line 1295
def collection_accessor_methods(reflection, association_proxy_class, writer = true)
collection_reader_method(reflection, association_proxy_class)
if writer
define_method("#{reflection.name}=") do |new_value|
# Loads proxy class instance (defined in collection_reader_method) if not already loaded
association = send(reflection.name)
association.replace(new_value)
association
end
define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
ids = (new_value || []).reject { |nid| nid.blank? }
send("#{reflection.name}=", reflection.class_name.constantize.find(ids))
end
end
end
You should definitely avoid to overwrite such this method to add magic stuff.
Rails is already "too much magic" sometimes. I would suggest to create a virtual attribute with all your custom logic for several reasons:
some other rails methods might rely on the default implementation
you rely on a specific API that might going to change in future ActiveRecord versions
After a bit of a hunt I found it:
http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/collection_accessor_methods
It didn't look like what I thought it would look like, so that's why I probably missed it. I ended up overriding the calendars= method instead of the calendar_ids= method and everything works well.
In response to the answer above, I used alias_method_chain to override the default setter and add my feature. Works quite well, though I'm not sure why I have to send the method setter instead of just using it normally. It didn't seem to work though so this will do :)
def calendars_with_primary_calendar=(new_calendars)
new_calendars << calendar unless new_record?
send('calendars_without_primary_calendar=', new_calendars) # Not sure why we have to call it this way
end
alias_method_chain :calendars=, :primary_calendar