Rails, custom finders - ruby-on-rails

So I want to be able to get an object using find_by_id_or_name, I feel like I saw another question like this but am trouble finding any resources on making my own finder.

You can do this by adding a class method to your model e.g.
class Model < ActiveRecord::Base
def self.find_by_id_or_name(id_or_name)
find :first, :conditions => ['id = ? or name = ?', id_or_name, id_or_name]
end
def self.find_all_by_id_or_name(id_or_name)
find :all, :conditions => ['id = ? or name = ?', id_or_name, id_or_name]
end
end
You will then by able to do
Model.find_by_id_or_name(id_or_name)
You can customise the method slightly depending on your requirements e.g. if you want to try by id first and then by name you could use Model.exists? first to see if there is matching record before doing a find_by_name. You could also look if id_or_name consisted of characters 0-9 and assume that was an id and only search by name if it contained other characters.

Related

Chaining named_scopes

I have a question regarding working with named_scopes:
lets say i have the following model (i know some of the named_scope i've provided can be achieved directly through active record but they are just for the example):
def person
named_scope :older_then, lambda {|min_age| {:conditions => ["age > ?",min_age]} }
named_scope :by_first_name, lambda {|name| {:conditions => {:first_name => name}}}
def self.some_function(age_param, name_param)
chain = Person
chain = chain.older_then(age_param) unless age_param.blank?
chain = chain.by_first_name(name_param) unless name_param.blank?
chain.all
end
end
now lets say i want to call:
people = Person.some_function(20, "john")
while building the chain of named_scopes Rails will make 2 calls to the db:
select * from persons where age>20
select * from persons where age>20 and name='john'
obviously all I wanted was the result of the second query and didnt intended that the first query will be executed while building the chain of named_scopes. any ideas what i'm doing wrong here / whats the correct way to combine multiple named_scope by conditions?
BTW, i'm using Ruby 1.8.7 its old, i know... :(
Change this:
def self.some_function(age_param, name_param)
chain = Person
to:
def self.some_function(age_param, name_param)
chain = self
This will keep the original chain intact instead of starting a new one.

Ruby on Rails search 2 models

Right... I've spent 3 days trying to do this myself to no a vale.
I have 2 models called Film and Screenings. Screenings belongs_to Film, Film has_many Screenings.
The Film has certain attributes(:title, :date_of_release, :description, :genre).
The Screening has the attributes(:start_time, :date_being_screened, :film_id(foreign key of Film)).
What I am trying to do is create a Search against both of these models.
I want to do something like this...
#films = Film.advanced_search(params[:genre], params[:title], params[:start_time], params[:date_showing])
And then in the Film model...
def self.advanced_search(genre, title, start_time, date)
search_string = "%" + title + "%"
self.find(:all, :conditions => ["title LIKE ? OR genre = ? OR start_time LIKE ? OR date_showing = ?", title, genre, start_time, date], order: 'title')
end
end
I don't think this could ever work quite like this, but I'm hoping my explanation is detailed enough for anyone to understand what im TRYING to do?? :-/
Thanks for any help guys
I would extract the search capability into a separate (non-ActiveRecord) class, such as AdvancedSearch as it doesn't neatly fit into either the Film or Screening class.
Rather than writing a complex SQL query, you could just search the films, then the screenings, and combine the results, for example:
class AdvancedSearch
def self.search
film_matches = Film.advanced_search(...) # return an Array of Film objects
screening_matches = Screening.advanced_search(...) # return an Array of Screening objects
# combine the results
results = film_matches + screening_matches.map(&:film)
results.uniq # may be necessary to remove duplicates
end
end
Update
Let's say your advanced search form has two fields - Genre and Location. So when you submit the form, the params sent are:
{ :genre => 'Comedy', :location => 'London' }
Your controller would then something like:
def advanced_search(params)
film_matches = Film.advanced_search(:genre => params[:genre])
screening_matches = Screening.advanced_search(:location => params[:location])
# remaining code as above
end
i.e. you're splitting the params, sending each to a different model to run a search, and then combining the results.
This is essentially an OR match - it would return films that match the genre or are being screened at that specified venue. (If you wanted and AND match you would need to the work out the array intersection).
I wanted to write something but this cast says all http://railscasts.com/episodes/111-advanced-search-form
Almost the same case as yours.

Is it possible to delete_all with inner join conditions?

I need to delete a lot of records at once and I need to do so based on a condition in another model that is related by a "belongs_to" relationship. I know I can loop through each checking for the condition, but this takes forever with my large record set because for each "belongs_to" it makes a separate query.
Here is an example. I have a "Product" model that "belongs_to" an "Artist" and lets say that artist has a property "is_disabled".
If I want to delete all products that belong to disabled artists, I would like to be able to do something like:
Product.delete_all(:joins => :artist, :conditions => ["artists.is_disabled = ?", true])
Is this possible? I have done this directly in SQL before, but not sure if it is possible to do through rails.
The problem is that delete_all discards all the join information (and rightly so). What you want to do is capture that as an inner select.
If you're using Rails 3 you can create a scope that will give you what you want:
class Product < ActiveRecord::Base
scope :with_disabled_artist, lambda {
where("product_id IN (#{select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})")
}
end
You query call then becomes
Product.with_disabled_artist.delete_all
You can also use the same query inline but that's not very elegant (or self-documenting):
Product.where("product_id IN (#{Product.select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})").delete_all
In Rails 4 (I tested on 4.2) you can almost do how OP originally wanted
Application.joins(:vacancy).where(vacancies: {status: 'draft'}).delete_all
will give
DELETE FROM `applications` WHERE `applications`.`id` IN (SELECT id FROM (SELECT `applications`.`id` FROM `applications` INNER JOIN `vacancies` ON `vacancies`.`id` = `applications`.`vacancy_id` WHERE `vacancies`.`status` = 'draft') __active_record_temp)
If you are using Rails 2 you can't do the above. An alternative is to use a joins clause in a find method and call delete on each item.
TellerLocationWidget.find(:all, :joins => [:widget, :teller_location],
:conditions => {:widgets => {:alt_id => params['alt_id']},
:retailer_locations => {:id => #teller_location.id}}).each do |loc|
loc.delete
end

How to search in this activerecord example?

Two models:
Invoice
:invoice_num string
:date datetime
.
.
:disclaimer_num integer (foreign key)
Disclaimer
:disclaimer_num integer
:version integer
:body text
For each disclaimer there are multiple versions and will be kept in database. This is how I write the search (simplified):
scope = Invoice.scoped({ :joins => [:disclaimer] })
scope = scope.scoped :conditions => ["Invoice.invoice_num = ?", "#{params[:num]}"]
scope = scope.scoped :conditions => ["Disclaimer.body LIKE ?", "%#{params[:text]}%"]
However, the above search will search again all versions of the disclaimer. How can I limit the search to only the last disclaimer (i.e. the version integer is the maximum).
Please note:
Invoice does not keep the version number. New disclaimers will be added to disclaimer table and keep old versions.
If you want only the invoices with the latest version from disclaimer, put a condition on the disclaimer_num. And I also suggest creating a helper method in Disclaimer to make the code cleaner in your scope.
class Disclaimer < ActiveRecord::Base
def latest
find(:first, :order => "version DESC")
end
end
scope = scope.scoped :conditions => { :disclaimer_num => Disclaimer.latest }
And I really hope you removed the sql injection prevention code from your scope for brevity.
Hmm... I might just be stored procedure happy, but I think at this point you'd benefit greatly from a stored procedure (or even a view) that did something like this:
CREATE PROCEDURE GetRecentDisclaimer
#BodyFragmentBeingSearched varchar(200)
AS
SELECT MAX(version), disclaimer_num, body
FROM Disclaimer
WHERE
body LIKE #BodyFragmentBeingSearched
GROUP BY disclaimer_num, body
From there, someone's written a blog about how you'd call a stored procedure in Rails and populate ActiveRecord objects, check it out here:
http://nasir.wordpress.com/2007/12/04/stored-procedures-and-rails-part-2/
Add these two conditions (can be done in scope):
"ORDER BY disclaimer.disclaimer_num DESC"
"LIMIT 0, 1"

How to filter results by multiple fields?

I am working on a survey application in ruby on rails and on the results page I want to let users filter the answers by a bunch of demographic questions I asked at the start of the survey.
For example I asked users what their gender and career was. So I was thinking of having dropdowns for gender and career. Both dropdowns would default to all but if a user selected female and marketer then my results page would so only answers from female marketers.
I think the right way of doing this is to use named_scopes where I have a named_scope for every one of my demographic questions, in this example gender and career, which would take in a sanitized value from the dropdown to use at the conditional but i'm unsure on how to dynamically create the named_scope chain since I have like 5 demographic questions and presumably some of them are going to be set to all.
You can chain named scopes together:
def index
#results = Results.scoped
#results = #results.gender(params[:gender]) unless params[:gender].blank?
#results = #results.career(params[:career]) unless params[:career].blank?
end
I prefer however to use the has_scope gem:
has_scope :gender
has_scope :career
def index
#results = apply_scopes(Results).all
end
If you use has_scope with inherited_resources, you don't even need to define the index action.
named_scope :gender,lambda { |*args|
unless args.first.blank?
{ :conditions => [ "gender = ?", args.first] }
end
}
If you write named scopes in this way, you can have all them chained, and if one of your params will be blank wont breaks.
Result.gender("Male") will return male results.
Result.gender("") will return male and female too.
And you can chain all of your methods like this. Finally as a filtering you can have like:
Result.age(16).gender("male").career("beginer")
Result.age(nil).gender("").career("advanced") - will return results with advanced career, etc.
Try some like this:
VistaFact.where( if active then {:field => :vista2} else {} end)
Or like this:
VistaFact.where(!data.blank? ? {:field=>data.strip} : {}).where(some? ? {:field2 => :data2} : {}).where ...
That work for me very nice!

Resources