I've got two models with optional relations has_many - belongs_to, like below:
class Journey < ApplicationRecord
has_many :activities, dependent: :destroy, foreign_key: 'cms_journey_id'
end
class Activity < ApplicationRecord
belongs_to :journey, optional: true, foreign_key: 'cms_journey_id'
end
As you see the relation is based on non-standard foregin_key name (both models are linked with each other by record called cms_journey_id). After I added foreign_key: 'cms_journey_id' I'm getting Rubocop error in both models:
Rails/InverseOf: Specify an `:inverse_of` option
If you don't explicitly specify the inverse relationships, Rails does its best to infer the inverse association on a model (at least for has_many, has_one, and belongs_to associations) using the class names as the basis for its guesses.
But anytime you use a scope or set non-standard naming, you need to explicitly tell Rails how to navigate the associations using inverse_of. In your case:
class Journey < ApplicationRecord
has_many :activities, dependent: :destroy,
foreign_key: 'cms_journey_id',
inverse_of: :journey
end
class Activity < ApplicationRecord
belongs_to :journey, optional: true,
foreign_key: 'cms_journey_id',
inverse_of: :activities
end
For future reference, the Rubocop documentation on individual cops is generally good and clear, and includes examples of 'good' and 'bad'. Just search for the cop name (e.g. 'Rails/InverseOf') and you're in good shape.
Related
I have the following models linked through a has_many :through associaton. Below is the following models
class Campaign
has_many :email_notification_code_percentages, dependent: :destroy
has_many :email_notification_code_percentage_trackers, through: :email_notification_code_percentages
end
class EmailNotificationCodePercentage < ApplicationRecord
belongs_to :campaign
has_many :email_notification_code_percentage_trackers, dependent: :destroy
end
class EmailNotificationCodePercentageTracker < ApplicationRecord
belongs_to :email_notification_code_percentage
end
When I try execute the following #campaign.email_notification_code_percentage_trackers.destroy_all I get the following error:
Cannot modify association 'Campaign#email_notification_code_percentage_trackers' because the source reflection class 'EmailNotificationCodePercentageTracker' is associated to 'EmailNotificationCodePercentage' via :has_many. (ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection)
What is the issue here? This should be a simple has many through association as defined through the rails examples. What am I missing here? This looks correct to me.
This is the expected behavior, as you can read in the API docs (https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html):
An important caveat with going through has_one or has_many
associations on the join model is that these associations are
read-only.
Associations with the :through option or defined via the has_and_belongs_to_many macro are a bit special.
So, a couple of alternative ways:
Iterate over each elem:
# be careful if the association is very large, you can run into some memory or timeout issues
#campaign.email_notification_code_percentage_trackers.each(&:destroy)
Make the query a bit different to be able to use the destroy_all:
EmailNotificationCodePercentage.merge(#campaign.email_notification_code_percentage_trackers).destroy_all
I have a model CompanyIntro which has two references to a Company:
class CompanyIntro < ApplicationRecord
belongs_to :company_one, class_name: "Company", foreign_key: "company_one_id"
belongs_to :company_two, class_name: "Company", foreign_key: "company_two_id"
...
I would like to do something like:
class Company < ApplicationRecord
has_many :company_intros, class_name: 'CompanyIntro', foreign_key: 'company_one_id'
has_many :company_intros, class_name: 'CompanyIntro', foreign_key: 'company_two_id'
...
But this is not valid
In my Company model, how to I create a has_many for both foreign keys? I am using Rails 6 which dos not allow custom sql for has_many (afaik). I also do not want to write a custom company_intros method on the Company model as I'm using another gem which looks for my has_many relationships.
You can't define has_many assocations where the foreign key is one of two columns. Its just not supported by ActiveRecord as the feature would add tons of complexity.
Using the same name for two assocations also just overwrites the previous assocation. If you want to have a single assocation here you need to add a join table.
class Company < ApplicationRecord
has_many :company_intro_participations
has_many :company_intros, through: :company_intro_participations
end
# for lack of a better name
class CompanyIntroParticipation < ApplicationRecord
belongs_to :company
belongs_to :company_intro
end
class CompanyIntro < ApplicationRecord
has_many :company_intro_participations
has_many :companies, through: :company_intro_participations
end
The alternative is creating a method which joins on company_one_id = companies.id OR company_two_id = companies.id but you will not be able to use that in the same way as an association when it comes to stuff like eager loading.
I have three activerecord models:
the "A"
class User < ApplicationRecord
has_many :user_violation, class_name: "UserViolation", inverse_of: :user
has_many :violations, through: :user_violation, class_name: "Violation"
end
the middle:
class UserViolation < ApplicationRecord
belongs_to :user, class_name: "User", foreign_key: :user_id
belongs_to :violation, class_name: "Violation", foreign_key: :violation_id
end
the B:
class Violation < ApplicationRecord
end
I need to find all users who have AT LEAST one violation with column: fatal set to true.
Im kinda stuck here and this is not working:
User.joins(:violations).where('violations.fatal = true')
To using query filter conditon in ORM I think this syntax should be:
User.joins(:violations).where(violations: {fatal: 'true'})
I think #Ninh Le's answer is right,(so did yours!) maybe you can simplify your models' code first to try to find where the problem is.
For example, I notice you missed the 's' in the has_many relation:
class User < ApplicationRecord
has_many :user_violations
has_many :violations, through: :user_violations
end
class UserViolation < ApplicationRecord
belongs_to :user
belongs_to :violation
end
class Violation < ApplicationRecord
# has_many :user_violations
# has_many :violations, through: :user_violations
end
I think it should work, if it's still not, check if there is record that has violation.fatal == true.
Then add the options of has_many, belongs_to if there 'really' needed.
For the most efficient approach to finding these users, you'd want what the database people would call a "semi-join" - an exists subquery.
Assuming that "fatal" is defined by a fatal column having a value of true
User.
where(
Violation.
where(fatal: true).
joins(:user_violations).
where(
UserViolation.arel_table[:user_id].eq(User.arel_table[:id])
).arel.exists
)
I'd suggest adding a scope to Violation that defines fatal without needing a where clause in that.
A report_template has_many report_template_columns, which each have a
name and an index attribute.
class ReportTemplateColumn < ApplicationRecord
belongs_to :report_template
validates :name, presence: true
end
class ReportTemplate < ApplicationRecord
has_many :report_template_columns, -> { order(index: :asc) }, dependent: :destroy
accepts_nested_attributes_for :report_template_columns, allow_destroy: true
end
The report_template_columns need to be ordered by the index column. I'm applying this with a scope on the has_many association, however doing so causes the following error:
> ReportTemplate.create!(report_template_columns: [ReportTemplateColumn.new(name: 'id', index: '1')])
ActiveRecord::RecordInvalid: Validation failed: Report template columns report template must exist
from /usr/local/bundle/gems/activerecord-5.1.4/lib/active_record/validations.rb:78:in `raise_validation_error'
If I remove the scope the same command succeeds.
If I replace the order scope with where scope that command fails in the same way, so it seems to be the presence of the scope rather than the use of order specifically.
How can I apply a scope to the has_many without breaking the nested creation?
I believe you need the :inverse_of option added to the has_many association.
class ReportTemplate < ApplicationRecord
has_many :report_template_columns, -> { order(index: :asc) },
dependent: :destroy, inverse_of: :report_template
end
The api states that :inverse_of:
Specifies the name of the belongs_to association on the associated object that is the inverse of this has_many association. Does not work in combination with :through or :as options. See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
I also like how the cocoon gem words their reason for using it:
Rails 5 Note: since rails 5 a belongs_to relation is by default required. While this absolutely makes sense, this also means associations have to be declared more explicitly. When saving nested items, theoretically the parent is not yet saved on validation, so rails needs help to know the link between relations. There are two ways: either declare the belongs_to as optional: false, but the cleanest way is to specify the inverse_of: on the has_many. That is why we write: has_many :tasks, inverse_of: :project
Could somebody explain me is my code correct.
I'm trying to get foreign_key option in rails associations.
I have 2 models:
Book and Author
Book db schema:
name
user_id
Author db schema:
name
My models:
class Author < ApplicationRecord
has_many :books, foreign_key: :user_id
end
class Book < ApplicationRecord
belongs_to :author, foreign_key: :user_id
end
Here I don't understand why we should define foreign_key in both models. Is it necessarily?
If you have used the table and column names that Rails expects, then you do not need to explicitly define the foreign_key. In your case, if the foreign key column was named author_id, then you could get by quite simply:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
end
However, in your case, the foreign key column is not named according to what Rails expects, so you have needed to explicitly define the foreign key column name. That's fine, but it does make a little more work for you.
In cases where you have explicitly defined the foreign key, you should define it for both associations. Your has_many association will not work without it.
In addition, you should define the inverse association:
class Author < ApplicationRecord
has_many :books, foreign_key: :user_id, inverse_of: :author
end
class Book < ApplicationRecord
belongs_to :author, foreign_key: :user_id, inverse_of: :books
end
Defining the inverse_of can cause ActiveRecord to make fewer queries, and gets rid of a few surprise behaviors. For an explanation of inverse_of, see Exploring the :inverse_of Option on Rails Model Associations by Ryan Stenberg