Inverse of Mongoid has_one polymorphic association - ruby-on-rails

I have a following model structure.
I have an Itinerary that has many itinerary nodes. Each itinerary node is a wrapper around either a place, hotel, activity etc. So for example.
Itinerary = "Trip to Paris"
Itinerary.itinerary_nodes = [Node1, Node2, Node3] where
Node1 = "JFK Airport"
Node2 = "CDG Airport"
Node3 = "Eiffel Tower"
So essentially nodes represents the places you will visit in your itinerary. In my model structure; lets assume that my airports are modeled different from monuments or hotels. Now I want to create a association such that;
class ItineraryNode
include Mongoid::Document
has_one :stopover
end
Where each stopover can be a different object. It's type and id is stored by default and is later inflated using that.
So how do I declare multiple models to be associated to ItineraryMode? I can implement this specifically by ensuring that I set these attributes manually in initializer; but curious if something like this is supported by default.
Cheers

This is not a "has_one", it is a "belongs_to" (polymorphic)
class ItineraryNode
include Mongoid::Document
belongs_to :stopover, :polymorphic => true
belongs_to :itinerary
end
class Airport
include Mongoid::Document
has_many :itinerary_nodes, :as => :stopover
end
class Place
include Mongoid::Document
has_many :itinerary_nodes, :as => :stopover
end
So now you can get:
#itinerary.itinerary_nodes.each do |node|
if node.stopover.is_a? Airport
puts "Welcome to #{note.stopover.name}"
elsif node.stopover.is_a? Event
puts "Bienvenue, would you like a drink?"
elsif node.stepover.is_a? Place
puts "The ticket line is over there"
end
end
(I used an if construct just to show better the polymorphism, you would use a case construct here...)
You see that node.stepover can be of many classes.
EDIT (after the comment, I understand that the ItineraryNodemodel is an attempt to a handcrafted polymorphism for a many-to-many association.
From the Mongoid documentation:
Polymorhic behavior is allowed on all relations with the exception of has_and_belongs_to_many.
So you need to use an intermediate model (ItineraryNode). The provided solution is the simplest one I can think of.

Related

Mongoid: How do I query for all object where the number of has_many object are > 0

I have a Gift model:
class Gift
include Mongoid::Document
include Mongoid::Timestamps
has_many :gift_units, :inverse_of => :gift
end
And I have a GiftUnit model:
class GiftUnit
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :gift, :inverse_of => :gift_units
end
Some of my gifts have gift_units, but others have not. How do I query for all the gifts where gift.gift_units.size > 0?
Fyi: Gift.where(:gift_units.exists => true) does not return anything.
That has_many is an assertion about the structure of GiftUnit, not the structure of Gift. When you say something like this:
class A
has_many :bs
end
you are saying that instance of B have an a_id field whose values are ids for A instances, i.e. for any b which is an instance of B, you can say A.find(b.a_id) and get an instance of A back.
MongoDB doesn't support JOINs so anything in a Gift.where has to be a Gift field. But your Gifts have no gift_units field so Gift.where(:gift_units.exists => true) will never give you anything.
You could probably use aggregation through GiftUnit to find what you're looking for but a counter cache on your belongs_to relation should work better. If you had this:
belongs_to :gift, :inverse_of => :gift_units, :counter_cache => true
then you would get a gift_units_count field in your Gifts and you could:
Gift.where(:gift_units_count.gt => 0)
to find what you're looking for. You might have to add the gift_units_count field to Gift yourself, I'm finding conflicting information about this but I'm told (by a reliable source) in the comments that Mongoid4 creates the field itself.
If you're adding the counter cache to existing documents then you'll have to use update_counters to initialize them before you can query on them.
I tried to find a solution for this problem several times already and always gave up. I just got an idea how this can be easily mimicked. It might not be a very scalable way, but it works for limited object counts. The key to this is a sentence from this documentation where it says:
Class methods on models that return criteria objects are also treated like scopes, and can be chained as well.
So, get this done, you can define a class function like so:
def self.with_units
ids = Gift.all.select{|g| g.gift_units.count > 0}.map(&:id)
Gift.where(:id.in => ids)
end
The advantage is, that you can do all kinds of queries on the associated (GiftUnits) model and return those Gift instances, where those queries are satisfied (which was the case for me) and most importantly you can chain further queries like so:
Gift.with_units.where(:some_field => some_value)

how to add records to has_many :through association in rails

class Agents << ActiveRecord::Base
belongs_to :customer
belongs_to :house
end
class Customer << ActiveRecord::Base
has_many :agents
has_many :houses, through: :agents
end
class House << ActiveRecord::Base
has_many :agents
has_many :customers, through: :agents
end
How do I add to the Agents model for Customer?
Is this the best way?
Customer.find(1).agents.create(customer_id: 1, house_id: 1)
The above works fine from the console however, I don't know how to achieve this in the actual application.
Imagine a form is filled for the customer that also takes house_id as input. Then do I do the following in my controller?
def create
#customer = Customer.new(params[:customer])
#customer.agents.create(customer_id: #customer.id, house_id: params[:house_id])
#customer.save
end
Overall I'm confused as to how to add records in the has_many :through table?
I think you can simply do this:
#cust = Customer.new(params[:customer])
#cust.houses << House.find(params[:house_id])
Or when creating a new house for a customer:
#cust = Customer.new(params[:customer])
#cust.houses.create(params[:house])
You can also add via ids:
#cust.house_ids << House.find(params[:house_id])
Preface
This is a strange scenario and I hesitated to answer. It seems like Agents should have many Houses rather than a one-to-one relationship, and a House should belong to just one Agent. But with that in mind....
"The best way" depends on your needs and what feels most comfortable/readable to you. Confusion comes from differences in ActiveRecord's behavior of the new and create methods and the << operator, but they can all be used to accomplish your goal.
The new Method
new will not add an association record for you. You have to build the House and Agent records yourself:
# ...
house = #cust.houses.new(params[:house])
house.save
agent = Agent.new(customer: #cust house: house)
agent.save
Note that #cust.houses.new and House.new are effectively the same because you still need to create the Agent record in both cases.
(This code looks weird, you can't easily tell what it's supposed to be doing, and that's a smell that maybe the relationships are set up wrong.)
The << Operator
As Mischa mentions, you can also use the << operator on the collection. This will only build the Agent model for you, you must build the House model:
house = House.create(params[:house])
#cust.houses << house
agent = #cust.houses.find(house.id)
The create Method
create will build both House and Agent records for you, but you will need to find the Agent model if you intend to return that to your view or api:
house = #cust.houses.create(params[:house])
agent = #cust.agents.where(house: house.id).first
As a final note, if you want exceptions to be raised when creating house use the bang operators instead (e.g. new! and create!).
Another way to add associations is by using the foreign key columns:
agent = Agent.new(...)
agent.house = House.find(...)
agent.customer = Customer.find(...)
agent.save
Or use the exact column names, passing the ID of the associated record instead of the record.
agent.house_id = house.id
agent.customer_id = customer.id

ROR: Select from multiple tables

I have two tables: interests and Link_ui (this is for recording user and interest)
I want to input the user id and show all interests name that user have.
In Link_ui controller:
def output
#interests = LinkUi.find_by_sql [ 'SELECT interests.name FROM link_uis, interests
WHERE link_uis.interest_id = interests.id AND link_uis.user_id=? ', params['user_id'] ]
And input page:
<%= form_tag :action => 'output', :method => 'post' %>
enter id.
<%= text_field_tag ':user_id', '', 'size' => 30 %>
It comes out nothing, but I am sure there is matched data in database. And if I don't input parameter just set link_uis.user_id = 1, it comes out:
your search are [#<LinkUi >, #<LinkUi >, #<LinkUi >, #<LinkUi >]
What's wrong with this..
Well, find_by_sql on a LinkUi model expects you to return columns from the link_uis table, whereas you're selecting just interests.name. However, you are picking a bit of a fight with ActiveRecord, there. :)
You usually want to avoid find_by_sql, and instead let ActiveRecord generate your SQL for you. Probably most important for your example are associations.
The way I see it, you have a bunch of Users, and a bunch of Interests. Your LinkUis tie these two together (a LinkUi belongs to a User and an Interest). Feel free to correct me on this; this is your business logic as I gather from your example.
These classes (whose names I've emphasized) are your models, defined in the app/models directory. The assocations (relationships) between them should be defined on those classes.
Start of with a simple association in your User model:
class User < ActiveRecord::Base
has_many :link_uis
end
And in your Interest model:
class Interest < ActiveRecord::Base
has_many :link_uis
end
Then the LinkUi model that ties it together:
class LinkUi < ActiveRecord::Base
belongs_to :user
belongs_to :interest
end
Now, given any User, you can get his/her LinkUis by simply saying user.link_uis.all, and for each LinkUi, you can get the Interest as link_ui.interest. You can tell ActiveRecord to try and fetch these two in one shot as efficiently as possible using :include, and get a list of Interest names using the standard Ruby collect method. It then becomes:
user = User.find params['user_id']
link_uis = user.link_uis.all(:include => :interest)
interest_names = link_uis.collect { |link_ui| link_ui.interest.name }
You can take it one step further; for any User, you can directly get his/her Interests. Once you've set up the above associations, you can fold two ‘steps’ into one, like this:
class User < ActiveRecord::Base
has_many :link_uis
has_many :interests, :through => :link_uis
end
Which could turn the example into this one-liner:
interest_names = User.find(params[:user_id]).interests.collect { |i| i.name }

How to handle this type of model validation in Ruby on Rails

I have a controller/model hypothetically named Pets. Pets has the following declarations:
belongs_to :owner
has_many :dogs
has_many :cats
Not the best example, but again, it demonstrates what I'm trying to solve. Now when a request comes in as an HTTP POST to http://127.0.0.1/pets, I want to create an instance of Pets. The restriction here is, if the user doesn't submit at least one dog or one cat, it should fail validation. It can have both, but it can't be missing both.
How does one handle this in Ruby on Rails? Dogs don't care if cats exists and the inverse is also true. Can anyone show some example code of what the Pets model would look like to ensure that one or the other exists, or fail otherwise? Remember that dogs and cats are not attributes of the Pets model. I'm not sure how to avoid Pets from being created if its children resources are not available though.
errors.add also takes an attribute, in this case, there is no particular attribute that's failing. It's almost a 'virtual' combination that's missing. Parameters could come in the form of cat_name="bob" and dog_name="stew", based on the attribute, I should be able to create a new cat or dog, but I need to know at least one of them exists.
You're looking for errors.add_to_base. This should do the trick:
class Pet < ActiveRecord::Base
belongs_to :owner
has_many :dogs
has_many :cats
validate :has_cats_or_dogs
def has_cats_or_dogs
if dogs.empty? and cats.empty?
errors.add_to_base("At least one dog or cat required")
end
end
end
If you want to pass cat_name or dog_name to the controller action, it may look like this:
class PetsController < ApplicationController
# ...
def create
#pet = Pet.new(params[:pet])
#pet.cats.build(:name => params[:cat_name]) if params[:cat_name]
#pet.dogs.build(:name => params[:dog_name]) if params[:dog_name]
if #pet.save
# success
else
# (validation) failure
end
end
end
Alternatively, for some more flexibility you can use nested attributes to create new cats and dogs in your controller.

Traversing HABTM relationships on ActiveRecord

I'm working on a project for my school on rails (don't worry this is not graded on code) and I'm looking for a clean way to traverse relationships in ActiveRecord.
I have ActiveRecord classes called Users, Groups and Assignments. Users and Groups have a HABTM relationship as well as Groups and Assignments. Now what I need is a User function get_group(aid) where "given a user, find its group given an assignment".
The easy route would be:
def get_group(aid)
group = nil
groups.each { |g| group = g if g.assignment.find(aid).id == aid }
return group
end
Is there a cleaner implementation that takes advantage of the HABTM relationship between Groups and Assignments rather than just iterating? One thing I've also tried is the :include option for find(), like this:
def get_group(aid)
user.groups.find(:first,
:include => :assignments,
:conditions => ["assignments.id = ?", aid])
end
But this doesn't seem to work. Any ideas?
First off, be careful. Since you are using has_and_belongs_to_many for both relationships, then there might be more than one Group for a given User and Assignment. So I'm going to implement a method that returns an array of Groups.
Second, the name of the method User#get_group that takes an assignment id is pretty misleading and un-Ruby-like.
Here is a clean way to get all of the common groups using Ruby's Array#&, the intersection operator. I gave the method a much more revealing name and put it on Group since it is returning Group instances. Note, however, that it loads Groups that are related to one but not the other:
class Group < ActiveRecord::Base
has_and_belongs_to_many :assignments
has_and_belongs_to_many :users
# Use the array intersection operator to find all groups associated with both the User and Assignment
# instances that were passed in
def self.find_all_by_user_and_assignment(user, assignment)
user.groups & assignment.groups
end
end
Then if you really needed a User#get_groups method, you could define it like this:
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
def get_groups(assignment_id)
Group.find_all_by_user_and_assignment(self, Assignment.find(assignment_id))
end
end
Although I'd probably name it User#groups_by_assignment_id instead.
My Assignment model is simply:
class Assignment < ActiveRecord::Base
has_and_belongs_to_many :groups
end

Resources