Rails find method didn't use my foreign key - mysql

I have the following two Models:
class TopicContent < ActiveRecord::Base
unloadable
belongs_to :topic
end
and
class Topic < ActiveRecord::Base
unloadable
has_one :topic_content
accepts_nested_attributes_for :topic_content
end
And the following show action, which get the :id from the selected topic:
def show
#text = TopicContent.find(params[:id])
end
The problem is, that the find method always take the primary-key(id) instead of foreign-key (topic_id) from the TopicContent table.
Is there something wrong with my defined associations?

.find(primary_key) always retrieves the records from database based on primary key.
Use .find_by(conditions) instead as it finds the first record matching conditions passed to it.
For eg:
#text = TopicContent.find_by(topic_id: params[:id])

You need to find the TopicContent via the Topic.
def show
#topic = Topic.find(:topic_id)
#text = #topic.topic_content
end
This is assuming you have your routes set up as well.

Related

Rails query with has_many relation in a list of [types, ids]

I have models with variables (many model classes : polymorphic relation), and constraints between variables (variables are not necessarily in the same model).
I try to make a query to find all constraints associated to a list of models (with all vars associated to the models in list), and I really don't know how to do it.
My models looks like this.
class Model1 < ApplicationRecord
has_many :vars, as: :model
end
class Model2 < ApplicationRecord
has_many :vars, as: :model
end
class Var < ApplicationRecord
belongs_to :model, polymorphic: true
# model_type and model_id in vars table
has_many :cns_vars
has_many :constraints, through: :cns_vars
end
class_CnsVar < ApplicationRecord
belongs_to :var
belongs_to :constraint
end
class Constraint < ApplicationRecord
has_many :cns_vars
has_many :vars, through: :cns_vars
end
To find constraints related to one model I have this query :
Constraint.includes(:vars).where(active: true, vars: {model_id: model.id, model_type: model.class.to_s})
This query give me the constraints that have at least one var associated to my model.
I need constraints with all vars associated to a list of models.
Is there a way to make the same query, but with all vars associated to the model ?
Is there a way to make the same query, but with all vars associated to a list of models ?
Constraint.includes(:vars).where(active: true, vars: {*[var.model_type, var.model_id] in my models list*})
Is there a solution to do this with one query ?
Or do I have to do it another way ?
Thanks for your help.
(ruby : 2.6.0 / rails : 5.2.3)
EDIT :
To give better explanation, look at this function that returns what I need, but this make too much queries !
def constraints_for_models_list(models)
all_vars = models.flat_map(&:vars)
all_constraints = all_vars.flat_map(&:constraints)
all_constraints.uniq!
constraints = []
all_constraints.each do |constraint|
next unless constraint.vars.included_in?(all_vars)
constraints << constraint
end
return constraints
end
Constraint.includes(:vars).where(active: true).where.not(vars: { model: nil })
of course if I correctly get the point of what you're trying.
for what you asked in comment:
Constraint.includes(:vars).where(active: true).where('vars.model_type IN
?', ['Model1',Model2'])

Rails Active Record: How to join three tables?

I've three tables as shown below:
Advertiser model:
class Advertiser < ActiveRecord::Base
has_many :advertisers_account_groups
AdvertisersAccountGroup model
class AdvertisersAccountGroup < ActiveRecord::Base
belongs_to :advertiser
belongs_to :v2_account_account_group, class_name: 'V2Account::AccountGroup', foreign_key: 'account_group_id'
I wanna know which advertiser belongs to v2_account_account_groups
and wanna get v2_account_account_groups.name
Desired Output:
What I tried;
Advertiser.where(media_type: "line").joins(advertisers_account_groups,v2_account_account_groups)
But it doesn't work
It looks to me that your current setup uses AdvertisersAccountGroup as a join table; therefore, I'd suggest using a has_many :through association.
To do this, you'd just need to switch up the models as follows:
class Advertiser < ActiveRecord::Base
has_many :v2_account_account_groups, through: :advertisers_account_groups
has_many :advertisers_account_groups
...
end
class V2Account::AccountGroup < ActiveRecord::Base
has_many :advertisers, through: :advertisers_account_groups
has_many :advertisers_account_groups
...
end
class AdvertisersAccountGroup < ActiveRecord::Base
belongs_to :advertiser
belongs_to :v2_account_account_group, class_name: 'V2Account::AccountGroup', foreign_key: 'account_group_id'
...
end
This will allow you to query against the advertiser as desired, i.e. advertiser.v2_account_account_groups.
However, this association is a many-to-many between advertisers and v2 account groups as is - therefore, you won't be able to call advertiser.v2_account_account_groups.name as advertiser.v2_account_account_groups returns a collection rather than a single record.
You could use advertiser.v2_account_account_groups.map(&:name) (to get an array of all groups' names) or advertiser.v2_account_account_groups.first&.name, but it sounds as if you might need to restructure the data if an advertiser should have just the one v2 account group.
Does that make sense and sound like what you're looking for? Let me know if you've any questions.
Edit:
Based on your comment, I think you should be able to construct a query as follows:
Advertiser.includes(advertiser_account_groups: : v2_account_account_group)
.where(advertiser_account_groups: { v2_account_groups: { name: "something" } })
Does that sound like what you're looking for?
A couple of things to note:
when referencing the associations in the includes, you want to use the association name
however, when plugging these into the where clause, you need to use the full table names, as they are in the databases (searchable via Model.table_name)
Also, in your comment, you reference adding media_type: "line", which the below also includes:
Advertiser.includes(advertiser_account_groups: : v2_account_account_group)
.where(media_type: "line", advertiser_account_groups: { v2_account_account_groups: { name: "something" } })
Probably the best way to structure this in your code is as a scope in your advertiser model, such as:
scope :by_v2_group_name, -> (name) { includes(advertiser_account_groups: :v2_account_account_group)
.where(media_type: "line", advertiser_account_groups: { v2_account_account_groups: { name: "something" } }) }
or
scope :by_v2_group_name, (lambda do |name|
includes(advertiser_account_groups: :v2_account_account_group)
.where(media_type: "line", advertiser_account_groups: { v2_account_account_groups: { name: "something" } })
end)
That will then allow you to keep your code clean and call Advertiser.by_v2_group_name("something").
Let me know how you get on with that and we'll work on it as needed :)
This answer is relevant only if you don't need a model for the join table
advertiser_account_gourps
(if you don't have any extra attributes other then advertiser_id and account_gourp_id on that join table - you usually don't need it)
While #SRack's answer is using the has_many :through association, I think you can use a simple many-to-many association (has_and_belongs_to_many) because in your images i can see that the join table only have the ids of the associated tables (Unless you do need the id column -> and in that case, use #SRack's solution!)
class Advertiser < ActiveRecord::Base
has_and_belongs_to_many :advertisers_account_groups
class AccountGroup < ActiveRecord::Base
has_and_belongs_to_many :advertisers
then you can get them using:
advertiser = Advertiser.first
advertiser.account_groups #to get account groups of that advertiser
and vice versa:
account_group = AccountGroup.first
account_group.advertisers # to get advertisers of that account group
#SRack's comment is correct! i changed it to HABTM association!
In that case you will need to create a migration to make the join table:
rails g migration CreateJoinTableAdvertiserAccountGroup advertiser account_group
This table will only include the associated advertiser_id and the account_group_id.
You don't need to create a model and handle stuff on that table, rails will fill that table for you.

Auto-create `belongs_to` record

Similar questions have been asked before but I just can't figure this out.
So I have Model_A and Model_B. Model_B belongs_to Model_A. What I want to do is when I create Model_A is to automatically call the create method for Model B. Then a script takes over a generates a bunch of data for Model_B. I use after_create because this only has to happen once.
It needs done this way. If you wanna know the details feel free to ask...
So I got Model_A here. I just can't seem to get the right syntax in create_model_b. In the example I used I just get an error saying the method doesn't exist for Model_A.
class Model_A < ActiveRecord::Base
belongs_to :Model_B
after_create :create_model_b
...
def create_model_b
#so I tried a bunch of stuff here but none of it worked
#I need to create a Model_B which will contain the current Model_A id
#ex. self.model_b.create(model_a_id: self.id)
end
end
Model_B doesn't do anything special really:
class Model_B < ApplicationController
def create
#model_b = Model_B.new(model_b_params)
create_the_data
respond_to do |format|
if #model_b.save
#redirect
else
#uh oh
end
end
end
end
Thanks!
Two ways:
1.
class Model_A < ApplicationController
def create
#model_a = ModelA.new(model_a_params)
if #model_a.save
ModelB.create(model_a_id: #model_a.id, .....)
#create data for model B either here or with after_create (of model B)
redirect_to somewhere_awesome_path
else
# rescue error
render 'new'
end
end
2.
class Model_A < ActiveRecord::Base
after_create :create_model_b
...
def create_model_b
ModelB.create(model_a_id: id)
end
end
Your Model A contains half of an association: belongs_to :Model_B
Your Model B is missing its association to Model A.
Depending on the relationship you set up, you can complete the association with a has_one :Model_A, or has_many :Model_A, for example.
Here's Active Record documentation for reference.

Rails 4: ActiveRecord or MySQL query where no related models have attribute

Having a tough time with this one. I have a Job model, and a JobStatus model. A job has many statuses, each with different names (slugs in this case). I need an 'active' method I can call to find all jobs where none of the associated statuses has a slug of 'dropped-off'.
class Job < ActiveRecord::Base
belongs_to :agent
has_many :statuses, :class_name => "JobStatus"
validates :agent_id,
:pickup_lat,
:pickup_lng,
:dropoff_lat,
:dropoff_lng,
:description,
presence: true
class << self
def by_agent agent_id
where(agent_id: agent_id)
end
def active
#
# this should select all items where no related job status
# has the slug 'dropped-off'
#
end
end
end
Job Status:
class JobStatus < ActiveRecord::Base
belongs_to :job
validates :job_id,
:slug,
presence: true
end
The closest I've gotten so far is:
def active
joins(:statuses).where.not('job_statuses.slug = ?', 'dropped-off')
end
But it's still selecting the Job that has a dropped-off status because there are previous statuses that are not 'dropped-off'. If i knew the raw sql, I could probably work it into activerecord speak but I can't quite wrap my head around it.
Also not married to using activerecord, if the solution is raw SQL that's fine too.
Job.where.not(id: JobStatus.where(slug: 'dropped-off').select(:job_id))
will generate a nested subquery for you.
Not the cleanest method, but you could use two queries.
# Getting the ID of all the Jobs which have 'dropped-off' JobStatuses
dropped_off_ids = JobStatus.where(slug: 'dropped-off').pluck(:job_id)
# Using the previous array to filter the Jobs
Job.where.not(id: dropped_off_ids)
Try this:
def active
Job.joins(:statuses).where.not('job_statuses.slug' => 'dropped-off')
end
or this:
def active
Job.joins(:statuses).where('job_statuses.slug != ?', 'dropped-off')
end
I think you may want to reevaluate your data model somewhat. If the problem is that you're turning up old statuses when asking about Job, you likely need to have column identifying the current status for any job, i.e. job.statuses.where(current_status: true)
Then you can very easily grab only the rows which represent the current status for all jobs and are not "dropped-off".
Alternatively, if I'm misunderstanding your use case and you're just looking for any job that has ever had that status, you can just go backwards and search for the status slugs first, i.e.
JobStatus.where.not(slug: "dropped-off").map(&:job)

Rails 3 ActiveRecord: Order by count on association

I have a model named Song. I also have a model named Listen. A Listen belongs_to :song, and a song :has_many listens (can be listen to many times).
In my model I want to define a method self.top which should return the top 5 songs listened to the most. How can I achieve that using the has_many relation?
I'm using Rails 3.1.
Thanks!
Using named scopes:
class Song
has_many :listens
scope :top5,
select("songs.id, OTHER_ATTRS_YOU_NEED, count(listens.id) AS listens_count").
joins(:listens).
group("songs.id").
order("listens_count DESC").
limit(5)
Song.top5 # top 5 most listened songs
Even better, use counter_cache which will be faster because you'll only because using one table in your query
Here is your song class:
class Song < ActiveRecord::Base
has_many :listens
def self.top
order('listens_count DESC').limit(5)
end
end
Then, your listen class:
class Listen < ActiveRecord::Base
belongs_to :song, counter_cache: true
end
Make sure you add a migration:
add_column :comments, :likes_count, :integer, default: 0
Bonus points, add test:
describe '.top' do
it 'shows most listened songs first' do
song_one = create(:song)
song_three = create(:song, listens_count: 3)
song_two = create(:song, listens_count: 2)
popular_songs = Song.top
expect(popular_songs).to eq [song_three, song_two, song_one]
end
end
Or, if you want to go with the above method, here it is a bit more simply, and using a class method rather than scope
def self.top
select('comments.*, COUNT(listens.id) AS listens_count').
joins(:listens).
group('comments.id').
order('listens_count DESC').
limit(5)
end
For rails 4.x try this if your rows without any association matters:
scope :order_by_my_association, lambda {
select('comments.*, COUNT(listens.id) AS listens_total')
.joins("LEFT OUTER JOIN listens ON listens.comment_id = comments.id")
.group('comments.id')
.order("listens_total DESC")
}