Too many chained conditions in if-else - ruby-on-rails

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

Related

How to eliminate N+1 queries from database query in Ruby on Rails?

In my Rails 6 app I have these models:
class User < ApplicationRecord
has_many :read_news_items
has_many :news_items, :through => :read_news_items
end
class NewsItem < ApplicationRecord
has_many :read_news_items
has_many :users, :through => :read_news_items
def read?(user)
read_news_items.where(:user_id => user.id).any?
end
end
class ReadNewsItem < ApplicationRecord
belongs_to :user
belongs_to :news_item
end
In my controller action I want to list all news items and highlight the ones that have not yet been read by the user:
class NewsItemsController < ApplicationController
def index
#news_items = NewsItem.all
end
end
The problem is that this generates N+1 queries for each record because the read?(current_user) gets called for each user record.
How can this problem be overcome?
I tried appending includes(:read_news_items) and joins(:read_news_items) to the database query in my controller but to no avail.
You could try:
class NewsItem < ApplicationRecord
has_many :read_news_items
def read?(user)
if read_news_items.loaded?
read_news_items.any? {|rni| rni.user_id == user.id }
else
read_news_items.where(:user_id => user.id).any?
end
end
end
class NewsItemsController < ApplicationController
def index
#news_items = NewsItem.includes(:read_news_items).all
end
end
OK, I learned something from every answer that was given here. So thanks for that.
I changed my read? method to the following which seems to have eliminated the N+1 queries:
class NewsItem < ApplicationRecord
def read?(user)
user.read_news_items.pluck(:news_item_id).include?(id)
end
end

Adding `or` on `has_many` association

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])

Rails has_many STI with sub STI

I think it is more of a "Model Design" issue than a rails issue.
For clarity sake here is the business logic: I've Venues and I want to implement multiple APIs to get data about those venues. All this APIs have a lot in common, therefore I used STI.
# /app/models/venue.rb
class Venue < ApplicationRecord
has_one :google_api
has_one :other_api
has_many :apis
end
# /app/models/api.rb
class Api < ApplicationRecord
belongs_to :venue
end
# /app/models/google_api.rb
class GoogleApi < Api
def find_venue_reference
# ...
end
def synch_data
# ...
end
end
# /app/models/other_api.rb
class OtherApi < Api
def find_venue_reference
# ...
end
def synch_data
# ...
end
end
That part works, now what I'm trying to add is Photos to the venue. I will be fetching those photos from the API and I realise that every API might be different. I thought about using STI for that as well and I will end up with something like that
# /app/models/api_photo.rb
class ApiPhoto < ApplicationRecord
belongs_to :api
end
# /app/models/google_api_photo.rb
class GoogleApiPhoto < ApiPhoto
def url
"www.google.com/#{reference}"
end
end
# /app/models/other_api_photo.rb
class OtherApiPhoto < ApiPhoto
def url
self[url] || nil
end
end
My goal being to have this at the end
# /app/models/venue.rb
class Venue < ApplicationRecord
has_one :google_api
has_one :other_api
has_many :apis
has_many :photos :through => :apis
end
# /app/views/venues/show.html.erb
<%# ... %>
#venue.photos.each do |photo|
photo.url
end
<%# ... %>
And photo.url will give me the right formatting that is dependent of the api it is.
As I'm going deeper in the integration, something seems not right. If I had to Api the has_many :google_api_photo then every Api will have GoogleApiPhoto. What does not make sense to me.
Any idea how I should proceed from here?
I think I solved it.
By adding this to venue.rb
has_many :apis, :dependent => :destroy
has_many :photos, :through => :apis, :source => :api_photos
By calling venue.photos[0].url call the right Class based on the type field of the ApiPhoto

rails current_model in other model method

In book/show I want to see it's sales in each existing library without abusing the views. Can the logic be somehow be transported into the model? Current book/show.haml:
= #book.name
- #libraries.each do |library|
= library.sales.where(book_id: #book.id).map(&:quantity).sum
My idea is to add a method in library.rb like:
def current_book_sold_by_library
#book = Book.find(:id)
#sales.where(book_id: #book.id).map(&:quantity).sum
sales.map(&:quantity).sum
end
But playing with this did not help. My setup:
book.rb:
class Book < ActiveRecord::Base
end
library.rb
class Library < ActiveRecord::Base
has_many :books, through: :sales
end
sale.rb
class Sale < ActiveRecord::Base
belongs_to :book
belongs_to :library
end
books_controller.rb
class BooksController < ApplicationController
def show
#libraries = Library.all
#sales = #book.sales
end
end
You may add a method with a book as a parameter to Library model:
# view
- #libraries.each do |library|
= library.book_sold_by_library(#book)
# Library model
def book_sold_by_library(book)
sales.where(book_id: book.id).map(&:quantity).sum
end

Ruby Validation When Adding to Database

I am really new in Ruby and I am on the last step to finish my project, when I'm trying to add appointment I have to change if doctor works in that time. I don't know how to do this :(
It is how my db works:
In appointment I have data_wizyty (visit_date), doctor_id and godzina_wizyty(visit_time) - it is in my adding form.
In schedules I have:
dzien_tygodnia(day_of_the_week), poczatek_pracy(start_working), koniec_pracy(end_working) and doctors_workplace_id
In doctors_workplace:
doctor_id, schedule_id, clinic_id
I want to check if doctor is available in any of the clinic in choosen date and time :)
Please help me with this :)
I have already validated if date and time is unique with:
class Appointment < ActiveRecord::Base
validates :doctor_id, uniqueness: { scope: [:data_wizyty, :godzina_wizyty], message: 'Ten termin jest juz zajety!' }
end
I need to check if it is unique and if doctor works.
Appointment:
class Appointment < ActiveRecord::Base
validates :doctor_id, uniqueness: { scope: [:data_wizyty, :godzina_wizyty], message: 'Ten termin jest juz zajety!' }
after_initialize :aInit
after_save :aSave
belongs_to :patient
belongs_to :doctor
belongs_to :schedule
belongs_to :refferal
belongs_to :clinic
has_many :employees
include MultiStepModel
def self.total_steps
3
end
def aInit
#wymaga_Potwierdzenia = true
end
def aSave
if self.refferal_id == nil
#potwierdzona = false
else
#potwierdzona = true
end
if self.wymaga_Potwierdzenia == false
#potwierdzona = true
end
end
end
Schedule:
class Schedule < ActiveRecord::Base
has_many :appointments
belongs_to :clinic
belongs_to :doctors_workplace
def full_schedule
"#{dzien_tygodnia} : #{poczatek_pracy} - #{koniec_pracy}"
end
end
Doctors_workplace:
class DoctorsWorkplace < ActiveRecord::Base
has_many :schedules
belongs_to :doctor
belongs_to :clinic_surgery
end
Now I have something like this :
def check_doctor_available
if Schedule.where(doctor: doctor, dzien_tygodnia: data_wizyty.wday)
.where('poczatek_pracy < ? and koniec_pracy > ?', godzina_wizyty, godzina_wizyty).empty?
self.errors.add(:doctor, message: 'nie pracuje w tym terminie!')
end
It's what I have now:
def check_doctor_available
if DoctorsWorkplace.where(doctor_id: doctor_id) and
Schedule.where(doctors_workplace_id: ????, dzien_tygodnia: data_wizyty.wday)
.where('poczatek_pracy < ? and koniec_pracy > ?', godzina_wizyty, godzina_wizyty).empty?
self.errors.add(:doctor, message: 'nie pracuje w tym terminie!')
end
You can use a custom validation. Create a private method in appointment that checks if the doctor is available at the given date/time.
validate :check_doctor_available
private
def check_doctor_available
#your implementation
end
Take a look at this if you have any doubts what to write in your custom validation method.

Resources