Complex Search with Multiple Models and Geokit using Rails - sql

I am attempting to make a search very complicated (of course, to make it easier for users)
I've got an app with 3 models: Campaigns, Businesses and Locations
like so:
\\ campaign.rb
belongs_to :business
has_many :locations, :through => :business
acts_as_mappable
\\ business.rb
has_many :campaigns
has_many :locations
\\ location.rb
belongs_to :business
has_many :campaigns, :through => :business
acts_as_mappable
The way this is set up, there are some businesses that have multiple locations. For those that don't, the geokit info is coded into the campaign database entry. For those that do have multiple locations the geokit info is coded into the location database entry.
I am trying to do a search for campaigns that will return the results within a certain distance. This is simple enough when dealing with businesses that have a single address.
Campaign.find(:all,
:conditions => [blahblahblah],
:origin => address,
:within => distance
)
However, I want to also include campaigns that belong to businesses that have multiple locations. I want the search to return a result for that campaign, if the business has multiple locations, and if any of those locations fall within the bounds. I was thinking something like:
Campaign.find(:all,
:include => [:business, :locations]
:conditions => [blahblahblah],
:origin => address,
:within => distance
)
But this doesn't return any results for campaigns that belong to businesses that have multiple locations. I'm a noob when it comes to SQL so I'm not exactly sure how to have rails search in one model (Campaign), and search across another model (the Business model) to grab results from the Location model. The fact that geokit is involved makes it even more complex.
I tried: acts_as_mappable :through => :locations in the campaign.rb but it just threw an sql error
I messed around with a polymorphic model "addressable" but I found I would pretty much have to start from the ground up on the controllers of the other models.
I've also thought about named_scopes but I believe that geokit does not support them.
Any suggestions?

If you think your model is overly complicated its a good sign that it needs a redesign.
Is it possible to have a campaign without a business? if not, then it doesnt make sense to make campaign act_as_mappable (Im assuming your campaign DB has lat and lng columns) if business already has mappable locations.
If campaign is associated with a business that has multiple locations, whats considered location of the campaign? What do you store in campaigns.lat and lng attributes?
If youre sticking with your datamodel, I recommend to break up your search into multiple commands:
def find_campaigns_near(origin,distance)
locations = Location.find(:all,
:origin => address,
:within => distance
);
# use Hash for uniqueness based on id
campaigns = Hash.new
locations.each do |location|
location.campaigns.each do |campaign|
campaigns[campaign.id] = campaign if campaigns[campaign.id].blank?
end
end
campaigns
end

Related

Sphinx and friend search

I have a high-level question about Sphinx on Ruby on Rails:
Say I want to search are the articles written by my friends.
The model structure would be:
User has_many users :through relationships
Article belongs_to User
My question is: what sort of syntax would I use on Sphinx so the user searches for articles and gets only the articles written by his friends? I am having trouble finding this online and I'd like to have a handle on how this would work before I implement my solution.
NOTE: I suspect one solution is to have an array of friend IDs and then use a :condition :with => {:id => array_of_friendIDs}. But maybe there is a more effective way of doing it?
You're basically correct - you're going to need to assemble an array of friend ids and pass it to the search, using the :with option. How you fetch that list isn't particularly related to Sphinx, though.
friend_ids = current_user.users.pluck(:id)
#articles = Article.search(params['search_term'], :with => {:user_id => friend_ids})
Using .pluck will save you a good bit of time that would otherwise be devoted to instantiating a bunch of User objects - you only need their ids. Make sure that you've set user_id as an attribute for Sphinx (using has user_id in the define_index block).

BEGINNER: Correct seeds.rb in rails 3

I've just created two models and one "join table". Person, Adress (create_adresses_personss)
class Person < ActiveRecord::Base
has_and_belongs_to_many :streets
end
class Street < ActiveRecord::Base
has_and_belongs_to_many :persons
end
Now I want to add some data to these models in the db/seeds.rb file. The tutorial I follow just adds the objects:
person = Person.create :name => 'Dexter'
street.create[{:streetname => 'street1'},
{:streetname => 'street2'},
{:streetname => 'julianave'},
{:streetname => 'street3'}]
Question 1: Why is persons' data added differently than streets'? Is it just the tutorial that wants to show that there are many ways of adding data in the seeds.rb?
Question 2: The tutorial doesn't make the connections/joins in the seeds.rb. It does that in the rails console;
>>p1 = Person.find(1)
>>s1 = Street.find(1)
>>p1.streets << s1
Can't theese connections be made in the seeds.rb file?
Question 3: Would it be better to do this join with a "rich many_to_many-assocciation"?
Thanks for your time and patience with a beginner ;)
1) The first method is creating one object. The second method is creating multiple objects. However, for the second method you would need to do Street.create, not street.create.
2) Yes, you can do that in the seed file the same way.
3) The "Rich many-to-many" you're talking about is an association with a Join Model, I guess you're talking about. This is opposed to just a join table, which is what has_and_belongs_to_many does. To use a join model, you'll want to look up has_many :through. It's generally considered better to always use a proper join model, however I still use HABTM when I just need a quick, simple association. has_many :through allows for more options and more flexibility, but it is a little more complicated to setup (not that much, though). It's your decision.
One way that I like to create seed data for many-to-many associations is setting up one of the models, the adding a tap block that sets up the other models through the association.
Person.create!(:name => "Fubar").tap do |person|
3.times do |n|
person.streets.create!(:streetname => "street #{n}")
end
# OR
person.streets.create!([
{:streetname => "street 1"},
{:streetname => "street 2"},
... and so on
])
end
All tap is doing is executing the block with the object as it's only parameter. I find it convenient for seeds.
One other tip I would toss out there would be to have your model attribute names spaced on the words with underscores.
:street_name instead of :streetname
The difference is more profound when you start wanting to use some of the ActiveSupport helers that take model attributes and turn them into text strings for use in the UI.
e
:streetname.to_s.titleize # "Streetname"
:street_name.to_s.titleize # "Street Name"
And one last nitpick, you might want your join table to be addresses_people not addresses_persons since the rais inflector is going to pluralize person as people. The same would go for your controller on the Person model, PeopleController instead of PersonsController. Though maybe it will work with persons as well.
:person.to_s.pluralize # "people"
:people.to_s.singularize # "person"
:persons.to_s.singularize # "person"

Should I drop a polymorphic association?

My code is still just in development, not production, and I'm hitting a wall with generating data that I want for some views.
Without burying you guys in details, I basically want to navigate through multiple model associations to get some information at each level. The one association giving me problems is a polymorphic belongs_to. Here are the most relevant associations
Model Post
belongs_to :subtopic
has_many :flags, :as => :flaggable
Model Subtopic
has_many :flags, :as => :flaggable
Model Flag
belongs_to :flaggable, :polymorphic => true
I'd like to display multiple flags in a Flags#index view. There's information from other models that I want to display, as well, but I'm leaving out the specifics here to keep this simpler.
In my Flags_controller#index action, I'm currently using #flags = paginate_by_sql to pull everything I want from the database. I can successfully get the data, but I can't get the associated model objects eager-loaded (though the data I want is all in memory). I'm looking at a few options now:
rewrite my views to work on the SQL data in the #flags object. This should work and will prevent the 5-6 association-model-SQL queries per row on the index page, but will look very hackish. I'd like to avoid this if possible
simplify my views and create additional pages for the more detailed information, to be loaded only when viewing one individual flag
change the model hierarchy/definitions away from polymorphic associations to inheritance. Effectively make a module or class FlaggableObject that would be the parent of both Subtopic and Post.
I'm leaning towards the third option, but I'm not certain that I'll be able to cleanly pull all the information I want using Rails' ActiveRecord helpers only.
I would like insight on whether this would work and, more importantly, if you you have a better solution
EDIT: Some nit-picky include behavior I've encountered
#flags = Flag.find(:all,:conditions=> "flaggable_type = 'Post'", :include => [{:flaggable=>[:user,{:subtopic=>:category}]},:user]).paginate(:page => 1)
=> (valid response)
#flags = Flag.find(:all,:conditions=> ["flaggable_type = 'Post' AND
post.subtopic.category_id IN ?", [2,3,4,5]], :include => [{:flaggable=>
[:user, {:subtopic=>:category}]},:user]).paginate(:page => 1)
=> ActiveRecord::EagerLoadPolymorphicError: Can not eagerly load the polymorphic association :flaggable
Don't drop the polymorphic association. Use includes(:association_name) to eager-load the associated objects. paginate_by_sql won't work, but paginate will.
#flags = Flag.includes(:flaggable).paginate(:page => 1)
It will do exactly what you want, using one query from each table.
See A Guide to Active Record Associations. You may see older examples using the :include option, but the includes method is the new interface in Rails 3.0 and 3.1.
Update from original poster:
If you're getting this error: Can not eagerly load the polymorphic association :flaggable, try something like the following:
Flag.where("flaggable_type = 'Post'").includes([{:flaggable=>[:user, {:subtopic=>:category}]}, :user]).paginate(:page => 1)
See comments for more details.
Issues: Count over a polymorphic association.
#flags = Flag.find(:all,:conditions => ["flaggable_type = 'Post' AND post.subtopic.category_id IN ?",
[2,3,4,5]], :include => [{:flaggable => [:user, {:subtopic=>:category}]},:user])
.paginate(:page => 1)
Try like the following:
#flags = Flag.find(:all,:conditions => ["flaggable_type = 'Post' AND post.subtopic.category_id IN ?",
[2,3,4,5]], :include => [{:flaggable => [:user, {:subtopic=>:category}]},:user])
.paginate(:page => 1, :total_entries => Flag.count(:conditions =>
["flaggable_type = 'Post' AND post.subtopic.category_id IN ?", [2,3,4,5]]))

Ruby-on-Rails: How to pull out most recent entries from a limited subset of a database table

Imagine something like a model User who has many Friends, each of who has many Comments, where I'm trying to display to the user the latest 100 comments by his friends.
Is it possible to draw out the latest 100 in a single SQL query, or am I going to have to use Ruby application logic to parse a bigger list or make multiple queries?
I see two ways of going about this:
starting at User.find and use some complex combination of :join and :limit. This method seems promising, but unfortunately, would return me users and not comments, and once I get those back, I'd have lots of models taking up memory (for each Friend and the User), lots of unnecessary fields being transferred (everything for the User, and everything about the name row for the Friends), and I'd still have to step through somehow to collect and sort all the comments in application logic.
starting at the Comments and using some sort of find_by_sql, but I just can't seem to figure out what I'd need to put in. I don't know how you could have the necessary information to pass in with this to limit it to only looking at comments made by friends.
Edit: I'm having some difficult getting EmFi's solution to work, and would appreciate any insight anyone can provide.
Friends are a cyclic association through a join table.
has_many :friendships
has_many :friends,
:through => :friendships,
:conditions => "status = #{Friendship::FULL}"
This is the error I'm getting in relevant part:
ERROR: column users.user_id does not exist
: SELECT "comments".* FROM "comments" INNER JOIN "users" ON "comments".user_id = "users".id WHERE (("users".user_id = 1) AND ((status = 2)))
When I just enter user.friends, and it works, this is the query it executes:
: SELECT "users".* FROM "users" INNER JOIN "friendships" ON "users".id = "friendships".friend_id WHERE (("friendships".user_id = 1) AND ((status = 2)))
So it seems like it's mangling the :through to have two :through's in one query.
Given the following relationships:
class User < ActiveRecord::Base
has_many :friends
has_many :comments
has_many :friends_comments, :through => :friends, :source => :comments
end
This statement will execute a single SQL statement. Associations essentially create named scopes for you that aren't evaluated until the end of the chain.
#user.friends_comments.find(:limit => 100, :order => 'created_at DESC')
If this is a common query, the find can be simplified into its own scope.
class Comments < ActiveRecord::Base
belongs_to :user
#named_scope was renamed to scope in Rails 3.2. If you're working
#if you're working in a previous version uncomment the following line.
#named_scope :recent, :limit => 100, : order => 'created at DESC'
scope :recent, :limit => 100, :order => 'created_at DESC'
end
So now you can do:
#user.friends_comments.recent
N.B.: The friends association on user may be a cyclical one through a join table, but that's not important to this solution. As long as friends is a working association on User, the preceding will work.

Query with Ruby on Rails

Hi: I'm struggling with a rails query. Finally, I want to make a JSON response with the following contents: A listing of all countries which have at least one company, associated through "Address" and a count of how of how many companies reside in a country.
Here is my simple model structure:
class Country < ActiveRecord::Base
has_many :addresses
end
class Address < ActiveRecord::Base
belongs_to :country
belongs_to :company
end
class Company < ActiveRecord::Base
has_one :address
end
What's the most elegant way to solve this with Rails?
You need to overload the to_json method :
class Country < ActiveRecord::Base
has_many :addresses
def to_json
# format your country as json with all the elements you need
end
end
Then in your controller do something like that:
countries = Country.find(:all).select{|c| c.companies.size > 0 }
json = countries.collect{|c| c.to_json }
You'll have to use has_many throught to get the companies from a country. Documentation here: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Sorry I don't have time for a more precise solution, but hopefully these pointers will help you. If you have specific problems, feel free to either comment or open a new question.
You might want to add a counter cache to Address
belongs_to :country, :counter_cache => :companies_count
It stores the number of companies in the country model, which saves you from the N+1 query issue.
Alternatively you could avoid it also by issuing a single query like this:
Country.find(:all, :select => 'countries.*, count(addresses.id) as companies_count', :joins => 'LEFT JOIN addresses ON addresses.country_id = countries.id', :group => 'countries.id')
This way all of the returned countries will have field companies_count containing the number of companies for that country.