ruby on rails specifying uniqueness in db across multiple columns - ruby-on-rails

I have a model as follows:
class EntityTag < ActiveRecord::Base
attr_protected :user_id, :post_id, :entity_id
belongs_to :user
belongs_to :post
belongs_to :entity
validates :user_id, :presence => true
validates :entity_id, :presence => true
validates :post_id, :presence => true
end
I want to guard against multiple rows which have the same combination of user_id, entity_id, and post_id (e.g. a unique ID for a row is all three of those values).
What's the easiest way I can communicate that to ActiveRecord?

As #dhruvg mentioned:
validates_uniqueness_of :user_id, :scope => [:entity_id, :post_id]
Do note that uniqueness validation on model level does NOT guarantee uniqueness in the DB. To have that, you should put a unique index on your table.
Add the following to your migrations.
add_index :entity_tags, [:user_id, :post_id, :entity_id], :unique => true

I would check for this in the create action of your controller.
EntityTag.where(["user_id = ? and entity_id = ? and post_id = ?",
params[:user_id], params[:entity_id], params[:post_id]]).all
will return an Array of any existing record that have those same values. If Array.count == 0, then you can continue to save the newly created object as normal. Otherwise you can either return the existing record or throw an error; it's up to you.

Related

How to make sure that relation is unique from both sides of many to many

I have a model that acts as a many to many relation. Class name is RelatedDocument which is self explanatory, I basically use it to related Document class instances with one to another.
I ran into issues with validations so for example I have this in RelatedDocument class:
validates :document, presence: true, uniqueness: { scope: :related_document }
validates :related_document, presence: true
This works I m unable to create duplicate document_id/related_document_id row. However if I wanted to make this unique from the other side of the relation and change the validation to this :
validates :document, presence: true, uniqueness: { scope: :related_document }
validates :related_document, presence: true, uniqueness: { scope: :document }
This does not work the same from the other side. I was writing rspec test when I noticed this. How can I write validation or a custom validation method that prevents saving of the same id combination, no matter from which side they are?
Update
Per first comment in the comment section saying that the first uniqueness validation will take care of the boths sides, I say it doesn't simply because my rspec test fails, here are they :
describe 'relation uniqueness' do
let!(:base_doc) { create(:document) }
let!(:another_doc) { create(:document) }
let!(:related_document) { described_class.create(document: another_doc, related_document: base_doc) }
it 'raises ActiveRecord::RecordInvalid, not allowing duplicate relation links' do
expect { described_class.create!(document: another_doc, related_document: base_doc) }
.to raise_error(ActiveRecord::RecordInvalid)
end
it 'raises ActiveRecord::RecordInvalid, not allowing duplicate relation links' do
expect { described_class.create!(document: base_doc, related_document: another_doc) }
.to raise_error(ActiveRecord::RecordInvalid)
end
end
Second test fails.
In case you'd like to consider an alternative to the custom validation that has already been provided, you could create reciprocal relationships each time a new record is added and use your existing validation.
For example, when I say that "Document A is related to Document B", I would then also insert a record stating that "Document B is related to Document A". You can keep your validations simple, and can more easily implement logic in the future for when you don't want a reciprocal relationship in some cases (perhaps the relationship is only reciprocated if the documents are from a different author).
Here is an untested example of the kind of model callbacks you would implement:
create_table :documents do |t|
t.string :title, null: false
end
create_table :related_documents do |t|
t.integer :source_document_id, null: false
t.integer :related_document_id, null: false
end
add_index :related_documents, [:source_document_id, :related_document_id], unique: true
add_foreign_key :related_documents, :documents, column: :source_document_id
add_foreign_key :related_documents, :documents, column: :related_document_id
class Document < ActiveRecord::Base
# We have many document relationships where this document is the source document
has_many :related_documents, foreign_key: :source_document_id
validates :title,
presence: true
end
class RelatedDocument < ActiveRecord::Base
after_create :add_reciprocal_relationship
after_destroy :remove_reciprocal_relationship
belongs_to :source_document, class_name: Document
belongs_to :related_document, class_name: Document
validates :source_document,
presence: true
validates :related_document,
presence: true,
uniqueness: {
scope: :source_document
}
private
# Creates a reciprocal relationship
def add_reciprocal_relationship
RelatedDocument.find_or_create_by(
related_document: self.source_document,
source_document: self.related_document
)
end
# Safely removes a reciprocal relationship if it exists
def remove_reciprocal_relationship
RelatedDocument.find_by(
related_document: self.source_document,
source_document: self.related_document
)&.destroy
end
end
A custom validator should work here
validate :unique_document_pair
def unique_document_pair
if RelatedDocument.exists?(:document => [self.document,self.related_document], :related_document => [self.document, self.related_document])
errors.add :base, "error"
end
end

How can I do search category.name by using elasticsearch in Rails?

My search working fine.
But I have to type "1" or "2" to get results of "roommate" or "sublet".
Model has a column called category_id which is an integer.
Model Category has column :name which is a string.
Thus, I have category_id 1 is having "roommate" and 2 is "sublet"
below is my Housing model:
class Housing < ActiveRecord::Base
extend FriendlyId
friendly_id :title, use: :slugged
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
belongs_to :user
belongs_to :category
validates :title, :presence => true
validates :category_id, :presence => true
validates :created_at, :presence => false
validates :user_email, :presence => true
validates :description, :presence => false
validates_length_of :title, :maximum => 30
def self.search(query)
__elasticsearch__.search(
{
query: {
# multi_match: {
simple_query_string: {
query: query,
fields: ['title^10', 'category_id']
}
}
}
)
end
end
How can I fix fields: ['title^10', 'category_id'] So user can search "roommate" instead of must search integer "1" to get result of roommate ?
I tried fields: ['title^10', 'category.name'] but not working.
fields: ['title^10', 'category.name'] won't work unless you have correct mapping defined. Elasticsearch doesn't know about your associations. ES is a document store and searches for records using it's own document store. So unless you add your category name to the document stored in ES, you won't be able to search it.
TL;DR
Define a mapping. Example:
mapping dynamic: 'strict' do
indexes :category do
indexes :name
end
indexes :title
end
Here category will now be stored as nested object inside your index and hence is searchable using category.name

Active Admin Scopes Not working 1.0.0.pre2

I'm using active admin with ActiveRecord scopes. However, I'm having an issue with adding the scopes.
Running ruby 2.2.1p85 (2015-02-26 revision 49769) [x86_64-linux] and
Rails 4.2.5.1
#app/model/accounts.rb
class Account < ActiveRecord::Base
searchkick
belongs_to :program
belongs_to :insurance
has_many :notes
scope :program_name, -> (program) {where(program_name: adult) }
validates :first_name, :last_name, :address, :phone, presence: true
validates :phone, format: { with: /\A\d{3} \d{3}-\d{4}\z/,
message: "must be in the format 123 456-7890" }
end
I want to be able to us this in app/admin/account.rb
#app/admin/account.rb
ActiveAdmin.register Account do
menu :priority => 2
permit_params :first_name, :last_name, :return_client, :program_id, :insurance_id, :address, :phone
index do
column :first_name
column :last_name
column :address
column :phone
column :created_at
column :return_client
column :program
column :insurance
actions
end
scope :all, :default => true
scope :adult, default: true do |accounts|
accounts.program_name('adult')
end
end
I tired using it with and without block. I want the total count of "programs" in that scope as an end result.
You can't have two default scopes and the scope :all is unnecessary so remove it.
You have this scope which looks fine
scope :program_name, -> (program) {where(program_name: adult) }
and you say that
I want to be able to us this in app/admin/account.rb
but you aren't actually using it. You are instead trying to use
scope :adult, default: true do |accounts|
accounts.program_name('adult')
end
So just add it
scope :program_name
But your question seems to be loaded with something else you're trying to do
I want the total count of "programs" in that scope as an end result.
And in that ^ sense, I think you may be misunderstanding how and what scopes are actually used for.

Rails 4 - How can I map 2 columns of a Model to another column of another model

I want to map 2 columns of the same model (dev_status and test_planning_status) to another model's column (Status.name) and in the UserStory form I want to have a dropdown with values from Status table
I have tried something like this but unable to figure out
Status model is like this
class Status < ActiveRecord::Base
has_many :dev_status, :class_name => 'UserStory', :foreign_key => 'dev_status_id'
has_many :test_planning_status, :class_name => 'UserStory', :foreign_key => 'test_planning_status_id'
end
Currently I have this in models/UserStory
class UserStory < ActiveRecord::Base
validates :us_number, presence: true
validates :team_id, presence: true
validates :dev_status, presence:true
validates :test_status, presence:true
belongs_to :team
CreateUserStories migration is
class CreateUserStories < ActiveRecord::Migration
def change
create_table :user_stories do |t|
t.string :us_number
t.references :team
t.string :dev_status
t.string :test_planning_status
t.integer :tc_count
t.timestamps null: false
end
add_foreign_key :user_stories, :pod
end
My UserStoryController params is
def user_story_params
params.require(:user_story).permit(:us_number, :team_id, :dev_status, :test_planning_status)
end
UserStory _form is
<%= f.label :dev_status,'Dev Status' %>
<%= f.select :status, Status.all.map {|u|[u.status, u.id]},
{include_blank: true} %>
<%= f.label :test_planning_status, 'Test Planning Status' %>
<%= f.select :status, Status.all.map {|u|[u.status, u.id]},
{include_blank: true} %>
The goal should be to call UserStory.dev_status.name to get the dev_status name, and UserStory.test_planning_status.name to get the test_planning_status name.
Your migration should be creating columns dev_status_id (not dev_status) and test_planning_status_id (not test_planning_status).
Use t.references or t.belongs_to in your future migrations.
Above columns should be integers, not strings.
You need to specify belongs_to on the UserStory object for your status fields.
belongs_to :dev_status, class_name: 'Status'
belongs_to :test_planning_status, class_name: 'Status'
Change
validates :test_status, presence:true
to
validates :test_planning_status, presence:true
The two f.select :status in your form need to be changed to f.select :test_planning_status and f.select :dev_status
That should get you pointed in the right direction. Hope it helps!
This sounds like a standard has_many relationship::
class Status < ActiveRecord::Base
# columns id | name | value | other | information | created_at | updated_at
has_many :user_stories
end
class UserStory < ActiveRecord::Base
# columns id | title | value | dev_status_id | test_planner_status | created_at | updated_at
belongs_to :dev_status, class_name: :status
belongs_to :test_planning_status, class_name: :status
end
This would give you the ability to access the following:
#app/controllers/statuses_controller.rb
class UserStoriesController < ActionController::Base
def show
#story = UserStory.find params[:id]
##story.dev_status = gives you dev's details, with status value from Status table
end
end
If you wanted to avoid the law of demeter (IE only have one point to access your data), you'll want to use the delegate method:
#app/models/user_story.rb
Class UserStory < ActiveRecord::Base
delegate :name to: :dev_status, prefix: true
# this will allow you to call #user.dev_status_name
end
If you then wanted to have statuses changed, you'll be able to use the collection_select helper to get it working with the Status objects:
#app/views/user_stories/edit.html.erb
...
<%= f.collection_select :dev_status_id, Status.all, :id, :name, prompt: true %>
<%= f.collection_select :test_planner_status, Status.all, :id, :name, prompt: true %>
--
ActiveRecord
You must remember that models are built, they are just classes. Rails uses an ORM (ActiveRecord) to pull data to populate these classes.
Many people become confused about how models fit into the Rails ecosystem. A model is made of "attributes", which you have to populate, either manually or through the Rails ORM API. IE your User model could have the following:
#app/models/user.rb
class User < ActiveRecord::Base
def will_you_marry_me?
"no"
end
end
#app/views/application.html.erb
Will the user marry?
<%= #user.will_you_marry_me? %>
When you talk about "mapping" columns, what you're really asking is how to call a different table's data to attributes in your model class. For example, if you have User class, how to populate #user.birthday with data from profiles table etc.
The answer to that is to use the relational structure of ActiveRecord. Relational databases simply work with foreign_keys to load data from other tables. For example, you could have profiles table with user_id to get information about a specific user (see the image above).
ActiveRecord makes the process of loading "other table" data very simple. By using the relationships in the API, you can populate data with the likes of has_many etc.

How do you validate uniqueness of a pair of ids in Ruby on Rails?

Suppose the following DB migration in Ruby:
create_table :question_votes do |t|
t.integer :user_id
t.integer :question_id
t.integer :vote
t.timestamps
end
Suppose further that I wish the rows in the DB contain unique (user_id, question_id) pairs. What is the right dust to put in the model to accomplish that?
validates_uniqueness_of :user_id, :question_id seems to simply make rows unique by user id, and unique by question id, instead of unique by the pair.
validates_uniqueness_of :user_id, :scope => [:question_id]
if you needed to include another column (or more), you can add that to the scope as well. Example:
validates_uniqueness_of :user_id, :scope => [:question_id, :some_third_column]
If using mysql, you can do it in the database using a unique index. It's something like:
add_index :question_votes, [:question_id, :user_id], :unique => true
This is going to raise an exception when you try to save a doubled-up combination of question_id/user_id, so you'll have to experiment and figure out which exception to catch and handle.
The best way is to use both, since rails isn't 100% reliable when uniqueness validation come thru.
You can use:
validates :user_id, uniqueness: { scope: :question_id }
and to be 100% on the safe side, add this validation on your db (MySQL ex)
add_index :question_votes, [:user_id, :question_id], unique: true
and then you can handle in your controller using:
rescue ActiveRecord::RecordNotUnique
So now you are 100% secure that you won't have a duplicated value :)
From RailsGuides. validates works too:
class QuestionVote < ActiveRecord::Base
validates :user_id, :uniqueness => { :scope => :question_id }
end
Except for writing your own validate method, the best you could do with validates_uniqueness_of is this:
validates_uniqueness_of :user_id, :scope => "question_id"
This will check that the user_id is unique within all rows with the same question_id as the record you are attempting to insert.
But that's not what you want.
I believe you're looking for the combination of :user_id and :question_id to be unique across the database.
In that case you need to do two things:
Write your own validate method.
Create a constraint in the database
because there's still a chance that
your app will process two records at
the same time.
When you are creating a new record, that doesn't work because the id of your parent model doesn't exist still at moment of validations.
This should to work for you.
class B < ActiveRecord::Base
has_many :ab
has_many :a, :through => :ab
end
class AB < ActiveRecord::Base
belongs_to :b
belongs_to :a
end
class A < ActiveRecord::Base
has_many :ab
has_many :b, :through => :ab
after_validation :validate_uniqueness_b
private
def validate_uniqueness_b
b_ids = ab.map(&:b_id)
unless b_ids.uniq.length.eql? b_ids.length
errors.add(:db, message: "no repeat b's")
end
end
end
In the above code I get all b_id of collection of parameters, then compare if the length between the unique values and obtained b_id are equals.
If are equals means that there are not repeat b_id.
Note: don't forget to add unique in your database's columns.

Resources