Rails model foreign key validation - ruby-on-rails

I need to validate the existence of a row referenced by a foreign key in one of my models. The situation is like this:
Project.rb
class Project < ActiveRecord::Base
has_one :project_category
# -----------------------------------------------------------------
# this does not work because the attribute is actually called
# 'category_id' instead of the rails expected 'project_category_id'
# -----------------------------------------------------------------
validates :project_category, presence: true
end
Project Migration
class CreateProjects < ActiveRecord::Migration
def change
create_table :projects do |t|
# ----------------------------------------------
# this is why the column is called 'category_id'
# ----------------------------------------------
t.references :category, references: :project_categories, null: false
# all of my other fields here, unimportant
end
add_foreign_key :projects, :project_categories, column: :category_id
end
end
I know that I can write a custom validation method to check if the :category_id exists in the project_categories table but I would prefer to let rails handle the validation if there is a way, so I can keep my code DRY.
EDIT
ProjectCategory.rb
class ProjectCategory < ActiveRecord::Base
belongs_to :project
validates :name, presence: true, uniqueness: { case_sensitive: false }
end
ProjectCategory Migration
class CreateProjectCategories < ActiveRecord::Migration
def change
create_table :project_categories do |t|
t.string :name, null: false
end
end
end

It appears you only need to add the foreign_key option to your has_one declaration in order to specify the custom column name you've specified i.e. category_id instead of project_category_id. See Options for has_one for details.
# app/modeles/project.rb
class Project < ActiveRecord::Base
has_one :project_category, foreign_key: 'category_id'
# -----------------------------------------------------------------
# this does not work because the attribute is actually called
# 'category_id' instead of the rails expected 'project_category_id'
# -----------------------------------------------------------------
validates :project_category, presence: true
end

Related

RAILS - CHANGE FIELD OF ANOTHER TABLE - BOOLEAN

Good afternoon. I'm new to rails and I'm using google translate to post in English here, so sorry if it's not very readable.
My question is, I have a User table, and a Setting table.
They are related (but I don't know if the relationship is correct), they can even confirm me, and I would like to know if:
when creating a user, I would like to automatically change the "email" and "push" fields of that user's settings table to true.
Would it be possible via a method that in the user model called: "setting_default"?
User model.
class User < ApplicationRecord
has_one :setting
before_save :setting_default
def setting_default
self.setting.update(:email, 'true')
self.setting.update(:push, 'true')
end
Setting Model
class Setting < ApplicationRecord
has_one :user
end
The Controller is normal, if you need it, I can put it in the post
My migration:
class CreateSettings < ActiveRecord::Migration[6.0]
def change
create_table :settings do |t|
t.boolean :email, default: true
t.boolean :push, default: true
t.timestamps
end
end
end
class AddSettingsToUser < ActiveRecord::Migration[6.0]
def change
add_reference :users, :setting, null: true, foreign_key: true
end
end
Google translate has worked well for you here.
First off you'll want to change your Setting model to belong to the User:
class Setting < ApplicationRecord
belongs_to :user
end
Your settings DB table is missing a user_id field to tie the setting back to the user. I'm not used to the add_reference technique so I just do things myself in the migrations. This would work:
class CreateSettings < ActiveRecord::Migration[6.0]
def change
create_table :settings do |t|
t.integer :user_id
t.boolean :email, default: true
t.boolean :push, default: true
t.timestamps
end
end
end
(Make note that your users DB table has a field setting_id that it does not need. I don't think it should be there. I would remove it. Unless it's a Rails 6 thing I'm not used to.)
Next it would probably be better to assign the values if the save succeeds (and not if it fails) so you'll want an after_save instead. And I'm simplifying your value assignment just in case you're having an issue there:
class User < ApplicationRecord
has_one :setting
after_save :setting_default
def setting_default
setting.email = true
setting.push = true
setting.save(validate: false)
end
private :setting_default
And to answer what seems to be your question, yes, what you're trying to do should be easily possible. This is a very common thing to do. It should work.
When you use one-to-one association you need to choose has_one in one and belongs_to in another model
Semantically user has one setting, but not setting has one user
So it's better to reverse them
To change your schema you need to write new migration
class ChangeOneToOneDirection < ActiveRecord::Migration[6.0]
def up
change_table :settings do |t|
t.belongs_to :user, foreign_key: true, null: false
end
User.where.not(setting_id: nil).find_each |user|
Setting.find(user.setting_id).update_columns(user_id: user.id)
end
change_table :users do |t|
t.remove :setting_id
end
end
def down
add_reference :users, :setting, null: true, foreign_key: true
Setting.find_each do |setting|
User.find(setting.user_id).update_columns(setting_id: setting.id)
end
change_table :settings do |t|
t.remove :user_id
end
end
end
After migration you can change User model
class User < ApplicationRecord
has_one :setting
after_commit :setting_default
private
def setting_default
setting&.update(email: true, push: true)
end
end
It's better to update associated model only if saves are in the database. And user can haven't setting. That's why after_commit and safe-navigator &

I want to be able to store values even if the related column is empty in rails

What we want to achieve
I'm doing the association in Rails, I'm associating the Schedule table with the PostItem table, and I'm associating the PostItem with belongs_to in the Schedule table, even if the PostItem_id associated with it in the Schedule table is empty. I would like to be able to save the file. In other words, I don't want to make the association every time; I always need the PostItem_id when I make the association within migration. Is there any way to allow saving even if the associated parent is empty?
Code
Migration File
class CreateSchedules < ActiveRecord::Migration[6.0]
def change
create_table :schedules do |t|
t.string :name
t.string :color
t.integer :start
t.integer :end
t.boolean :timed
t.boolean :long_time
t.integer :postItem_id # I want to save the file even if it is empty.
t.timestamps
end
end
end
Schedule Model
class Schedule < ApplicationRecord
belongs_to :post_item
validates :start, presence: true
validates :end, presence: true
# validates :postItem_id, allow_nill: true, allow_blank: true
end
postItem model
class PostItem < ApplicationRecord
belongs_to :post
has_many :schedules
end
After Rails 5 belongs_to associations are required by default. If you want it to be optional, you can add optional: true.
belongs_to :post_item, optional: true
You can also change this behaviour per model by doing:
self.belongs_to_required_by_default = false
https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#active-record-belongs-to-required-by-default-option

Querying many-to-many relationship filtering by many to many table but ordering by an associated table's field

I am trying to come up with a query for a many-to-many relationship with filtering for a certain field in the many-to-many table while ordering by a field in an associated table.
How do I get all the active firm_emps of a specific firm and order the firm_emps by user's name?
user.rb
Class User < ApplicationRecord
has_many :firm_emps, :dependent => :destroy
has_many :firms, through: :firm_emps
end
user.rb migration file
...
t.string :name
t.boolean :active
...
firm_emp.rb
Class FirmEmp < ApplicationRecord
belongs_to :firm
belongs_to :user
end
firm_emp.rb's migration file
...
t.belongs_to :user, index: true
t.belongs_to :firm, index: true
t.boolean :admin, default: false
t.boolean :active, default: true
...
firm.rb
Class Firm < ApplicationRecord
has_many :firm_emps, dependent: :destroy
has_many :users, through: :firm_emps
end
Firm.rb's migration file
...
t.string :full_name
t.boolean :active
...
I've tried the following queries in rails console:
f = Firm.first
f.users.where(active: true).order('users.name asc')
# But this filters on User's table field active: true and not the FirmEmps table field active: true
f.users.joins(:firmemps).where(active: true).order('users.name asc')
# Just doesn't work
f.firm_emps.active.order('firm_emps.active')
# But i can't order by user's field 'name'
EDIT:
#PragyaSriharsh's and #ArunEapachen's answers worked.
Try it:
f.users.joins(:firm_emps).where('firm_emps.active=?', true).order('users.name asc')
If it doesn't work use sort_by method.
Try the following.
f = Firm.first
f.users.where('firm_epms.active = ?' , true).order('users.name asc')

Rails - namespaced migrations

I have the following model and migration:
class Content::Panels::Iframe < Content::Panel
## Associations ##
belongs_to :panel_holder, polymorphic: true
## Validations ##
validates :uri, presence: true
## Methods ##
def self.plural_name
'iframe_index'
end
end
class AddHeightToIframes < ActiveRecord::Migration[5.1]
def change
add_column :iframes, :height, :integer, after: :headline
end
end
The migration fails as there is no table called 'iframes'. After googling I've tried adding to the iframe class:
self.table_name_prefix = 'content_panels_'
self.table_name = 'content_panels_iframes'
and changing the table title to 'content_panels_iframes', However neither of these work when tried independently or together.
What am I doing wrong? Thanks in advance
When the table name is content_panels_iframes, the migration should be:
add_column :content_panels_iframes, :height, :integer, after: :headline

Multiple associations to the same model in Rails

Let's say I have a model Dogs and each dog has exactly 2 Cat "enemies", enemy1 and enemy2, how do I write the migration file such that I can call dog.enemy1 to retrieve the first enemy and dog.enemy2 to retrieve the second enemy?
I tried this:
create_table :dog do |t|
t.string :name
t.timestamps null: false
end
add_index :dog, :name
add_foreign_key :dogs, :cats, column: :enemy1_id
add_foreign_key :dogs, :cats, column: :enemy2_id
end
I also tried it with the t.references method but could not get it to work. Been working on this problem for hours. and it works fine in development but not on Heroku Postgres.
The error i get is
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR:column "enemy1_id" referenced in foreign key constraint does not exist.
Any ideas?
Migration:
create_table :dog do |t|
t.string :name
t.integer :enemy1_id
t.integer :enemy2_id
t.timestamps null: false
end
Dog Model:
class Dog < ActiveRecord::Base
has_one :enemy1, class: 'Cat', foreign_key: :enemy1_id
has_one :enemy2, class: 'Cat', foreign_key: :enemy2_id
end
There is not a 'has_two' association in rails. So you should settle with has_many association.
class Dog < ActiveRecord::Base
has_many :cats, limit: 2
scope :enemy1, Proc.new { |object| object.cats.first }
scope :enemy2, Proc.new { |object| object.cats.last }
end
in class cat
class Cat < ActiveRecord::Base
belongs_to :dog
end
Now the migration for create cats should have
t.references :dog
You need not have foreign fields in your dog model. This should solve your problems.
The Rails Way should not be to do this in a migration. Migrations, in my opinion, is a tool to define the structure your data would live in.
The Rails Way would suggest you have a has_many association on the Cat class for Dogs
class Dog < ApplicationRecord
# ... other code ...
has_many :enemies, class_name: Cat
# ... other code ...
end
You'd also have to define the belongs to association in Cat
class Cat < ApplicationRecord
# ... other code ...
belongs_to :dog
# ... other code ...
end
Note that the cats table should be defined to have a reference to the dogs table. So your migration for the cats table should read something like
class CreateCats < ActiveRecord::Migration[5.x]
create_table :cats do |t|
# ... other code ...
t.references :dog
# ... other code ...
end
end
With these setup, you should then define enemy_one and enemy_two in your Dog class
class Dog < ApplicationRecord
# ... other code ...
def enemy_one
enemies.first
end
def enemy_two
enemies.second
end
# ... other code ...
end
To make things even stricter, you may decide to add a validation (on create) which checks that only two Cats are ever created as enemies per Dog. Moreso, you could hide the creating button (or general access) when a Dog has reached its limit for enemies. This, I leave to your discretion.
p.s: All these constrictions could also be done in the database layer. But the application layer is here to abstract all that out.
I just solved it independently, 2 seconds after the first answer came in..
Finally.
class CreateDogs < ActiveRecord::Migration
def change
create_table :dogs do |t|
t.string :name
t.references :enemy1, index: true
t.references :enemy2, index: true
t.timestamps null: false
end
add_index :dogs, :name
add_foreign_key :dogs, :cats, column: :enemy1_id
add_foreign_key :dogs, :cats, column: :enemy2_id
end

Resources