Should I denormalize a has_many has_many? - ruby-on-rails

I have this:
class User < ActiveRecord::Base
has_many :serials
has_many :sites, :through => :series
end
class Serial < ActiveRecord::Base
belongs_to :user
belongs_to :site
has_many :episodes
end
class Site < ActiveRecord::Base
has_many :serials
has_many :users, :through => :serials
end
class Episode < ActiveRecord::Base
belongs_to :serial
end
I would like to do some operations on User.serials.episodes but I know this would mean all sorts of clever tricks. I could in theory just put all the episode data into serial (denormalize) and then group_by Site when needed.
If I have a lot of episodes that I need to query on would this be a bad idea?
thanks

I wouldn't bother denormalizing.
If you need to look at counts, you can check out counter_cache on the relationship to save querying for that.
Do you have proper indexes on your foreign keys? If so, pulling the data from one extra join shouldn't be that big of a deal, but you might need to drop down to SQL to get all the results in one query without iterating over .serials:
User.serials.collect { |s| s.episodes }.uniq # ack! this could be bad

It really depends on the scale you are needing out of this application. If the app isn't going to need to serve tons and tons of people then go for it. If you are getting a lot of benefit from the active record associations then go ahead and use them. As your application scales you may find yourself replacing specific instances of the association use with a more direct approach to handle your traffic load though.

Related

Persist array from before_save callback for ActiveModel::Dirty like change tracking on has_many through relationship

Firstly, apologies for the snappy question title! It does however sum up what I am trying to do.
I've been using ActiveModel::Dirty successfully to create a kind of audit trail on various model attributes within my app (like on Product below).
I've now have a fairly pressing request to be able to track the changes (additions & deletions in this case) on an associated has_many through relationship.
The models in question are:
class Product < ActiveRecord::Base
has_many :products_territories, :dependent => :destroy
has_many :territories, :through => :products_territories
end
class Territory < ActiveRecord::Base
has_many :products_territories
has_many :products, :through => :products_territories
end
class ProductsTerritory < ActiveRecord::Base
belongs_to :territory
belongs_to :product
end
I've failed with using ActiveModel::Dirty, it doesn't seem possible, so am trying my own thing which on the surface is quite simple; grab an array of a product's products_territories before_save and then again after_save and then perform comparisons on the two arrays to identify the additions and deletions. What I can't get my head around is the best way to persist the array of products_territories from the before save so it's then available to my after_save callback. I'm pretty certain ## class variables aren't the way to go and i'm also not so sure about session variables. I'm wondering whether something like Redis or Memchached is what I should be looking at?
Can anyone that's had to do something similar to this give me any pointers or direct me to some further reading please?
Thanks in advance.

Is there a way to get a single record back through a has_many relationship in Rails?

In my Rails (3.2) app, an Order has many LineItems. A LineItem has many LineItemPayments. A LineItemPayment has one Payment. (LineItems can potentially be payed for multiple times (subscriptions), which is why I have the join table there.)
I need to be able to query for order information from a payment record. I can get an array of orders via relationships, but I know they will always be the same order. Is there a way in Rails to set up the association to reflect this? If not, would it better to set up a method for retrieving the array and then picking the order out of that, or rather just storing the order_id with the payment and set up a direct relationship that sidesteps all this?
You'll need to work with the orders collection and narrow it down accordingly per your own logic. Although you certainly 'can' add the order_id to the payment directly, that will denormalize your data (as a cache) which is only recommended when you start hitting performance bottlenecks in your queries - otherwise it's asking for trouble in the area of data integrity:
class Payment < ActiveRecord::Base
has_many :line_item_payments
has_many :line_items, :through => :line_item_payments
has_many :orders, :through => :line_items
# use this to get the order quickly
def order
orders.first
end
# use this to narrow the scope on the query interface for additional modifications
def single_order
orders.limit(1)
end
end
class LineItemPayment < ActiveRecord::Base
belongs_to :line_item
belongs_to :payment
end
class LineItem < ActiveRecord::Base
belongs_to :order
has_many :line_item_payments
end

Simplify Rails Query

I'm trying to reduce the number of queries in my application and need some help with the following setup:
I have 5 models:
Bet
Choice
Spotprice
Spotarea
Product
They are associated with the following:
Bet belongs_to Choice
Choice belongs_to Spotarea
Choice belongs_to Product
Choice has_many Bets
Spotprice belongs_to Spotarea
Spotprice belongs_to Product
Spotarea has_many Spotprices
Spotarea has_many Choices
Product has_many Sprotprices
Product has_many Choices
My goal is to find the Spotprices that matches a Specific Bet. To do that I uses the following queries, but I'm sure it can be done in a better way, so when I run through 100 bets and want to see if they are above or below the corrosponding Spotprice I don't overload the DB with queries.
a = Bet.find(5)
b = Choice.find(a.choice_id)
c = Spotprice.where(:spotarea_id => b.spotarea_id, :product_id => b.product_id,
:deliverydate => b.deliverydate).first
Thanks!
first of all, set up join bridges:
class Choice
has_many :spotprices, :through => :spotarea
end
class Bet
has_many :spotprices, :through => :choice
end
then you can query things like
Bet.joins(:spotprices).where("spotprices.price > bets.value")
Before trying to decrease the number of queries, you should run a performance test on your app, and monitor the database load. Sometimes it's better to run a few small queries rather than one huge query with a few joins. Certain versions of Oracle seem especially bad at joins.
An alternative to joins, if you're trying to avoid the n+1 query problem, is to use preload and pass the association (preload takes the same arguments as includes). This makes ActiveRecord run one query per table.
Basically:
you always want to avoid the n+1 problem.
trying to combine multiple queries into a join could in the best case be a premature optimization, and in the worst case actually make performance worse.
Well here's one pretty easy change:
b = Bet.includes(:choice).find(5).choice
After a few hours and a lot of Google search I found a solution that works.. After adding the join bridges I wanted to do:
Bet.find(5).spotprice
But that didn't work because to do that I needed something like this in my Choice model:
has_one :spotprice, :through => [:spotarea, :product] :source => :spotprices
I that is not possible.. apperently..
So I found this link has_one :through => multiple and I could use that answer in my situation.
class Choice < ActiveRecord::Base
belongs_to :user
belongs_to :spotarea
belongs_to :product
has_many :bets
def spotprice
Spotprice.where(:product_id => self.product_id, :spotarea_id => self.spotarea_id, :deliverydate => self.deliverydate).first
end
class Bet < ActiveRecord::Base
belongs_to :user
belongs_to :choice
has_one :spotprice, :through => :choice
With the above I can now do:
Bet.find(5).choice.spotprice
If anybody got a better solution please let me know :)

ActiveRecord relationships between HAVE and IS

So I have the following models in my Ruby on Rails setup: users and courses
The courses need to have content_managers and those content_managers are made up of several individuals in the users model.
I'm a newbie, so bear with me. I was thinking of creating a new model called content_managers that has a user_id and a course_id that links the two tables. It makes sense to me that courses HAVE content_managers. However from the users model, it doesn't make sense that users HAVE content_managers. Some of them ARE content_managers.
From that point of view I believe I'm thinking about it incorrectly and need to set up my ActiveRecord in a different manner from what I'm envisioning. Any help is appreciated.
Thanks!
There's no "have" or "are" in ActiveRecord, only "has_many", "has_one" and "belongs_to". With those tools you should be able to do what you want.
An example:
class Course < ActiveRecord::Base
has_many :content_managers
end
class ContentManager < ActiveRecord::Base
has_many :content_manager_members
has_many :users,
:through => :content_manager_members,
:source => :user
end
class ContentManagerMember < ActiveRecord::Base
belongs_to :course_manager
belongs_to :user
end
class User < ActiveRecord::Base
has_many :content_manager_members
has_many :content_managers,
:through => :content_manager_members
end
Be sure to index these correctly and you should be fine, though navigating from User to Course will be slow. You may need to cache some of this in order to find the level of performance you want, but that's a separate issue that will be uncovered during testing.
Whenever implementing something like this, be sure to load it up with a sufficient amount of test data that will represent about 10x the anticipated usage level to know where the ceiling is. Some structures perform very well only at trivial dataset sizes, but melt down when exposed to real-world conditions.

Fake a composite primary key? Rails

I have a table with id|patient_id|client_id|active. A record is unique by patient_id, client_id meaning there should only be one enrollment per patient per client. Normally I would make that the primary key, but in rails I have id as my primary key.
What is the best way to enforce this? Validations?
Sounds like you have a model relationship of:
class Client < ActiveRecord::Base
has_many :patients, :through => :enrollments
has_many :enrollments
end
class ClientPatient < ActiveRecord::Base
belongs_to :client
belongs_to :patient
end
class Patient < ActiveRecord::Base
has_many :clients, :through => :enrollments
has_many :enrollments
end
To enforce your constraint I would do it in ActiveRecord, so that you get proper feedback when attempting to save a record that breaks the constraint. I would just modify your ClientPatient model like so:
class Enrollment < ActiveRecord::Base
belongs_to :client
belongs_to :patient
validates_uniqueness_of :patient_id, :scope => :client_id
end
Be careful though because, while this is great for small-scale applications it is still prone to possible race conditions as described here: http://apidock.com/rails/v3.0.5/ActiveRecord/Validations/ClassMethods/validates_uniqueness_of under "Concurrency and Integrity"
As they describe there, you should also add a unique index to the table in the database. This will provide two immediate benefits:
The validation check and any searches through this model based on these two id's will perform faster (since they're indexed)
The uniqueness constraint will be enforced DB-side, and on the rare occurrence of a race condition you won't get bad data saved to the database... although users will get a 500 Server Error if you don't catch the error.
In a migration file add the following:
add_index :enrollments, [:patient_id, :client_id], :unique => true
Hopefully this was helpful :)
Edit (fixed some naming issues and a couple obvious bugs):
It's then very easy to find the data you're looking for:
Client.find_by_name("Bob Smith").patients
Patient.find_by_name("Henry Person").clients
Validations would work (Back them up with a unique index!), but there's no way to get a true composite primary key in vanilla Rails. If you want a real composite primary key, you're going to need a gem/plugin - composite_primary_keys is the one I found, but I'm sure there are others.
Hope this helps!
Add a UNIQUE constraint to your table across the two columns. Here's a reference for MySQL http://dev.mysql.com/doc/refman/5.0/en/constraint-primary-key.html

Resources