Rails uniqueness constraint on two column values in different models - ruby-on-rails

I want to know the best way to have an uniqueness constraint enforced on two related model attributes in rails that are both no primary keys
class Parent > ApplicationRecord
has_many :children
:name
end
class Child > ApplicationRecord
:name
end
I want to enforce that (parent.name, child.name) is unique for every parent. e.g.
(parent1, child1) and (parent2, child1) is allowed
(parent1, child1) and (parent1, child1) is a violation
Ideally, I would enforce this in Postgres, however I have only seen the option to add uniqueness constraints on multiple columns of the same table.
Alternatively, I have written a custom validator for rails that does what I want, but this is cumbersome. There needs to be a better solution...
For completeness, here is the constraints validator which requires one to add a children function to a model returning the list of children.
class NamePairValidator < ActiveModel::Validator
def validate(record)
record.children.values.each do |model_children|
names = model_children.to_a.collect {|model| model.name}
if (names.select{|name| names.count(name) > 1 }.size > 0)
record.errors[:name] << 'Path leading to this resource has no unique name'
end
end
end
end
(in Parent.rb)
def children
{children: :children}
end
Migrations:
class CreateDomains < ActiveRecord::Migration[5.0]
def change
create_table :domains do |t|
t.string :name
t.string :domain_type
t.timestamps
end
end
end
class CreateSubjects < ActiveRecord::Migration[5.0]
def change
create_table :subjects do |t|
t.string :name
t.string :subject_type
t.timestamps
end
end
end
class CreateJoinTableDomainSubject < ActiveRecord::Migration[5.0]
def change
create_join_table :domains, :subjects do |t|
t.index [:domain_id, :subject_id]
t.index [:subject_id, :domain_id]
end
end
end

I just used similar one in my code
validates :child_name, uniqueness: { scope: :parent_id }
More..
(i) https://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_uniqueness_of
(ii) Validate uniqueness of multiple columns

Insipered by the-has-many-through-association of the official doc of ruby on rails:
class CreateAppointments < ActiveRecord::Migration[5.0]
def change
create_table :domains do |t|
t.string :name, null: false
t.string :domain_type
t.timestamps
end
create_table :subjects do |t|
t.string :name, null: false
t.string :subject_type
t.timestamps
end
create_table :fields do |t|
t.belongs_to :domain, index: true
t.belongs_to :subject, index: true
t.timestamps
end
end
end
Note
I took the initative to rename your model JoinTableDomainSubject by Field to be more readable.
I also force name field not be nil to check uniqueness.
(adding null: false in migrations files and validates :name, presence: true in both models)
Now the dedicated classes:
class Subject < ApplicationRecord
has_many :fields
has_many :domains, through: :fields
validates :name, presence: true
end
class Domain < ApplicationRecord
has_many :fields
has_many :subjects, through: :fields
validates :name, presence: true
end
class Field < ApplicationRecord
belongs_to :domain
belongs_to :subject
validate :domain_and_subject_names_uniqueness
private
def domain_and_subject_names_uniqueness
if class.includes(:domain, subject)
.where(domain: { name: domain.name }, subject: { name: subject.name })
.exists?
errors.add :field, 'duplicity on names'
end
end
end
Since the models are associated, I can use Field.first.domain to access Domain model of a given Field and vice versa.

Related

Foreign key does not exist when deleting dependent records

I have two models. The first one:
class Keyword < ActiveRecord::Base
has_many :words, dependent: :delete_all
end
The second:
class Word < ActiveRecord::Base
end
My migrations:
class Keywords < ActiveRecord::Migration
def change
create_table :keywords do |t|
t.string :name, null: false, unique: true
t.string :description, null: false
t.string :keys, null: false
t.timestamps null: false
end
end
end
And for the words:
class Words < ActiveRecord::Migration
def change
create_table :words do |t|
t.belongs_to :keyword
t.string :name, null: false
t.timestamps null: false
end
end
end
When I'm trying to delete the keywords instance Rails throws out the following exception:
PG::UndefinedColumn: ERROR: column words.keyword_id does not exist LINE 1: DELETE FROM "words" WHERE "words"."keyword_id" = $1 ^ : DELETE FROM "words" WHERE "words"."keyword_id" = $1
So my question is, why Rails created keyword_id reference to the table instead of using keywords_id? And how to fix it.
The code in your repository is actually different than migration code you posted. In your repo you have:
t.belongs_to :keywords
Change it to:
t.belongs_to :keyword
as you posted in the question and then recreate the database.
Firstly, you'll need to add a belongs_to relation to your Word model:
class Word < ActiveRecord::Base
belongs_to :keyword
end
Secondly, to create the column keyword_id for the foreign key, you'll need to use the add_reference method in your migration:
class Words < ActiveRecord::Migration
def change
create_table :words do |t|
t.string :name, null: false
t.timestamps null: false
end
add_reference :words, :keyword, index: true, foreign_key: true
end
end
If what you actually want is to have a many-to-many relation, you'll need to add a join table, i.e. KeywordsWords.
You need the following:
#app/models/keyword.rb
class Keyword < ActiveRecord::Base
has_many :words
end
#app/models/word.rb
class Word < ActiveRecord::Base
belongs_to :keyword
end
This will lead Rails to look for the keyword_id foreign key in the words table:
#words
id | keyword_id | name | created_at | updated_at
Looks like you need to change your words table to include the keyword_id foreign key:
$ rails g migration ChangeWordsAddKeywordID
#db/migrate/change_words_add_keyword_id______.rb
class ChangeWordsAddKeywordID
def change
rename_column :words, :keywords_ids, :keyword_id
## OR ##
create_column :words, :keyword_id, :integer
end
end
$ rake db:migrate

Rails amoeba gem doesn't copy many-to-many relation

I have a problem with copying database records. I have a simple model User, that contains one-to-many relation with Language model and many-to-many relation with Skill model. I wanted to use amoeba gem to copy records with all associations. One-to-many copying works fine, but many-to-many doesn't copy at all.
Here's the code of User and Skill model:
user.rb
class User < ActiveRecord::Base
belongs_to :language
has_and_belongs_to_many :skills
validates_presence_of :email, :name
validates :email,
presence: { with: true, message: "cannot be empty" },
uniqueness: { with: true, message: "already exists in database" }
amoeba do
enable
end
end
skill.rb
class Skill < ActiveRecord::Base
has_and_belongs_to_many :users
end
I have also migration files that crates users, skills and skills_users tables:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false
t.references :language
t.timestamps null: false
end
end
end
.
class CreateSkills < ActiveRecord::Migration
def change
create_table :skills do |t|
t.string :name
t.timestamps null: false
end
end
end
.
class AddUsersSkillsTable < ActiveRecord::Migration
def change
create_table 'skills_users', :id => false do |t|
t.column :user_id, :integer
t.column :skill_id, :integer
end
end
end
Controller action 'show' in users_controller looks like this:
def copy
#user_copy = #user.dup
if #user_copy.save(validate: false)
redirect_to action: "index"
end
end
I tried copying relations in user.rb like this, but it didnt work:
amoeba do
enable
clone [:skills]
end
What may cause the problem?
Ok, I found the mistake. I wrote
#user_copy = #user.dup
in the controller file instead of
#user_copy = #user.amoeba_dup

How to add foreign key in rails migration with different table name

How can I assign different table name with adding foreign key. for e.g
I have a model like
class MyPost < ActiveRecord::Base
has_many :comments, class_name: PostComment
end
class PostComment < ActiveRecord::Base
belongs_to :post, class_name: MyPost
end
Now i want to change my migration file like this:
class CreatePostComments < ActiveRecord::Migration
def change
create_table :post_comments do |t|
t.belongs_to :post, index: true
t.timestamps null: false
end
add_foreign_key :post, :class_name => MyPost
end
end
But it is not working. Migration is getting cancelled. How do I change my migration file to work with my model structure.
You can pass in options for the foreign key as following:
class CreatePostComments < ActiveRecord::Migration
def change
create_table :post_comments do |t|
t.references :post, foreign_key: { to_table: :my_posts }, index: true
t.timestamps null: false
end
end
end
This is also true for the index option if you like to add a unique constraint:
t.references :post, foreign_key: { to_table: :my_posts }, index: { unique: true}
By the way, references is an alias for belongs_to, or to be more exact, belongs_to is an alias for references.
See the details in the implementation rails 5.0.rc2 & rails 4.2
It should look like this:
class CreatePostComments < ActiveRecord::Migration
def change
create_table :post_comments do |t|
t.belongs_to :post, index: true
t.timestamps null: false
end
add_foreign_key :post_comments, :my_posts, column: :post_id
end
end
Take a look at the documentation: http://apidock.com/rails/ActiveRecord/ConnectionAdapters/SchemaStatements/add_foreign_key
You use the column option when the column is named differently.

ActiveRecord: find appropriate translation

I have models:
Category:
class Category < ActiveRecord::Base
has_many :entities, as: :resourcable
end
class CreateCategories < ActiveRecord::Migration
def change
create_table :categories do |t|
t.string :name
t.text :short_descr
t.text :full_descr
t.timestamps
end
end
end
Language:
class Language < ActiveRecord::Base
has_many :entities, as: :resourcable, dependent: :destroy
validates :code, uniqueness: true
end
class CreateLanguages < ActiveRecord::Migration
def change
create_table :languages do |t|
t.string :name
t.string :code
t.timestamps
end
end
end
And Entity:
class Entity < ActiveRecord::Base
belongs_to :language
belongs_to :resourcable, polymorphic: true
end
class CreateEntities < ActiveRecord::Migration
def change
create_table :entities do |t|
t.integer :language_id
t.string :name
t.text :short_descr
t.text :full_descr
t.references :resourcable, polymorphic: true
t.timestamps
end
end
end
In Categories there are default values for fields (short_descr, full_descr), In Entities there are translations for this fields. I need to render as json all Categories with appropriate translations: at first, I need to take Language with appropriate code (for example ru), next, I need to find all language Entities for this language, next, if Entity have filled short_descr and full_descr I need to render Category with this values, else I need to render the Category with default values (this values in Categories table). How to do this? I prefer ActiveRecord buy consider pure SQL.
EDITED
Now I'm trying to use gem 'squeel':
Language.joins{entities.category}.
select{coalesce(entities.short_descr, categories.short_descr)}.
where{languages.code == 'en'}
but it doesn't work (undefined methodshort_descr' for nil:NilClass`). There is the problem?
Entity.joins(:language, :category).
select('categories.*, coalesce(entities.short_descr, categories.short_descr) as short_descr,
coalesce(entities.full_descr, categories.full_descr) as full_descr').
where('languages.code = ?', 'en')

STI and has_many association with "type" column as Key

I am using Single Table Inheritance for managing different types of projects.
I decided to store some information associated with each project type. So i created new table "project_types" with "model_type" field as primary key. Primary key values are values of "type" field of "projects" table. Problem: When i trying to get associated with Project object ProjectTypes object it always returns null.
>> p = Project.find(:first)
=> #<SiteDesign id: 1, type: "SiteDesign", name: "1", description: "dddd", concept: "d", client_id: 40, created_at: "2009-10-15 08:17:45", updated_at: "2009-10-15 08:17:45">
>> p.project_type
=> nil
Getting projects associated with ProjectTypes project is OK. Is there way to make it works properly?
Models:
class Project < ActiveRecord::Base
belongs_to :project_type, :class_name => "ProjectTypes", :foreign_key => "model_name"
end
class SiteDesign < Project
end
class TechDesign < Project
end
class ProjectTypes < ActiveRecord::Base
self.primary_key = "model_name"
has_many :projects, :class_name => "Project", :foreign_key => "type"
end
Migrations:
class CreateProjectTypes < ActiveRecord::Migration
def self.up
create_table :project_types, :id => false do |t|
t.string :model_name , :null => false
t.string :name, :null => false
t.text :description
t.timestamps
end
add_index :project_types, :model_name, :unique => true
#all project types that are used.
models_names = {"SiteDesign" => "Site design",
"TechDesign" => "Tech design"}
#key for model_name and value for name
models_names.each do |key,value|
p = ProjectTypes.new();
p.model_name = key
p.name = value
p.save
end
end
def self.down
drop_table :project_types
end
end
class CreateProjects < ActiveRecord::Migration
def self.up
create_table :projects do |t|
t.string :type
t.string :name
t.text :description
t.text :concept
t.integer :client_id
t.timestamps
end
end
def self.down
drop_table :projects
end
end
Not surprising you're getting problems. By moving from a pure STI system to your current system you are horribly breaking the patterns you are using by intermingling parts of one with parts of another.
I'd personally go for something like:
class Project < ActiveRecord::Base
attr_readonly(:project_type)
belongs_to :project_type
before_create :set_project_type
def set_project_type()
project_type = ProjectType.find_by_model_name(this.class)
end
end
class SiteProject < Project
end
class TechProject < Project
end
class ProjectType < ActiveRecord::Base
has_many :projects
end
with migrations:
class CreateProjectTypes < ActiveRecord::Migration
def self.up
create_table :project_types do |t|
t.string :model_name , :null => false
t.string :name, :null => false
t.text :description
t.timestamps
end
add_index :project_types, :model_name, :unique => true
#all project types that are used.
models_names = {"SiteDesign" => "Site design",
"TechDesign" => "Tech design"}
#key for model_name and value for name
models_names.each do |key,value|
p = ProjectTypes.new();
p.model_name = key
p.name = value
p.save
end
end
def self.down
drop_table :project_types
end
end
class CreateProjects < ActiveRecord::Migration
def self.up
create_table :projects do |t|
t.string :type
t.references :project_type, :null => false
t.text :description
t.text :concept
t.integer :client_id
t.timestamps
end
end
def self.down
drop_table :projects
end
end
It just cleans things up and it also helps clarify what you're doing. Your 'ProjectType' table is purely for extra data, your inheritance tree still exists. I've also thrown in some checks to make sure your project type is always set (and correctly, based on the model name) and stops you from changing project type once it's been saved by making the attribute read only.

Resources