Include nested associations in JSON - ruby-on-rails

# Person.rb
class Person < ActiveRecord::Base
has_many :events
end
# Event.rb
class Event < ActiveRecord::Base
belongs_to :person
has_many :tables
end
# Table.rb
class Table < ActiveRecord::Base
belongs_to :event
end
Within Person.rb I am trying to create a method to get all events and tables in one query
def complete_events
events.includes(:tables)
end
I can see in the console it is loading both events and table
Event Load (1.1ms) SELECT "events".* FROM "events" WHERE "event"."person_id" = $1 [["person_id", 17]]
Table Load (0.5ms) SELECT "tables".* FROM "tables" WHERE "tables"."event_id" IN (10)
However, the returned object is only the event records.
#<ActiveRecord::Relation [#<Event id: 1, name: "some event">]
How can I get the returned record to nested like below?
#<ActiveRecord::Relation [#<Event id: 1, name: "some event", tables: [id: 1, seats: 4, id: 2, seats: 4]>]
Edit
I am able to iterate and create a all_events object, but it not nested.
def complete_events
all_events = []
events.includes(:tables).each do |event|
all_events << event
all_events << event.tables
end
all_events
end

Use as_json:
def complete_events
as_json(
include: {
events: {
include: tables
}
)
end

try this.
User.all.as_json(methods:[:profile,:addresses])
here User is the Model, and profile and address are the mapped table having has_many or has_one associations , so the json that will be rendered will be containing the table in nested way as follows
user : {
name:"dummy name",
profile: {},
addresses: {}
}
so in one single object you will have all the data and the associated data.

Related

Querying a join table?

I am trying to do two things:
query an attribute from an inner join table in Rails' Console.
query and displaying the attribute in a view.
These are my Models:
retreat.rb:
class Retreat < ApplicationRecord
belongs_to :user
belongs_to :account
validates :name, presence: true
has_many :retreats_teams
has_many :teams, through: :retreats_teams
accepts_nested_attributes_for :retreats_teams
end
retreats_team.rb:
class RetreatsTeam < ApplicationRecord
belongs_to :team
belongs_to :retreat
end
team.rb:
class Team < ApplicationRecord
belongs_to :account
has_many :team_members
has_many :users, through: :team_members
accepts_nested_attributes_for :team_members
has_many :retreats
has_many :retreats, through: :retreats_teams
end
In Rails' console, if I type:
Retreat.last.teams
I get the output:
irb(main):008:0> Retreat.last.teams
Retreat Load (0.9ms) SELECT "retreats".* FROM "retreats" ORDER BY "retreats"."id" DESC LIMIT $1 [["LIMIT", 1]]
Team Load (0.9ms) SELECT "teams".* FROM "teams" INNER JOIN "retreats_teams" ON "teams"."id" = "retreats_teams"."team_id" WHERE "retreats_teams"."retreat_id" = $1 LIMIT $2 [["retreat_id", 38], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Team id: 56, name: "My house", account_id: 2, created_at: "2020-02-10 15:57:25", updated_at: "2020-02-10 15:57:25">]>
irb(main):009:0>
How do I retrieve the team name: "My house"?
Also, there might be many teams that display here, too.
#teams returns a collection of team objects. The simplest solution is to call first on the teams to get the first team in the collection:
Retreat.last.teams.first.name
=> "My house"
But if you want all the names in teams you might use pluck. This will allow you to do this:
retreat = Retreat.last
foo = Team.create(name: 'Foo')
bar = Team.create(name: 'Bar')
retreat.teams << foo
retreat.teams << bar
retreat.teams.pluck(:name).to_sentence
=> "My house, Foo, and Bar"
A word on naming
The naming convention for join models is SingularSingular. The table should be named singular_plural. has_and_belongs_to_many is the only part of Rails that actually uses the oddball plural_plural naming scheme.
RetreatsTeam # bad
RetreatTeam # better
Even better though is to actually give your join tables meaningful names instead of just placeholder names.
1) querying an attribute from an inner join table in Rails Console.
Since the association between Retreat and RetreatsTeams in one to many you can actually only fetch aggregates. Otherwise which attribute should it fetch, from the first row, the last row or all the rows?
So for example you can do:
Retreat.joins(:retreats_teams)
.select('retreats.*', 'COUNT(retreats_teams.*) AS retreats_teams_count')
If you are storing more data on the join table that you want to display you want to iterate through the join table:
#retreat = Retreat.eager_load(retreats_teams: :teams).first
#retreat.retreats_teams.each do |rt|
puts rt.foo
puts rt.team.name
end
2) querying and displaying the attribute in a view.
In Rails you're usually just fetching records in the controller and then iterating through them in the view:
class ResortsController < ApplicationController
def show
#resort = Resort.includes(:teams).find(params[:id])
end
end
# app/views/resorts/show.html.erb
<h1><%= #resort.name %></h1>
<h2>Teams</h2>
<% if #resort.teams.any? %>
<ul>
<% #resort.teams.each do |team| %>
<li><%= team.name %></li>
<% end %>
</ul>
<% else %>
<p>This resort has no teams</p>
<% end %>

Workaround for AssociationTypeMismatch?

I'm in the middle of a refactoring, where I have written new models for new incoming data that will be behind a feature flag, while simultaneously keeping old models active for incoming data for people who don't yet have the feature flag. The old and new models both interact with the same table in the database.
i.e.
class Poem < ApplicationRecord
belongs_to: :author
belongs_to: :anthology, optional: true
end
class Anthology < ApplicationRecord
belongs_to: :author
has_many: :poems
end
class Author < ApplicationRecord
has_many: :anthologies
has_many: :poems
end
class NewPoem < ApplicationRecord
self.table_name = 'poems'
belongs_to: :author, class_name: 'NewAuthor'
belongs_to: :anthology, class_name: 'NewAnthology', optional: true
end
class NewAnthology < ApplicationRecord
self.table_name = 'anthologies'
belongs_to: :author, class_name: 'NewAuthor'
has_many: :poems, class_name: 'NewPoem'
end
class NewAuthor < ApplicationRecord
self.table_name = 'authors'
has_many: :anthologies, class_name: 'NewAnthology'
has_many: :poems, class_name: 'NewPoem'
end
When I have new books being created, I want to assign the new book to the author
anthology = Anthology.find(1)
#poem = NewPoem.new
#poem.author = anthology.author
Which gives me an error
ActiveRecord::AssociationTypeMismatch (NewAuthor(#70008442274840) expected, got Author(#47031421032620)):
Is there any way to get around this? or will I not be able to associate the models to the old models without a data migration?
You would need to change the first line that fetches the Anthology record. Currently, it fetches a record of the Anthology model as anthology = Anthology.find(1). However, this model refers to the old Author model.
In the next line, #poem = NewPoem.new instantiates an object of the NewPoem model, and the next line tries to assign an author to it. However, #poem.author would be an object of the class NewAuthor, but it is being given a value as #poem.author = anthology.author, where anthology.author still refers to the old Author table.
Changing the first line to anthology = NewAnthology.find(1) would fix the issue. It would fetch the same record as Anthology.find(1) since they query on the same table.
Overall code then becomes:
anthology = NewAnthology.find(1)
#poem = NewPoem.new
#poem.author = anthology.author
It looks like the new models are just supersets of the old ones. You could define NewPoem (and the rest of new models) as:
class NewPoem < Poem
validates :new_colums
def new_cool_stuff
end
end
That way you can avoid to redefine associations.
ActiveRecord actually has a built in Single Table Inheritance mechanism that you can use here.
Start by adding a column named type to the table:
class AddTypeToPoems < ActiveRecord::Migration[5.2]
def change
add_column :poems, :type, :string
add_index :poems, :type
end
end
Then set the classes to inherit:
class Poem < ApplicationRecord
end
class NewPoem < Poem
# gets its table name from the parent class
end
ActiveRecord automatically sets the type column and even uses it when retrieving records:
irb(main):001:0> NewPoem.create
(0.3ms) BEGIN
NewPoem Create (1.3ms) INSERT INTO "poems" ("created_at", "updated_at", "type") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2019-05-08 19:17:45.086947"], ["updated_at", "2019-05-08 19:17:45.086947"], ["type", "NewPoem"]]
(0.7ms) COMMIT
=> #<NewPoem id: 1, title: nil, created_at: "2019-05-08 19:17:45", updated_at: "2019-05-08 19:17:45", type: "NewPoem">
irb(main):002:0> Poem.first
Poem Load (0.7ms) SELECT "poems".* FROM "poems" ORDER BY "poems"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> #<NewPoem id: 1, title: nil, created_at: "2019-05-08 19:17:45", updated_at: "2019-05-08 19:17:45", type: "NewPoem">
irb(main):003:0>
So even if we use Poem.first it instanciates a NewPoem instance from the result. If you use the finders on the subclasses rails will automatically scope it:
irb(main):003:0> NewPoem.all
NewPoem Load (1.8ms) SELECT "poems".* FROM "poems" WHERE "poems"."type" IN ('NewPoem') LIMIT $1 [["LIMIT", 11]]
If you only want the "old" type you need to use:
Poem.where(type: [nil, 'Poem'])

Loading Relations

Goal: I would like to include all of a customers medical conditions as an array in the result of a customer.
for:
cust = Customer.includes(:conditions).find(1)
expected result:
#<Customer id: 1, first_name: "John", last_name: "Doe", conditions [...]>
actual result:
#<Customer id: 1, first_name: "John", last_name: "Doe">
code:
I have 2 classes and a 3rd join class (ConditionsCustomer).
class Customer < ApplicationRecord
has_many :conditions_customers
has_many :conditions, through: :conditions_customers
end
#join table. Contains 2 foreign_keys (customer_id, condition_id)
class ConditionsCustomer < ApplicationRecord
belongs_to :customer
belongs_to :condition
end
class Condition < ApplicationRecord
has_many :conditions_customers
has_many :customers, through: :conditions_customers
end
What's interesting is that I see 3 select queries getting fired (customer, join table and medical conditions table) so I know the includes is somewhat working but unfortunately customer returns without the medical conditions.
I've also tried using a join but I get an array of same customer over and over again.
Is there an easy way to do this with ActiveRecord? I would prefer not having to merge the record manually.
Not really possible via active record, as json offers some cool possibilities :
render json: customers,
include: {
conditions: {
only: [:attr1, :attr2], # filter returned fields
methods: [:meth1, :meth2] # if you need model methods
},
another_joined_model: {
except: [:password] # to exclude specific fields
}
}

Rails Create Related Models together

I have two Models Order(id, user_id,...) and OrderEvent(id, order_id, ...)the data of these are stored in two Objects #order and #order_event.
Now i want to save Order and update the order_id in OrderEvent and
then save the content in the database.
In CakePHP if we had the content structured in a certain way we could save all of these together at once. Is there a way to save the content in a single call so that if there is any validation error both records are not created and fails
Order
--- !ruby/object:Order
attributes:
id:
host_id: 1
user_id: 1
order_no: PH1504-F3D11353
type_of_order: events
order_date: !ruby/object:DateTime 2015-04-17 10:49:52.066168000 Z
sub_total: 7050.0
tax_rate:
rate_cost:
deliver_charges:
discount_code:
discount_rate:
discount_cost:
total_cost: 7050.0
order_dishes_count:
order_events_count:
status: 0
created_at:
updated_at:
OrderEvent
--- !ruby/object:OrderEvent
attributes:
id:
order_id:
event_id: 2
no_of_ppl: 3
event_date: 2015-01-22 00:00:00.000000000 Z
cost: 2350.0
total_cost: 7050.0
status: 0
created_at:
updated_at:
Order
class Order < ActiveRecord::Base
belongs_to :user
belongs_to :host
has_many :order_events
has_many :messages
end
OrderEvent
class OrderEvent < ActiveRecord::Base
belongs_to :event
belongs_to :order
end
If you have set proper associations, following will work for you:
#order = Order.create(...)
#order_event = #order.order_events.create(...) # pass all attrs except id & order_id
EDIT:
If I have the object ready and just wanna save can I use '.save' instead of .create – Harsha M V
In that case, you can directly update #order_event as:
#order_event.update(:order => #order)
Best way is to use Transactions of rails. Reference : Transactions
What you are looking for might be :
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
If you only want to update the OrderEvent just do
class OrderEvent
belongs_to :order
end
And in your controller
#order_event.update_attributes(order: #order)
Edit : update_attributes saves to the database.
See http://apidock.com/rails/ActiveRecord/Persistence/update for more info.
If you have both models ready but none has an id yet then I think you should go for nested attributes :
class Order
accepts_nested_attributes_for :order_events
has_many :order_events
end
class OrderEvent
belongs_to :order
end
I controller, In order to save the Order, with its order_events :
def update
#order.update_attributes(order_events_attributes: [#order_event])
end
Should work like a charm.
Inverse_of and accepts_nested_attributes_for allow you to create two associated objects at the same time.
Your models should be something like:
class Order < ActiveRecord::Base
has_many :order_events, inverse_of: :order
accepts_nested_attributes_for :order_events
end
class OrderEvent < ActiveRecord::Base
belongs_to :order, inverse_of: :order_event
end
In the orders controller:
def new
#order = Order.new
#order.order_events.build
end
def create
#order = Order.new(order_params)
...
end
Allow order_events attributes in the params:
def order_params
params.require(:order).permit(order_event_attributes: [])
end
In the form:
<%= form_for #order do |f| %>
<%= f.fields_for :order_events do |event| %>
<!-- event fields go here -->
<% end %>
Here is an article with more details what invese_of does: http://viget.com/extend/exploring-the-inverse-of-option-on-rails-model-associations

ActiveModel Serializer sending all records instead of specified limited amount

I have a controller that looks like this:
class MetricsController < ApplicationController
def index
org = current_user.organization
metrics = Metric.where(organization: org).last(100)
render json: metrics, status: 200, include: [:organization]
end
end
metrics model:
class Metric < ActiveRecord::Base
belongs_to :organization
end
organization:
class Organization < ActiveRecord::Base
has_many :metrics
end
MetricSerializer:
class MetricSerializer < ActiveModel::Serializer
embed :ids
attributes :id, :provisioned_users, :connected_users, :created_at
has_one :organization
end
OrganizationSerializer:
class OrganizationSerializer < ActiveModel::Serializer
attributes :id, :name, :org_id, :gauth_enabled
has_many :metrics
end
the JSON though that gets returned
organizations: [
{
id: 1,
name: "ACME",
org_id: "org_id232323",
gauth_enabled: "f",
metric_ids: [
1,
2,
3,
...
1000
]
}
],
As you can see my serializer is spitting out every record in the Metric's table when I presumably only want the last 100. I'm unsure what I've set up wrong in AMS (0.8).
Interestingly my rails console shows the right SQL with the correct limits:
Metric Load (0.7ms) SELECT "metrics".* FROM "metrics" ORDER BY "metrics"."id" DESC LIMIT 100
Organization Load (0.3ms) SELECT "organizations".* FROM "organizations" WHERE "organizations"."id" = $1 LIMIT 1 [["id", 1]]
Check out the guides on associations:
By default, serializers simply look up the association on the original object. You can customize this behavior by implementing a method with the name of the association and returning a different Array.
So, each model gets serialized according to its serializer. Your organization models were being put through your OrganizationSerializer, which had has_many :metrics specified. Thus, rails looked up all of the metrics for that organization, and serialized them.
You didn't want that, you just cared about the one organization which the metric belonged to. So simply remove that line, and rails won't serialize the rest.

Resources