Rails model multiple has_many :through relationships output to json - ruby-on-rails

I have the following data models and would like to render a json hash that includes information from each model. For example, client.id, client.name_first, client, name_last, every workout description for each client and each exercise description for each workout.
class Client < ActiveRecord::Base
belongs_to :account
belongs_to :trainer
has_many :programs
has_many :workouts, :through => :programs
end
class Workout < ActiveRecord::Base
has_many :programs
has_many :clients, :through => :programs
has_many :routines
has_many :exercises, :through => :routines
end
class Exercise < ActiveRecord::Base
has_many :routines
has_many :workouts, :through => :routines
end
My database migrations:
class CreateClients < ActiveRecord::Migration
def change
create_table :clients do |t|
t.integer :account_id
t.integer :trainer_id
t.string :name_first
t.string :name_last
t.string :phone
t.timestamps
end
end
end
class CreateWorkouts < ActiveRecord::Migration
def change
create_table :workouts do |t|
t.string :title
t.string :description
t.integer :trainer_id
t.timestamps
end
end
end
class CreateExercises < ActiveRecord::Migration
def change
create_table :exercises do |t|
t.string :title
t.string :description
t.string :media
t.timestamps
end
end
end
I am able to return the workouts for a particular client:
#client = Client.find(params[:id])
clients_workouts = #client.workouts.select('workouts.*,programs.client_id').group_by(&:client_id)
render json: clients_workouts
And I am able to return the exercises for a particular workout:
#workout = Workout.find(params[:id])
exercises_workouts = #workout.exercises.select('exercises.*, routines.workout_id').group_by(&:workout_id)
render json: exercises_workouts
However, I do not know how to return the data with information from all three tables (Client, Workout, Exercise) included (joined through Programs and Routines). Is this possible? And how is it done?

First, I'm not really sure what's happening in your query:
clients_workouts = #client.workouts.select('workouts.*,programs.client_id').group_by(&:client_id)
Is this not sufficient?
#client.workouts
Now, on to the answer... assuming I'm still following:
ActiveRecord offers a .to_json method, which is what's being implicitly called here. The explicit version would be e.g.
render json: clients_workouts.to_json
Knowing that, you can look up to_json in the api (here's some good documentation even though it shows as deprecated: http://apidock.com/rails/ActiveRecord/Serialization/to_json). But, basically, the answer is to start with the root object -- the client I believe -- and build the included objects and attributes/methods from there in the options hash.
render json: #client.to_json(include: { workouts: { include: :exercises } })
You can customize which attributes or methods are included from each related model if needed, just dig into the documentation a little. Have fun!

Very possible and their are different ways to optain this.
One, without any 3rd party library is to use includes, just as if you were solving an n+1 problem orā€¦
Use a much cooler approach and use active model serializers
Active Model Serializers

Related

Defining "has_many through: assocation" with scoped conditionals in the through model

I have a schema that looks something like this:
create_table "customers" do |t|
t.integer "customer_number"
end
create_table "past_payments" do |t|
t.integer "customer_number"
t.datetime "transaction_date"
t.integer "arbitrary_sequence_number"
end
create_table "payment_details" do |t|
t.datetime "transaction_date"
t.integer "arbitrary_sequence_number"
end
TL;DR from the schema - A Customer is associated with a past_payment through a primary/foreign key. And a PastPayment is associated with a single PaymentDetail when their transaction_date AND arbitrary_sequence_number are equal. Payments and Details have no formal primary/foreign key relationship.
That gives me the following ActiveRecord models:
class Customer < ActiveRecord::Base
has_many :past_payments, foreign_key: :customer_number, primary_key: :customer_number
has_many :payment_details, through: :past_payments # unfortunately, broken šŸ˜¢
end
class PastPayment < ActiveRecord::Base
has_one :payment_detail, ->(past_payment) {
where(arbitrary_sequence_number: past_payment.arbitrary_sequence_number)
}, foreign_key: :transaction_date, primary_key: :transaction_date
end
Since a Customer has_many :past_payments and a PastPayment has_one :payment_detail, I would think there's an association that can be defined such that a Customer has_many :payment_details, through: :past_payments, but I can't get that to work because of the scope defined on the has_one :payment_detail association.
Specifically, calling Customer.payment_details raises the NoMethodError: undefined method 'arbitrary_sequence_number' for #<Customer:0x2i8asdf3>. So it would seem the Customer is getting passed to my scope as opposed to the PastPayment.
Is it possible to define the has_many :payment_details association on the Customer? Am I doing something wrong?
To be clear, I'd like to be able to say Customer.where(some_conditions).includes(:payment_details) and execute just the two queries so if there's a way to accomplish that without associations, I'm open to it.
Note: I can't change this database. It's a database some other application writes to, and I need to read from it.
Unrelated to my question, here's the workaround I'm currently working with. If there is no way to properly use associations, I'd be happy to have this solution critiqued:
class Customer < ActiveRecord::Base
attr_writer :payment_details
def payment_details
#payment_details ||= Array(self).with_payment_details.payment_details
end
module InjectingPaymentData
def with_payment_details
results = self.to_a
return self unless results.first.is_a?(Customer)
user_ids = results.collect(&:id)
# i've omitted the details of the query, but the idea is at the end of it
# we have a hash with the customer_number as a key pointing to an array
# of PaymentDetail objects
payment_details = PaymentDetails.joins().where().group_by(&:customer_number)
results.each do |customer|
customer.payment_details = Array(payment_details[customer.customer_number])
end
end
end
end
ActiveRecord::Relation.send(:include, Customer::InjectingPaymentData)
Array.send(:include, Customer::InjectingPaymentData)
And with that I can do things like the following with minimal querying:
#customers = Customer.where(id: 0..1000).with_payment_details
#customers.each { |c| do_something_with_those_payment_details }
Problems with that approach?
You can leverage this gem to better handle "composite primary keys" in ActiveRecord: https://github.com/composite-primary-keys/composite_primary_keys
class Customer < ActiveRecord::Base
has_many :past_payments, foreign_key: :customer_number, primary_key: :customer_number
has_many :payment_details, through: :past_payments # this works now
end
class PastPayment < ActiveRecord::Base
has_one :payment_detail, foreign_key: [:transaction_date, :arbitrary_sequence_number],
primary_key: [:transaction_date, :arbitrary_sequence_number]
end

Associating two models in a way both models' attributes are always pulled together in any view

I'm building a tech-specific pricegrabber-like web app, and I have a model that carries params that are common in all products. This model is called Product. Then I have one model for each type of product that I'm going to work with, for example, I'm now trying to build the first specific model, which is Videocard. So, the Product model always must have one Specific model, in this case Product-Videocard.
At this moment I'm stuck finding a way to make a product and a specific model always come tied together whenever I reach to them, be it in an index view, show view, form_for, a search, etc. But I can't picture in my head how a form will create an item and its specifications and insert a foreign key into another model with only one submit request.
Below are both models and the migrations for each:
class Product < ApplicationRecord
#belongs_to :productable, :polymorphic => true
has_one :videocard, dependent: :destroy
# Comment for this Stackoverflow question: the way I'm thinking I
# should have to make tons of has_one associations, for the other
# products. Is there a DRY way to do this?
has_many :prices, through: :stores
validates :platform, presence: { message: "should be specified." }
validates :name, presence: { message: "should be specified." }
validates_associated :videocard
end
class Videocard < ApplicationRecord
belongs_to :product
end
Migrations (shortened to make this question as clear as possible):
class CreateProducts < ActiveRecord::Migration[5.0]
def change
create_table :products do |t|
t.references :productable, polymorphic: true, index: true
t.string :name
t.string :image
t.string :partnum
t.string :manufacturer
t.string :platform #mobile, desktop, server, laptop
t.timestamps
end
end
end
class CreateVideocards < ActiveRecord::Migration[5.0]
def change
create_table :videocards do |t|
t.references :product, index: true
t.integer :memory
t.string :interface
# [...lots of attributes...]
t.integer :displayport
t.integer :minidisplayport
t.integer :tdp
t.timestamps
end
end
end
Also how can I make it so that Product only needs one has_one association, instead of using multiple ones. Remember that Videocard will have one type of specification, Memory will have other, and so on.

Rails/SQL no results show from Query

Versions: CENTOS7, mysql2('>= 0.3.13', '< 0.5'), rails('4.2.6')
index.html.erb
<% #sections.each do |section| %>
<tr>
<td><%= section.course_id %></td>
<td><%= section.term_id %></td>
<td><%= section.user_id %></td>
</tr>
<% end %>
sections controller
class SectionsController < ApplicationController
before_action :authenticate_account!, except: [:show]
def index
#sections = User.find_by_account_id(current_user).courses
end
def show
end
end
createSections migration
class CreateSections < ActiveRecord::Migration
def change
create_table :courses do |t|
t.integer :course_id
t.timestamps null: false
end
create_table :terms do |t|
t.integer :term_id
t.timestamps null: false
end
create_table :users do |t|
t.integer :user_id
t.timestamps null: false
end
create_table :sections do |t|
t.belongs_to :course, index: true
t.belongs_to :term, index: true
t.belongs_to :user, index: true
t.timestamps null: false
end
end
end
course.rb model
belongs_to :user
has_many :sections
has_many :terms, :through => :sections
term.rb model
belongs_to :user
has_many :sections
has_many :courses, :through => :sections
section.rb model
belongs_to :course
belongs_to :term
belongs_to :user
user.rb
has_many :sections
has_many :courses, :through => :sections
has_many :terms, :through => :sections
Expected result: List the current(logged in) user's courses/terms/ID
Current result: blank
This is my first time working with rails and SO, I tried changing the relationships a few times to see if anything would change but not sure how to approach this. I have tried using ActiveRecord:Associations as a reference. What do I need to do to make this work?
If you already have current_user then you can just call the related models directly.
#sections = User.find_by_account_id(current_user).courses
to
#sections = current_user.courses
The first thing to address is that you typically want separate migrations for each distinct thing that you're migrating. It's a convention that helps you keep fine-grained control over your changes, and it helps keep your migrations clean.
Sometimes, though, you actually want to have a "mass" migration. You have a mass-migration here. In the mass-migration circumstance, the migration name should reflect the combined purpose, so you'd want to name it something like CreateCoreTables; CreateSections is too narrow a name for creating multiple tables. You will also need to change the name of the migration file to be 2016XXXXXXXX_create_core_tables.rb, where XXXXXXXXX is left as the previous value.
Next, you'll want to correct your use of keys (the _id columns), as these are improperly declaring the association fields, which will cause the associations to not work (or work incorrectly).
Instead, you want something like this:
class CreateCoreTables < ActiveRecord::Migration
def change
create_table :courses do |t|
t.timestamps
end
create_table :terms do |t|
t.timestamps
end
create_table :users do |t|
t.timestamps
end
create_table :sections do |t|
t.integer :user_id, null: false
t.integer :term_id, null: false
t.integer :course_id, null: false
t.timestamps
end
end
end
It's difficult to tell what the actual intended relationships are from the code provided. It would be worth reading on Active Record Migrations to make sure that you understand what relationships you intend, and how to describe them. While you're working out the migrations, keep revisiting the model relationships, as well. These are the bedrock of the application, so you want to spend time getting them right.
Remember, for ActiveRecord relationships, these rules will guide you:
If a model owns another model, use has_many or has_one
In the owned model, use belongs_to
If a model needs access to another model (but doesn't own it), use a has_many, :through relationship
Models cannot have has_many or has_one relationships directly to each other; you need an intermediate table in that case with the requisite belongs_to for each of the other tables
Once you have the migrations and relationships worked out, you can move on to the query. You can use Rails Eager Loading to optimize the query to retrieve the associations at the same time. This will address your current functional needs, and prevent an N+1 query issue at the same time.
#sections = Section.joins(courses: :terms).where(user: current_user)
When you have retrieved #sections, you can do these types of actions to get the data that you want. The courses and terms members are collections, and you can interact with them as though they were arrays:
#sections.each do |section|
puts "Section: #{section.id}"
puts "Number of user sections: #{#sections.courses.length}"
section.courses.each do |course|
puts "Course: #{course.id}"
end
puts "Number of user terms: #{#sections.terms.length}"
#sections.terms do |term|
puts "Term: #{term.id}"
end
puts "User's email: #{#sections.user.email}"
end
Once you've mastered these, you've got the basics of Rails. Work on one model/controller at a time to keep from overcomplicating the work; you can always add on more once once component is working like you expect. Always make sure that you have your foundation working before you move onto new aspects of the app that will depend on it.
Also, remember to use the Rails guides. They're very helpful, so keep them on hand at all times while you're learning. SO is also a great resource, and make sure that you ask pointed questions, so that you can get direct answers.

Rails: incrementing attribute from a model, upon creation of an instance from another model

Our Rails app works with the following models:
class User < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :calendars, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
class Calendar < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :users, through: :administrations
end
And here are our migrations:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :first_name
t.string :last_name
t.string :email
...
t.integer :total_calendar_count
t.integer :owned_calendar_count
t.timestamps null: false
end
end
end
class CreateAdministrations < ActiveRecord::Migration
def change
create_table :administrations do |t|
t.references :user, index: true, foreign_key: true
t.references :calendar, index: true, foreign_key: true
t.string :role
t.timestamps null: false
end
end
end
class CreateCalendars < ActiveRecord::Migration
def change
create_table :calendars do |t|
t.string :name
t.timestamps null: false
end
end
end
When a new #calendar is created, we need to increment :total_calendar_count and :owner_calendar_count by one in the User table.
We tried this in the CalendarsController:
class CalendarsController < ApplicationController
def create
#calendar = current_user.calendars.create(calendar_params)
current_user.total_calendar_count += 1
current_user.owned_calendar_count += 1
current_user.administrations.find_by(calendar_id: #calendar.id).update(role: 'Creator')
...
end
But it does not seem to update :total_calendar_count and :owner_calendar_count by one in the User table.
Are we missing a step here? Should we use an update action instead?
The actual problem in your code is that you don't then save the user.
So you update the counter... but this changes it on the local instance... and then after the controller action is done the change you made just disappears.
if you wanted to keep your code the way it is, you could do:
current_user.save
at the end.
but I'd advise you to look into the counter_cache, because it's the Rails way.
Also I'll point out that you haven't checked that the calendar successfully got created, before incrementing that counter... it's possible that it could fail a validation and not really have been created... you need to check for that first.
I have a best idea to solve your problems is as below....
Create a method that will call on the creating of calendar with the callbacks of model like as below...
Add the below inside the calendar model just after the validation and ORM relations
after_create :increment_counter
def increment_counter
calendar_user = self.user
calendar_user.update(:total_calendar_count += 1, :owned_calendar_count += 1 )
end
With the above code you don't need to do anything. It will increment the counter of calendar on every new entry of calendar.

Combining polymorphic association with many-to-many association... is this setup right?

I have a Genre model, and I want both videos to have many genres and profiles to have many genres. I also want genres to have many videos and genres to have many profiles. I understand the polymorphic and join table stuff, so I'm wondering if my code below will work as I intend it to. Also, I'd appreciate any advice on how to access things in my controller and views.
This is what I envision that the join table should look like (I don't think I need an elaborate :has :through association because all I need in the join table are the associations and nothing else, so the table won't have a model):
genres_videos_profiles:
-----------------------------------------------------
id | genre_id | genre_element_id | genre_element_type
Here's my genre.rb:
has_and_belongs_to_many :genre_element, :polymorphic => true
Here's video.rb:
has_and_belongs_to_many :genres, :as => :genre_element
Here's profile.rb:
has_and_belongs_to_many :genres, :as => :genre_element
Will this work as I intend it to? I'd like some feedback.
As far as I know HABTM associations canĀ“t be polymorphic, I couldnĀ“t find an example like yours in the API documentation. If you want only join tables, your code could look like this:
class Genre
has_and_belongs_to_many :videos
has_and_belongs_to_many :profiles
end
class Video
has_and_belongs_to_many :genres
end
class Profile
has_and_belongs_to_many :genres
end
And access it like Mike already wrote:
#genre.profiles
#profile.genres
#genre.videos
#video.genres
Migrations (for join tables only):
class CreateGenresVideosJoinTable < ActiveRecord::Migration
def self.up
create_table :genres_videos, {:id => false, :force => true} do |t|
t.integer :genre_id
t.integer :video_id
t.timestamps
end
end
def self.down
drop_table :genres_videos
end
end
class CreateGenresProfilesJoinTable < ActiveRecord::Migration
def self.up
create_table :genres_profiles, {:id => false, :force => true} do |t|
t.integer :genre_id
t.integer :profile_id
t.timestamps
end
end
def self.down
drop_table :genres_profiles
end
end
I think that has_and_belongs_to_many can be a bit difficult to follow when it comes to polymorphic (if it even works). So if you want to do the polymorhpic thing, then you can't use any "through" syntax:
class Genre < ActiveRecord::Base
has_many :genres_videos_profiles
end
class GenresVideosProfile
belongs_to :genre
belongs_to :genre_element, :polymorphic => true
scope :videos, where(:genre_element_type => "Video")
scope :profiles, where(:genre_element_type => "Profile")
end
And then you use it like:
# All genre elements
#genre.genres_videos_profiles.each do |gvp|
puts gvp.genre_element.inspect
end
# Only video genre elements
#genre.genres_videos_profiles.videos.each do |gvp|
puts gvp.genre_element.inspect
end
Check out that: http://blog.hasmanythrough.com/2006/4/3/polymorphic-through
For me it was perfect and clean!

Resources