Fun Rails ActiveRecord Join with attributes - ruby-on-rails

I'm feeling rusty on my basic rails and working on a simple music stats service.
class User
has_many :monthly_top_artists # this model also has play_count and month attrs- not just a join
has_many :top_artists, through: monthly_top_artists, source: :artist
I've basically written my own #monthly_top_artists query method which queries for a specific month's artists. right now i'm working with a line like this:
current_top_artists = MonthlyTopArtist.where(user_id: id, month: time_range)
.joins(:artist).select('month,name,play_count,user_id,artist_id,monthly_top_artists.updated_at')
The purpose of this is to load up my MonthlyTopArtist model with all the artist data as well- but am I just forgetting some basic rails/active record methods that can handle this for me? I could also use user.top_artists with the code above, but this would give me the same problem- the monthly statistics are in another table. Am I overthinking this in trying to limit my db queries? I realize that writing my own getter method is a little bit more than vanilla rails but this join + selecting attributes not defined on my MTA model is making me think there is something bigger wrong with my schema (or i'm just forgetting something).

Related

How to design/model a has many relationship that has a meaningful join table?

I wasn't able to put well into words (in Question title) what I'm trying to do, so in honor of the saying that an image is worth a thousand words; In a nutshell what I'm trying to do is..
Basically, what I have is A Teacher has many Appointments and A Student has many Appointments which roughly translates to:
I'm trying to stay away from using the has_and_belongs_to_many macro, because my appointments model has some meaning(operations), for instance it has a Boolean field: confirmed.
So, I was thinking about using the has_many :through macro, and perhaps using an "Appointable" join table model? What do you guys think?
The Scenario I'm trying to code is simple;
A Student requests an Appointment with a Teacher at certain Date/Time
If Teacher is available (and wants to give lesson at that Date/Time), She confirms the Appointment.
I hope you can tell me how would you approach this problem? Is my assumption of using the has_many :through macro correct?
Thank you!
Both teachers and students could inherit from a single class e.g. Person. Then create an association between Person and Appointments. This way you keep the architecture open so that if in the future you want to add 'Parents' then they could easily be integrated and may participate in appointments.
It may not be completely straightforward how you do the joins with the children classes (Students, Parents, Teachers). It may involve polymorphic relationships which I don't particularly like. You should though get away with a single join table.
In any case, you want to design so that your system can be extended. Some extra work early on will save you a lot of work later.

A database design for variable column names

I have a situation that involves Companies, Projects, and Employees who write Reports on Projects.
A Company owns many projects, many reports, and many employees.
One report is written by one employee for one of the company's projects.
Companies each want different things in a report. Let's say one company wants to know about project performance and speed, while another wants to know about cost-effectiveness. There are 5-15 criteria, set differently by each company, which ALL apply to all of that company's project reports.
I was thinking about different ways to do this, but my current stalemate is this:
To company table, add text field criteria, which contains an array of the criteria desired in order.
In the report table, have a company_id and columns criterion1, criterion2, etc.
I am completely aware that this is typically considered horrible database design - inelegant and inflexible. So, I need your help! How can I build this better?
Conclusion
I decided to go with the serialized option in my case, for these reasons:
My requirements for the criteria are simple - no searching or sorting will be required of the reports once they are submitted by each employee.
I wanted to minimize database load - where these are going to be implemented, there is already a large page with overhead.
I want to avoid complicating my database structure for what I believe is a relatively simple need.
CouchDB and Mongo are not currently in my repertoire so I'll save them for a more needy day.
This would be a great opportunity to use NoSQL! Seems like the textbook use-case to me. So head over to CouchDB or Mongo and start hacking.
With conventional DBs you are slightly caught in the problem of how much to normalize your data:
A sort of "good" way (meaning very normalized) would look something like this:
class Company < AR::Base
has_many :reports
has_many :criteria
end
class Report < AR::Base
belongs_to :company
has_many :criteria_values
has_many :criteria, :through => :criteria_values
end
class Criteria < AR::Base # should be Criterion but whatever
belongs_to :company
has_many :criteria_values
# one attribute 'name' (or 'type' and you can mess with STI)
end
class CriteriaValues < AR::Base
belongs_to :report
belongs_to :criteria
# one attribute 'value'
end
This makes something very simple and fast in NoSQL a triple or quadruple join in SQL and you have many models that pretty much do nothing.
Another way is to denormalize:
class Company < AR::Base
has_many :reports
serialize :criteria
end
class Report < AR::Base
belongs_to :company
serialize :criteria_values
def criteria
self.company.criteria
end
# custom code here to validate that criteria_values correspond to criteria etc.
end
Related to that is the rather clever way of serializing at least the criteria (and maybe values if they were all boolean) is using bit fields. This basically gives you more or less easy migrations (hard to delete and modify, but easy to add) and search-ability without any overhead.
A good plugin that implements this is Flag Shih Tzu which I've used on a few projects and could recommend.
Variable columns (eg. crit1, crit2, etc.).
I'd strongly advise against it. You don't get much benefit (it's still not very searchable since you don't know in which column your info is) and it leads to maintainability nightmares. Imagine your db gets to a few million records and suddenly someone needs 16 criteria. What could have been a complete no-issue is suddenly a migration that adds a completely useless field to millions of records.
Another problem is that a lot of the ActiveRecord magic doesn't work with this - you'll have to figure out what crit1 means by yourself - now if you wan't to add validations on these fields then that adds a lot of pointless work.
So to summarize: Have a look at Mongo or CouchDB and if that seems impractical, go ahead and save your stuff serialized. If you need to do complex validation and don't care too much about DB load then normalize away and take option 1.
Well, when you say "To company table, add text field criteria, which contains an array of the criteria desired in order" that smells like the company table wants to be normalized: you might break out each criterion in one of 15 columns called "criterion1", ..., "criterion15" where any or all columns can default to null.
To me, you are on the right track with your report table. Each row in that table might represent one report; and might have corresponding columns "criterion1",...,"criterion15", as you say, where each cell says how well the company did on that column's criterion. There will be multiple reports per company, so you'll need a date (or report-number or similar) column in the report table. Then the date plus the company id can be a composite key; and the company id can be a non-unique index. As can the report date/number/some-identifier. And don't forget a column for the reporting-employee id.
Any and every criterion column in the report table can be null, meaning (maybe) that the employee did not report on this criterion; or that this criterion (column) did not apply in this report (row).
It seems like that would work fine. I don't see that you ever need to do a join. It looks perfectly straightforward, at least to these naive and ignorant eyes.
Create a criteria table that lists the criteria for each company (company 1 .. * criteria).
Then, create a report_criteria table (report 1 .. * report_criteria) that lists the criteria for that specific report based on the criteria table (criteria 1 .. * report_criteria).

How to find all items not related to another model - Rails 3

I have a fairly complicated lookup i'm trying to do in Rails and I'm not entirely sure how hoping someone can help.
I have two models, User and Place.
A user is related to Place twice. Once for visited_places and once for planned_places. Its a many to many relationship but using has_many :through. Here's the relationship from User.
has_many :visited_places
has_many :visited, :class_name=>"Place", :through=>:visited_places, :source=>:place
has_many :planned_places
has_many :planned, :class_name=>"Place", :through=>:planned_places, :source=>:place
In place the relationship is also defined. Here's the definition there
has_many :visited_users, :class_name=>"User", :through=>:visited_places
has_many :planned_users, :class_name=>"User", :through=>:planned_places
I'm trying to write a find on Place that returns all places in the database that aren't related to a User through either visited or planned. Right now I'm accomplishing this by simply querying all Places and then subtracting visited and planned from the results but I want to add in pagination and I'm worried this could complicate that. Here's my current code.
all_places = Place.find(:all)
all_places = all_places - user.visited - user.planned
Anyone know how i can accomplish this in just a call to Place.find. Also this is a Rails 3 app so if any of the active record improvements make this easier they are an option.
How about something like:
unvisited_places = Place.find(:all, :conditions => "id NOT IN(#{visited_places.map(&:place_id)})")
That's the general idea -- it can be made more efficient and convenient depending on your final needs.
You don't show it but if I am right in assuming that the VisitedPlace and PlannedPlace models have a belongs_to :user relationships then those tables have a user_id secondary key, right?
So in that case I would think it would be most efficient to do this in the database in which case you are looking for a select across a table join of places, visited_places and planned_places where users.id is not in either of visited_places or planned_places
in sql:
select * from places where id not in
(
(select place_id from visited_places where user_id = ?)
union
(select place_id from planned_places where user_id=?)
)
If that query works, you can use as follows:
Places.find_by_sql(...the complete sql query ...)
I would not know how to write such a query, with an exclusion, in Rails 3 otherwise.
I ran into a similar desire recently... I wanted to get all Model1s that weren't associated with a Model2. Using Rails 4.1, here's what I did:
Model1.where.not(id: Model2.select(:user_id).uniq)
This creates a nested SELECT, like #nathanvda suggested, effectively letting the database do all the work. Example SQL produced is:
SELECT "model1s".* FROM "model1s" WHERE ("model1s"."id" NOT IN (SELECT DISTINCT "model2s"."model1_id" FROM "model2s"))

Simple question about ActiveRecord associations

My application has the concept of a "Loan". Each loan has a creditor, a debtor, and an amount.
From a database perspective, I know that I want the loans table to look something like this:
|id|Amount|creditor_id|debtor_id|
| 1| 100| 5| 7|
Where creditor/debtor ids reference User ids (i.e., the primary key for rows in my users table).
My question is how I should set this up in ActiveRecord. I can't do something like:
class Loan < ActiveRecord::Base
has_one :creditor
Since this will cause rails to look for a 'creditors' table (and the creditors are all stored in the users table).
The option (naturally, well-documented in the Rdoc) is :class_name. I believe the syntax is:
has_one :creditor, :class_name => 'User'
Also, you may want to bookmark this:
http://api.rubyonrails.org/
Could save a few minutes next time.
This is not as easy as it looks. Can users be both creditors and debtors? Can they have more than one loan?
Basically, it looks like you want a self-referential many-to-many relationhip. You want a join model called loan because you have extra data in your join table so you should use has-many :through. Here is a good blog post on the differences between habtm and has-many :through.
There are plenty of examples around of how to do complicated things with your model relationships. Be warned though that all the fancy model setup is not matched by examples of how to then setup your controllers and views. It seems the tutorial writers all steer well clear of full examples of many-to-many relationships with controllers and views because it is pretty hard.
Good luck

Rails ActiveRecord Relationships

How do the relationships magically function when only the models are altered?
If I want a "has__and___belongs___to__many" relationship, what should I name the table (so Rails can use it) that contains the two foreign keys?
Short answer: You can't just tell the models that they're related; there have to be columns in the database for it too.
When you set up related models, Rails assumes you've followed a convention which allows it to find the things you wrote. Here's what happens:
You set up the tables.
Following conventions in Rails, you name the table in a particular, predictable way (a plural noun, e.g. people). In this table, when you have a relationship to another table, you have to create that column and name it in another predictable way (e.g. bank_account_id, if you're relating to the bank_accounts table).
You write a model class inheriting from ActiveRecord::Base
class Person < ActiveRecord::Base
When you instantiate one of these models, the ActiveRecord::Base constructor looks at the name of the class, converts it to lowercase and pluralizes it. Here, by reading Person, it yields people, the name of the table we created earlier. Now ActiveRecord knows where to get all the information about a person, and it can read the SQL output to figure out what the columns are.
You add relationships to the model: has_many, belongs_to or has_one.
When you type something like, has_many :bank_accounts, it assumes a few things:
The name of the model that you relate to is BankAccount (from camel-casing :bank_accounts).
The name of the column in the people table which refers to a bank account is bank_account_id (from singularizing :bank_accounts).
Since the relationship is has_many, ActiveRecord knows to give you methods like john.bank_accounts, using plural names for things.
Putting all of that together, ActiveRecord knows how to make SQL queries that will give you a person's bank accounts. It knows where to find everything, because you followed a naming convention that it understands when you created the table and its colums.
One of the neat things about Ruby is that you can run methods on a whole class, and those methods can add other methods to a class. That's exactly what has_many and friends are doing.
This works because you are following "Convention over Configuration".
If you state that a customer model has many orders then rails expects there to be a customer_id field on the orders table.
If you have followed these conventions then rails will use them and will be able to build the necessary SQL to find all the orders for a given customer.
If you look at the development.log file when you are developing your application you will be able to see the necessary SQL being built to select all orders for a given customer.
Rails does not create tables without you asking it to. The creation of tables is achieved by generating a migration which will create/alter tables for you. The fact that you create a customer model and then state within it that it has_many :orders will not create you an orders table. You will need to do that for yourself within a migration to create an orders table. Within that migration you will need to either add a customer_id column or use the belongs_to: customer statement to get the customer_id field added to the orders table.
The rails guide for this is pretty useful

Resources