Adding `or` on `has_many` association - ruby-on-rails

I have these models and I want to get all the addresses of both customer_x and customer_y through the assoc has_many :addresses.
Is there a method or something that can modify the has_many :addresses to add a codition OR in the query?
# customer.rb
class Customer < ApplicationRecord
has_many :addresses
end
# customer_x.rb
class CustomerX < Customer
has_many :customer_ys
end
# customer_y.rb
class CustomerY < Customer
belongs_to :customer_x, foreign_key: :customer_x_id
end
# address.rb
class Address < ApplicationRecord
belongs_to :customer
end
I tried this but of course it will only return all the addresses belonging to customer_id 1.
customer = Customer.first
customer.addresses
=> SELECT * FROM addresses WHERE customer_id = 1
What I want is to add OR in the condition statement like this:
=> SELECT * FROM addresses WHERE customer_id = 1 OR customer_x_id = 2

customer_x = Customer.find(1)
customer_y = Customer.find(2)
This will give you the addresses of customer_x or customer_y
address_of_custormers_x_or_y = customer_x.addresses.or(customer_y.addresses)

I guess this design looks very complicated. If You still want to use same way then take a look at following hack.
It works if you are not using STI here.
# customer.rb
class Customer < ApplicationRecord
has_many :addresses
def addresses
if self.type == 'customer_x'
adrs = []
self.customer_ys.each do|c|
adrs << c.addresses
end
adrs << super
return adrs
else
super
end
end
end
# customer_x.rb
class CustomerX < Customer
has_many :customer_ys
end
# customer_y.rb
class CustomerY < Customer
belongs_to :customer_x, foreign_key: :customer_x_id
end
# address.rb
class Address < ApplicationRecord
belongs_to :customer
end
else if you are using STI. You need to move the addresses method to customer_x.rb class, as follows
# customer.rb
class Customer < ApplicationRecord
has_many :addresses
end
# customer_x.rb
class CustomerX < Customer
has_many :customer_ys
def addresses
adrs = []
self.customer_ys.each do|c|
adrs << c.addresses
end
adrs << super
adrs
end
end
# customer_y.rb
class CustomerY < Customer
belongs_to :customer_x, foreign_key: :customer_x_id
end
# address.rb
class Address < ApplicationRecord
belongs_to :customer
end
But if you observe carefully the addresses method, we are making one query for every customer_y for addresses to fetch. Instead if you want you can change that method to following way.
def addresses
cus_ids = self.customer_ys.pluck(:id)
cus_ids << self.id
Address.Where(customer_id: cus_ids)
end
JFYI: Its my friend Rahul's solution.

You can use simple where condition on address like below,
Address.where(customer_id: :customer_ids, customer_ids: [1,2])

Related

Adding an additional model relationships using build() on Rails

Currently I have the following four models:
users <-- agency_memberships --> agencies
|
|
agency_roles
agency_memberships is a join table, and agency_roles is a small lookup table with the roles :
ID NAME
-----------
1 admin
2 editor
...
Before adding the AgencyRole model, when a user was created, if a param create_agency = true, then this was enough to create a new Agency (along the join table AgencyMembership).
# user_controller.rb
def create
#user = User.new(user_params)
#user.agencies.build(name: "Demo Agency") if params[:user][:create_agency]
if #user.save!
...
end
However, now I need to add a valid AgencyRole before saving.
Is it possible to do so with .build() or what is the Rails best practice to do so?
Right now, I'm creating all relationships manually before saving, which works but isn't as compact:
def create
#user = User.new(user_params)
if (params[:user][:create_agency])
agency_name = params[:user][:agency_name]
agency_role = AgencyRole.find_by(name: 'admin')
#agency_membership = AgencyMembership.new(agency_role: #agency_role)
#agency = Agency.new(name: agency_name)
#agency_membership.agency = #agency
#agency_membership.user = #user
#agency_membership.agency_role = agency_role
#user.agency_memberships << #agency_membership
end
if #user.save!
...
end
EdIT: My model relationships are as follows:
class AgencyMembership < ApplicationRecord
belongs_to :agency
belongs_to :user
belongs_to :agency_role
end
class AgencyRole < ApplicationRecord
validates :name, uniqueness: { case_sensitive: false }, presence: true
end
class Agency < ApplicationRecord
has_many :agency_memberships
has_many :projects
has_many :users, through: :agency_memberships
end
class User < ApplicationRecord
has_many :agency_memberships, inverse_of: :user
has_many :agencies, through: :agency_memberships
end
You can encapsulate and separate it from the controller, keep thin controller fat model, beside that you can use autosave in order to auto save association.
class AgencyMembership < ApplicationRecord
belongs_to :agency_role, autosave: true
end
class Agency < ApplicationRecord
has_one :agency_membership, autosave: true
end
module BuildAgency
def build_with_role(attributes = {}, &block)
agency_role_name = attributes.delete(:role)
agency = build(attributes, &block) # association: user - membership - agency
# add role
agency_role = AgencyRole.find_by(name: agency_role_name)
agency.agency_membership.agency_role = agency_role # autosave
agency
end
end
class User < ApplicationRecord
has_many :agencies, autosave: true, :extend => BuildAgency
def build_agency(attributes)
new_agency = agencies.build_with_role(attributes)
# ...
new_agency
end
end
# controller
def create
if (params[:user][:create_agency])
#user.build_agency(name: params[:user][:agency_name], role: params[:user][:agency_role])
end
if #user.save! # it'll save agencies since we set `autosave`
end

Association in rails Model

Below is the model Structure.
class UserFamilyRole < ActiveRecord::Base
belongs_to :user_family
belongs_to :user_role
belongs_to :organization
end
class UserFamily < ActiveRecord::Base
has_many :user_family_roles
has_many :user_roles
end
I am looking for below result from the above UserFamilyRole table.
{
user_family1: [user_role1, user_role2],
user_family2: [user_role1, user_role2, user_role3]
...
}
I achieved this Result from below query:
user_family_roles = UserFamilyRole.where(:organization_id => org_id)
results = {}
juser_family_roles.each do |user_family_role|
user_family = UserFamily.find(user_family_role.user_family_id)
result[user_family.name] = user_family.user_roles.collect(&:title)
end
puts results
But Above query doesn't look optimized one so can anyone help me to write the optimized query.
In our App org_id is present.

Get Orders not Liked by logged in User Model in Rails 4

I have 3 Models: User, LikeOrder and Like. User has many LikeOrders. A User can like a LikeOrder only once. So I created Models as below:
class User < ActiveRecord::Base
has_many :like_orders
accepts_nested_attributes_for :like_orders
has_many :likes, dependent: :destroy
end
class LikeOrder < ActiveRecord::Base
belongs_to :user
has_many :likes, dependent: :destroy
end
class Like < ActiveRecord::Base
belongs_to :user
belongs_to :like_order
end
And Migration for Like Model is:
class CreateLikes < ActiveRecord::Migration
def change
create_table :likes do |t|
t.references :user, index: true, foreign_key: true
t.references :like_order, index: true, foreign_key: true
t.timestamps null: false
end
end
end
So when User likes a LikeOrder I do it this way (using likes method directly) without any problem:
class User < ActiveRecord::Base
...
def like(order)
likes.create(like_order: order) if likes.where(like_order: order).count == 0
end
end
class UserController < ApplicationController
def like
#user = User.find(params[:user_id])
#order = LikeOrder.find(params[:order_id])
#user.like #order
end
end
There was no problem.
My Problem is I want to get Orders that:
their status are pending and the id is greater that from_id param and are not liked by logged in User.
class LikeOrder < ActiveRecord::Base
...
def self.not_likeds(user, from_id)
joins(:likes).where("like_orders.id > ? and like_orders.status = ?", from_id, 'pending')
end
end
I'm getting the greater than from_id and pending ones.
I made a Join(:likes) But don't know how to Get Not Liked ones from likes table? I've been trying for 6 hours with no luck.
UPDATED: (1 Oct 2015)
Finally I think this is working:
class LikeOrder < ActiveRecord::Base
...
def self.not_likeds(user, from_id)
not_liked = []
pending_form_id(from_id).each do |order|
not_liked << order if order.likes.where('user_id = ?', user.id).count == 0
end
not_liked
end
end
But there might be another way without any block, using where method. can anyone help?
UPDATED: (15 Dec 2015)
I found a better solution:
where("id > ? AND status = ?",from_id, 'pending').where("id not in (SELECT like_order_id from likes WHERE user_id = ?)",user.id).where("user_id != ?",user.id).limit(limit)
I want to get Orders that: their status are pending and the id is
greater that from_id param and are not liked by logged in User.
#app/models/order.rb
class Order < ActiveRecord::Base
def not_liked user, from_id
joins(:likes).where(status: "pending", id > from_id).not(likes: {user_id: user.id})
end
end
This would allow you to call:
#order = Order.not_liked current_user, "5"
Not tested.
Your structure really confused me; why don't you just have a Like model instead of LikeOrder...
#app/models/like.rb
class Like < ActiveRecord::Base
#you could include an "order" attribute here
belongs_to :user
belongs_to :order
validates :user, uniqueness: { scope: :order, message: "Only one Order like per user!" }
end
#app/models/user.rb
class User < ActiveRecord::Base
has_many :likes
has_many :favourites, through: :likes, class: "Order", foreign_key: "order_id"
end
#app/models/order.rb
class Order < ActiveRecord::Base
has_many :likes
has_many :fans, through: :likes, class: "User", foreign_key: "user_id"
end
This would allow you to call...
#user = User.find params[:id]
#user.favourites => all the orders they liked
Hey you can try in this way
def self.not_likeds(user, from_id)
joins(:likes).where("like_orders.id > ? and like_orders.status = ? and likes.user_id not in (?)", from_id, 'pending',current_user.id)
end
UPDATE
In this case, I'm guessing that user is current_user so try to do this:
def self.not_likeds(user, from_id)
joins(:user, :likes).where("like_orders.id > ? and like_orders.status = ? and likes.id NOT IN (?)", from_id, 'pending', user.like_ids)
end
the solution for me was:
class LikeOrder < ActiveRecord::Base
...
def self.not_liked(user, from_id=0, limit=20)
where("id > ? AND status = ?",from_id, 'pending').where("id not in (SELECT like_order_id from likes WHERE user_id = ?)",user.id).where("user_id != ?",user.id).limit(limit)
end
end

Too many chained conditions in if-else

Using Rails. How to best rewrite the country_photo?
# country.rb
class Country < ActiveRecord::Base
has_many :zones
def country_photo
if !zones.blank? && !zones.first.shops.blank? && !zones.first.shops.first.photos.blank?
zones.first.shops.first.photos.first.url(:picture_preview)
end
end
end
# zones.rb
class Zone < ActiveRecord::Base
belongs_to :country
has_many :zone_shops
has_many :shops, :through => :zone_shops
end
# zone_shop.rb
class ZoneShop < ActiveRecord::Base
belongs_to :zone
belongs_to :shop
end
# shop.rb
class Shop < ActiveRecord::Base
end
Note that !x.blank? -> x.present?. Anyway, if you are ok with doing assignations in ifs (they are pretty common in Ruby), you can write:
def country_photo
if (zone = zones.first) &&
(shop = zone.shops.first) &&
(photo = shop.photos.first)
photo.url(:picture_preview)
end
end
If you like fancy abstractions, with Ick you can write:
def country_photo
zones.first.maybe { |zone| zone.shops.first.photos.first.url(:picture_preview) }
end
Assuming you want to show an image in a view, I would do something like this:
# show.html.haml
- if #country.photo
image_tag #country.photo.url(:picture_preview)
# country.rb
class Country < ActiveRecord::Base
def photo
zones.first.photo unless zones.blank?
end
end
# zone.rb
class Zone < ActiveRecord::Base
def photo
shops.first.photo unless shops.blank?
end
end
# shop.rb
class Shop < ActiveRecord::Base
def photo
photos.first unless photos.blank?
end
end

how to emulate has_many :through with polymorphic classes

I understand why ActiveRecord can't support has_many :through on polymorphic classes. But I would like to emulate some of its functionality. Consider the following, where a join table associates two polymorphic classes:
class HostPest < ActiveRecord::Base
belongs_to :host, :polymorphic => true
belongs_to :pest, :polymorphic => true
end
class Host < ActiveRecord::Base
self.abstract_class = true
has_many :host_pests, :as => :host
end
class Pest < ActiveRecord::Base
self.abstract_class = true
has_one :host_pest, :as => :pest
end
class Dog < Host ; end
class Cat < Host ; end
class Flea < Pest ; end
class Tick < Pest ; end
The goal
Since I can't do has_many :pests, :through=>:host_pests, :as=>:host (etc), I'd like to emulate these four methods:
dog.pests (returns a list of pests associated with this dog)
flea.host (return the host associated with this flea)
cat.pests << Tick.create (creates a HostPest record)
tick.host = Cat.create (creates a HostPest record)
Question 1
I've got a working implementation for the first two methods (pests and host), but want to know if this is the best way (specifically, am I overlooking something in ActiveRecord associations that would help):
class Host < ActiveRecord::Base
def pests
HostPest.where(:host_id => self.id, :host_type => self.class).map {|hp| hp.pest}
end
end
class Pest < ActiveRecord::Base
def host
HostPest.where(:pest_id => self.id, :pest_type => self.class).first.host
end
end
Question 2
I'm stumped on how to implement the << and = methods implied here:
cat.pests << Tick.create # => HostPest(:host=>cat, :pest=>tick).create
tick.host = Cat.create # => HostPest(:host=>cat, :pest=>tick).create
Any suggestions? (And again, can ActiveRecord associations provide any help?)
Implementing the host= method on the Pest class is straight forward. We need to make sure we clear the old host while setting a new host (as AR doesn't clear the old value from the intermediary table.).
class Pest < ActiveRecord::Base
self.abstract_class = true
has_one :host_pest, :as => :pest
def host=(host)
Pest.transaction do
host_pest.try(:destroy) # destroy the current setting if any
create_host_pest(:host => host)
end
end
end
Implementing pests<< method on Host class is bit more involved. Add the pests method on the Host class to return the aggregated list of pests. Add the << method on the object returned by pests method.
class Host < ActiveRecord::Base
self.abstract_class = true
has_many :host_pests, :as => :host
# pest list accessor
def pests
#pests ||= begin
host = self # variable to hold the current self.
# We need it later in the block
list = pest_list
# declare << method on the pests list
list.singleton_class.send(:define_method, "<<") do |pest|
# host variable accessible in the block
host.host_pests.create(:pest => pest)
end
list
end
end
private
def pest_list
# put your pest concatenation code here
end
end
Now
cat.pests # returns a list
cat.pests << flea # appends the flea to the pest list
You can address your problem by using STI and regular association:
class HostPest < ActiveRecord::Base
belongs_to :host
belongs_to :pest
end
Store all the hosts in a table called hosts. Add a string column called type to the table.
class Host < ActiveRecord::Base
has_many :host_pests
has_many :pests, :through => :host_pests
end
Inherit the Host class to create new hosts.
class Dog < Host ; end
class Cat < Host ; end
Store all the pests in a table called pests. Add a string column called type to the table.
class Pest < ActiveRecord::Base
has_one :host_pest
has_one :host, :through => :host_pest
end
Inherit the Pest class to create new pests.
class Flea < Pest ; end
class Tick < Pest ; end
Now when you can run following commands:
dog.pests (returns a list of pests associated with this dog)
flea.host (return the host associated with this flea)
cat.pests << Tick.create (creates a HostPest record)
tick.host = Cat.create (creates a HostPest record)
Note
Rails supports has_many :through on polymorphic classes. You need to specify the source_type for this to work.
Consider the models for tagging:
class Tag
has_many :tag_links
end
class TagLink
belongs_to :tag
belongs_to :tagger, :polymorphic => true
end
Let's say products and companies can be tagged.
class Product
has_many :tag_links, :as => :tagger
has_many :tags, :through => :tag_links
end
class Company
has_many :tag_links, :as => :tagger
has_many :tags, :through => :tag_links
end
We can add an association on Tag model to get all the tagged products as follows:
class Tag
has_many :tag_links
has_many :products, :through => :tag_links,
:source => :tagger, :source_type => 'Product'
end

Resources