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'])
Related
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 %>
Let's say I have the following two models, joined separately by the following two joins models:
class Game
has_many :game_plays
has_many :game_views
end
class Person
has_many :game_plays
has_many :game_views
end
# Games that a Person has played
class GamePlay
belongs_to :game
belongs_to :person
end
# Games that a Person has viewed
class GameView
belongs_to :game
belongs_to :person
end
Given a specific GamePlay, I want to get the GameView for the same Person-Game combo, e.g:
game_play = GamePlay.first
game_view = GameView.find_by(game_id: game_play.game_id, person_id: game_play.person_id)
I also need to eager load that association.
I'd love to create an association between GamePlay and GameView, but nothing I've tried has worked so far.
Attempt 1
class GamePlay
belongs_to :game
belongs_to :person
has_one :game_view, -> (gp) { where(game_id: gp.game_id) }, foreign_key: :person_id, primary_key: :person_id
end
This works, but I can't include this:
GamePlay.includes(:game_view).first
# => ArgumentError: The association scope 'game_view' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.
Attempt 2
class GamePlay
belongs_to :game
belongs_to :person
def game_view
GameView.find_by(game_id: game_id, person_id: person_id)
end
end
This obviously works, but I can't include this because it isn't defined as an association.
Any thoughts? Thanks!
Rails 5.0.0postgres 9.6.2
How about:
class GamePlay < ApplicationRecord
belongs_to :game
belongs_to :person
has_one :game_view, through: :person, source: :game_views
end
irb(main):002:0> GamePlay.includes(:game_view).find(2)
GamePlay Load (0.2ms) SELECT "game_plays".* FROM "game_plays" WHERE "game_plays"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
Person Load (0.2ms) SELECT "people".* FROM "people" WHERE "people"."id" = 1
GameView Load (0.2ms) SELECT "game_views".* FROM "game_views" WHERE "game_views"."person_id" = 1
=> #<GamePlay id: 2, game_id: 1, person_id: 1>
irb(main):008:0> GamePlay.find(2).game_view
GamePlay Load (0.1ms) SELECT "game_plays".* FROM "game_plays" WHERE "game_plays"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
GameView Load (0.2ms) SELECT "game_views".* FROM "game_views" INNER JOIN "people" ON "game_views"."person_id" = "people"."id" WHERE "people"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<GameView id: 2, game_id: 1, person_id: 1>
I had the same challenge, I solved this with a custom version of Rails preloader https://github.com/2rba/smart_preloader
The association is exactly as you described:
class GamePlay
belongs_to :game
belongs_to :person
has_one :game_view, -> (gp) { where(game_id: gp.game_id) }, foreign_key: :person_id, primary_key: :person_id
end
and then explicitly call preloader as:
game_plays =GamePlay.all
ActiveRecord::SmartPreloader.(game_plays, ActiveRecord::CompositeKey.new(:game_view, [:game_id, :person_id])
that is pretty much the same as Rails default GamePlay.preloads(:game_view) behaviour which under the hood calls ActiveRecord::Associations::Preloader. The only difference preloader called explicitly, and preloader is slightly modified to support multikey and polymorphic associations.
I have User and Review models. A review can have an author and a subject, both pointing to a User:
class Review < ApplicationRecord
belongs_to :subject, class_name: 'User', optional: true
belongs_to :author, class_name: 'User', optional: true
end
class CreateReviews < ActiveRecord::Migration[5.0]
def change
create_table :reviews do |t|
t.references :subject
t.references :author
end
end
end
This works fine and now I can assign two separate User objects to the Review object to represent who wrote the review against whom.
The user though, doesn't "know" how many reviews he's associated with either as a subject or the author. I added has_and_belongs_to_many :users on reviews and vice-versa, and though doable, isn't exactly what I want.
How do I set up the associations to be able to do the following:
review.author = some_other_user
review.subject = user2
another_review.author = some_other_user
another_review.subject = user2
user2.a_subject_in.count
#=> 2
user2.a_subject_in
#=> [#<Review>, #<Review>]
some_other_user.an_author_in.count
#=> 2
In other words, how do I see how many times a User has been saved as an author or subject for a model with belongs_to?
IF you want to use has_many association on users side, you need to define two separate has_many relations like
class User < ApplicationRecord
has_many :reviews, foreign_key: :author_id
has_many :subject_reviews, class_name: 'Review', foreign_key: :subject_id
end
Now with this you can simply use
irb(main):033:0> s.reviews
Review Load (0.2ms) SELECT "reviews".* FROM "reviews" WHERE "reviews"."author_id" = ? [["author_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Review id: 1, comment: "random", subject_id: 2, author_id: 1, created_at: "2016-07-12 01:16:23", updated_at: "2016-07-12 01:16:23">]>
irb(main):034:0> s.subject_reviews
Review Load (0.2ms) SELECT "reviews".* FROM "reviews" WHERE "reviews"."subject_id" = ? [["subject_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy []>
Comment: subject_reviews is not a good name :), change it to your requirements.
I think you're looking for this query:
class User
def referenced_in
# this fetches you all the reviews that a user was referenced
Review.where("reviews.author_id = :user_id OR reviews.subject_id = :user_id", user_id: id).distinct
end
end
User.first.referenced_in #should give you all records a user was referenced
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.
I have 2 rails models, a security and stock_quote model, which are as follows
class StockQuote < ActiveRecord::Base
belongs_to :security, class_name: "Security", foreign_key: 'security_id'
end
class Security < ActiveRecord::Base
has_many :stock_quotes, dependent: :destroy
end
In the rails console when i try doing,
a = Security.create(security: "Goldman Sachs", category: "Investment Banking")
b = a.stock_quotes.first
c = b.security.create(security: "Facebook", category: "Tech")
The last query generates the following error
Security Load (0.3ms) SELECT "securities".* FROM "securities" WHERE "securities"."id" = ? LIMIT 1 [["id", 2]]
NoMethodError: undefined method `create' for #<Security:0xbbd2d78>
What I'm i doing wrongly because my associations are correctly defined
Instead c = b.security.create(security: "Facebook", category: "Tech") you need b.create_security(security: "Facebook", category: "Tech"). This similar to
b.security = Security.new;
b.security.save; b.security
Read doc for ActiveRecord::Base#belongs_to