caching table values (avoiding unnecessary find queries) - is this a good approach? - ruby-on-rails

In my app i often need to find brand id by brand name in many places, so what if...
class Brand < ActiveRecord::Base
...
after_save :refresh_names_hash
#this is a rare action because brands list is static enough
def refresh_names_hash
Brand.names_hash(true)
end
def self.names_hash(refresh=false)
#names_hash = nil if refresh
#names_hash ||= Hash[all.map{|b| [b.name, b.id]}]
end
...
end
Hope everything is clear enough from this code
And here is a question: is this a good approach to use Brand.names_hash[params[:name]] to achieve brand id every time i need it instead of query it from db in production environment (classes cache enabled)?

Related

In rails how to allow creation of new class and models at run time

I am facing a design problem with respect to a rails app I am developing for my company product right now. My app allows creation of two classes which are subclasses of a parent class.
class Coupon
include Commonelements
end
class ServiceCenterCoupon < Coupon
end
class DealershipCoupon < Coupon
end
When you go to the view and you want to create a new coupon, you select either of the two and a new coupon is created depending upon the params[:coupon_type]
In the controller:
if params[:coupon_type] == 'dealershipcoupon'
#coupon = DealershipCoupon.new(coupon_params)
if #coupon.save!
redirect_to #coupon
else
render :new
end
elsif params[:coupon_type] == 'servicecentercoupon'
#coupon = ServiceCenterCoupon.new(coupon_params)
if #coupon.save!
redirect_to #coupon
else
render :new
end
end
I wanna give admin users the ability to create new coupon types at the run time as well. Say, someone wants to create Repairshopcoupons class. What changes do I need to bring to the views for example add a new form or what params I need to add to the existing form to be able to create new sub classes of Coupons at run time?
I do understand that using
repairshopcoupon = Class.new()
can work. For example anonymous function like this code in the controller can work:
Repairshopcoupon = Class.new(Coupon) do
include ActiveModel::Validations
validates_presence_of :title
def self.name
"Oil Change"
end
end
#newrepairshopcoupon = Repairshopcoupon.new
#newrepairshopcoupon.save
But I am not sure.
My first questions is: What would be the proper flow if I want users to create new classes from the view. What should controller handle and how will it save?
My second question is: There are few customers who belong to both dealerships and service centers group. Each group has authority over what coupon type they can manage. I want these users who belong to multiple groups to be able to see respective coupon inventory as well as which users downloaded those. I feel the need of changing my data model so that all coupon inventory and download lists belong to exactly one authorized group but I don't have a concrete idea of what would be the best way.
My third question is: What would be the best approach to change my view/UX for creating and managing coupons so that the users of multiple groups would be able to switch between each inventory ? What would be the professional industry standard for UX deign in this case ?
Would really appreciate your help.
Letting the users of an application generate code at runtime is just a really bad idea as the amount of potential bugs and vulnerabilities is mind boggling as your basically allowing untested code to be injected into the app at runtime.
It will also wreck havoc with any class based caching in the application.
It also won't work with cloud platforms like Heroku that use an ephemeral file system which is created from the last code commit.
First off you probably don't actually need different classes for each "type" of coupon. You need to consider if the logic for each class is substantially different.
You can probably get by just by creating a polymorphic association to the issuer (the dealship or service center).
class Coupon < Coupon
belongs_to :issuer, polymorphic: true
end
If you want to avoid polymorphism than just set it up as a standard STI setup:
class Coupon
include Commonelements
end
class ServiceCenterCoupon < Coupon
self.table_name = 'coupons'
belongs_to :service_center
end
class DealershipCoupon < Coupon
self.table_name = 'coupons'
belongs_to :dealership
end

ActiveRecord uniqueness validation prevents update

I'm writing a web app using Rails, part of which includes giving users the ability to leave reviews for things. I wanted to put a validation in the review model to ensure that one user can't leave multiple reviews of the same item, so I wrote this:
class NoDuplicateReviewValidator < ActiveModel::Validator
def validate(record)
dup_reviews = Review.where({user_id: record.user,
work_id: record.work})
unless dup_reviews.length < 1
record.errors[:duplicate] << "No duplicate reviews!"
end
end
end
This validator has the desired behavior, i.e. it guarantees that a user can't review a work twice. However, it has the undesired side-effect that a user can't update an already existing review that he/she left. I'm using a really simple
def update
#review.update(review_params)
respond_with(#work)
end
in the reviews controller. How can I change either the validator or the update method so that duplicate reviews are prevented but updates are allowed?
I'm very new to Rails and web development, so I'm sure I've done something goofy here. I didn't use one of the built-in unique validators because what is unique is the user/work pair; there can more than one review by the same user of different works, and there can be more than one review of the same work by different users.
You can use validates_uniqueness_of on multiple attributes, like this:
validates_uniqueness_of :user_id, :scope => :work_id
Then a user would not be allowed to review a already reviewed work.
#Sharvy Ahmed's answer is definitely the best, as long as the case is simple enough – the OP's case seems like one of them.
However, if the conditions are more complex, you may need/want to write your custom validation. For that purpose, here's an example (checked with Rails 6.0).
class NoDuplicateReviewValidator < ActiveModel::Validator
def validate(record)
dup_reviews = Review.where(user_id: record.user,
work_id: record.work)
dup_reviews = dup_reviews.where.not(id: record.id) unless record.new_record?
if dup_reviews.count > 0
record.errors[:duplicate] << "No duplicate reviews!"
end
end
end
The idea is,
In create, all the relevant DB records retrieved with where can and should be used to judge the uniqueness. In the example new_record? is used to check it out, but it is actually redundant (because nil id matches no records).
In update, the DB row of the record to update must be excluded from the unique comparison. Otherwise, the update would always fail in the validation.
The count method is slightly more efficient in terms of DB transaction.

Correct way to create or update with multiple belongs_to in Rails

New to Rails and Ruby and trying to do things correctly.
Here are my models. Everything works fine, but I want to do things the "right" way so to speak.
I have an import process that takes a CSV and tries to either create a new record or update an existing one.
So the process is 1.) parse csv row 2.) find or create record 3.) save record
I have this working perfectly, but the code seems like it could be improved. If ParcelType wasn't involved it would be fine, since I'm creating/retrieving a parcel FROM the Manufacturer, that foreign key is pre-populated for me. But the ParcelType isn't. Anyway to have both Type and Manufacturer pre-populated since I'm using them both in the search?
CSV row can have multiple manufacturers per row (results in 2 almost identical rows, just with diff mfr_id) so that's what the .each is about
manufacturer_id.split(";").each do |mfr_string|
mfr = Manufacturer.find_by_name(mfr_string)
# If it's a mfr we don't care about, don't put it in the db
next if mfr.nil?
# Unique parcel is defined by it's manufacturer, it's type, it's model number, and it's reference_number
parcel = mfr.parcels.of_type('FR').find_or_initialize_by_model_number_and_reference_number(attributes[:model_number], attributes[:reference_number])
parcel.assign_attributes(attributes)
# this line in particular is a bummer. if it finds a parcel and I'm updating, this line is superfulous, only necessary when it's a new parcel
parcel.parcel_type = ParcelType.find_by_code('FR')
parcel.save!
end
class Parcel < ActiveRecord::Base
belongs_to :parcel_type
belongs_to :manufacturer
def self.of_type(type)
joins(:parcel_type).where(:parcel_types => {:code => type.upcase}).readonly(false) unless type.nil?
end
end
class Manufacturer < ActiveRecord::Base
has_many :parcels
end
class ParcelType < ActiveRecord::Base
has_many :parcels
end
It sounds like the new_record? method is what you're looking for.
new_record?() public
Returns true if this object hasn’t been saved yet — that is, a record
for the object doesn’t exist yet; otherwise, returns false.
The following will only execute if the parcel object is indeed a new record:
parcel.parcel_type = ParcelType.find_by_code('FR') if parcel.new_record?
What about 'find_or_create'?
I have wanted to use this from a long time, check these links.
Usage:
http://rubyquicktips.com/post/344181578/find-or-create-an-object-in-one-command
Several attributes:
Rails find_or_create by more than one attribute?
Extra:
How can I pass multiple attributes to find_or_create_by in Rails 3?

Improving performance of authorization checks in rails model

I am looking at improving the performance of a controller action in our Rails application. After looking at some perf counters I now know that the problem lies with the way we have multiple authorization checks peppered throughout our model code. They look something like:
Class Company < ActiveRecord::Base
def member?(user)
#look up a table to check for membership if #is_member does not exist else return #is_member
end
def employee?(user)
#look up a table to check for membership
end
def manager?(user)
#look up a table to check for membership
end
end
class SomeModel < ActiveRecord::Base
def some_method
do_something if current_company.employee?(current_user)
end
end
Since there are a bunch of places where we do a check similar to some_method, requests typically end up hitting the database a LOT of times. This seems like a wasteful way of doing things. What are the ways to speed up such authorization checks? (Assuming that caching is the way to go here)
Since authorizations change rarely, and it appears you have a finite list of memberships to account for, you could:
Add column to the user database for each role
create a callback on the role model to calculate memberships for the user
That way, you only calculate memberships once, when the role is saved. Everything else is a simple/fast column lookup.

Cache all models in a table

I need to cache (and expire) all the models in a table.
For example, if i have a model named Currency, i only have less than 10 possible currencies. Therefore, it would be nice to have:
class Currency < ActiveRecord::Base
cache_all(:expire_in => 10.minutes)
end
so that
Currency.all
Currency.find_by_name("USD")
should not hit the DB.
What do you think it could be a good approach?
Also, if you believe it would be better to use a model that is not backed up by a DB, please comment on this. Please notice that i would like to have a AR-style association.
Since the data set is so small, probably the best thing is to cache it in local memory. There are a couple ways to do this, one is to use Memoization like I show here. However that isn't the most efficient because it will store the all method and find_by_name method in separate caches even though they are the same object.
An alternative is to redefine the methods to cache the objects manually. Something like this.
class Currency < ActiveRecord::Base
def self.all
#all_cache ||= super.map(&:freeze) # freeze so you don't modify the cached objects
end
def self.find_by_name(name)
all.detect { |c| c.name.to_s.downcase == name.to_s.downcase }
end
def self.flush_all_cache
#all_cache = nil
end
end
There may be a plugin to handle this for you, but I haven't looked into that at all.

Resources