How to search in this activerecord example? - ruby-on-rails

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"

Related

Datamapper: Sorting results through association

I'm working on a Rails 3.2 app that uses Datamapper as its ORM. I'm looking for a way to sort a result set by an attribute of the associated model. Specifically I have the following models:
class Vehicle
include DataMapper::Resource
belongs_to :user
end
class User
include DataMapper::Resource
has n, :vehicles
end
Now I want to be able to query the vehicles and sort them by the name of the driver. I tried the following but neither seems to work with Datamapper:
> Vehicle.all( :order => 'users.name' )
ArgumentError: +options[:order]+ entry "users.name" does not map to a property in Vehicle
> Vehicle.all( :order => { :users => 'name' } )
ArgumentError: +options[:order]+ entry [:users, "name"] of an unsupported object Array
Right now I'm using Ruby to sort the result set post-query but obviously that's not helping performance any, also it stops me from further chaining on other scopes.
I spent some more time digging around and finally turned up an old blog which has a solution to this problem. It involves manually building the ordering query in DataMapper.
From: http://rhnh.net/2010/12/01/ordering-by-a-field-in-a-join-model-with-datamapper
def self.ordered_by_vehicle_name direction = :asc
order = DataMapper::Query::Direction.new(vehicle.name, direction)
query = all.query
query.instance_variable_set("#order", [order])
query.instance_variable_set("#links", [relationships['vehicle'].inverse])
all(query)
end
This will let you order by association and still chain on other scopes, e.g.:
User.ordered_by_vehicle_name(:desc).all( :name => 'foo' )
It's a bit hacky but it does what I wanted it to do at least ;)
Note: I'm not familiar with DataMapper and my answer might not be within the standards and recommendations of using DataMapper, but it should hopefully give you the result you're looking for.
I've been looking through various Google searches and the DataMapper documentation and I haven't found a way to "order by assocation attribute". The only solution I have thought of is "raw" SQL.
The query would look like this.
SELECT vehicles.* FROM vehicles
LEFT JOIN users ON vehicles.user_id = users.id
ORDER BY users.name
Unfortunately, from my understanding, when you directly query the database you won't get the Vehicle object, but the data from the database.
From the documentation: http://datamapper.org/docs/find.html. It's near the bottom titled "Talking directly to your data-store"
Note that this will not return Zoo objects, rather the raw data straight from the database
Vehicle.joins(:user).order('users.name').all
or in Rails 2.3,
Vehicle.all(:joins => "inner join users on vehicles.user_id = user.id", :order => 'users.name')

Rails active record query

How would i do a query like this.
i have
#model = Model.near([latitude, longitude], 6.8)
Now i want to filter another model, which is associated with the one above.
(help me with getting the right way to do this)
model2 = Model2.where("model_id == :one_of_the_models_filtered_above", {:one_of_the_models_filtered_above => only_from_the_models_filtered_above})
the model.rb would be like this
has_many :model2s
the model2.rb
belongs_to :model
Right now it is like this (after #model = Model.near([latitude, longitude], 6.8)
model2s =[]
models.each do |model|
model.model2s.each do |model2|
model2.push(model2)
end
end
I want to accomplish the same thing, but with an active record query instead
i think i found something, why does this fail
Model2.where("model.distance_from([:latitude,:longitude]) < :dist", {:latitude => latitude, :longitude => longitude, :dist => 6.8})
this query throws this error
SQLite3::SQLException: near "(": syntax error: SELECT "tags".* FROM "tags" WHERE (model.distance_from([43.45101666666667,-80.49773333333333]) < 6.8)
, why
use includes. It will eager-load associated models (only two SQL queries instead of N+1).
#models = Model.near( [latitude, longitude], 6.8 ).includes( :model2s )
so when you will do #models.first.model2s, associated model2s will already be loaded (see RoR guides for more info).
If you want to get an array of all model2s belonging to your collection of models, you can do :
#models.collect( &:model2s )
# add .flatten at the end of the chain if you want a one level deep array
# add .uniq at the end of the chain if you don't want duplicates
collect (also called map) will gather in an array the result of any block passed to each of the caller's elements (this does exactly the same as your code, see Enumerable's doc for more info). The & before the symbol converts it into a Proc passed to each element of the collection, so this is the same as writing
#models.collect {|model| model.model2s }
one more thing : #mu is right, seems SQLite does not know about your distance_from stored procedure. As i suspect this is a GIS related question, you may ask about this particular issue on gis.stackexchange.com

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

Rails, custom finders

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.

"Section" has_many versioned "Articles" -- How can I get the freshest subset?

I have a Model called Section which has many articles (Article). These articles are versioned (a column named version stores their version no.) and I want the freshest to be retrieved.
The SQL query which would retrieve all articles from section_id 2 is:
SELECT * FROM `articles`
WHERE `section_id`=2
AND `version` IN
(
SELECT MAX(version) FROM `articles`
WHERE `section_id`=2
)
I've been trying to make, with no luck, a named scope at the Article Model class which look this way:
named_scope :last_version,
:conditions => ["version IN
(SELECT MAX(version) FROM ?
WHERE section_id = ?)", table_name, section.id]
A named scope for fetching whichever version I need is working greatly as follows:
named_scope :version, lambda {|v| { :conditions => ["version = ?", v] }}
I wouldn't like to end using find_by_sql as I'm trying to keep all as high-level as I can. Any suggestion or insight will be welcome. Thanks in advance!
I would take a look at some plugins for versioning like acts as versioned or version fu or something else.
If you really want to get it working the way you have it now, I would add a boolean column that marks if it is the most current version. It would be easy to run through and add that for each column and get the data current. You could easily keep it up-to-date with saving callbacks.
Then you can add a named scope for latest on the Articles that checks the boolean
named_scope :latest, :conditions => ["latest = ?", true]
So for a section you can do:
Section.find(params[:id]).articles.latest
Update:
Since you can't add anything to the db schema, I looked back at your attempt at the named scope.
named_scope :last_version, lambda {|section| { :conditions => ["version IN
(SELECT MAX(version) FROM Articles
WHERE section_id = ?)", section.id] } }
You should be able to then do
section = Section.find(id)
section.articles.last_version(section)
It isn't the cleanest with that need to throw the section to the lambda, but I don't think there is another way since you don't have much available to you until later in the object loading, which is why I think your version was failing.

Resources