I have a model Person.
One controller Api::V1::PersonsController
In my controller:
def index
#persons = Person.new(user_id: #current_user.id, type_id: params[:type_id]).method
render json: #persons, status: :ok
end
In my model:
attr_accessor :user_id, :type_id
validates_presence_of :type_id
Also tried:
validates :type_id, :presence => true
When I create my Person with no type_id, I don't get any error, what else do I need to do, or is there a better way of doing this?
From the Rails guide validation section:
The following methods trigger validations, and will save the object to
the database only if the object is valid:
create
create!
save
save!
update
update!
The bang versions (e.g. save!) raise an exception if the record is
invalid. The non-bang versions don't, save and update return false,
create just returns the object.
When you create an object using the new method, the validation rules do not fire as the object is not persisted to the database.
You can call Person.save or Person.save! after Person.new or create a Person object using create or create!. Both of these methods persist the object to the database so a validation error will be raised.
Also, in your case, you can do something like this:
Person.new(user_id: #current_user.id, type_id: params[:type_id]).valid? # => false
This way, you can check if the object is a valid object and then proceed with the rest of your code.
.new is not going to persist your Person to the database.
Validation will not be carried out unless using .save after .new or in a .create or .create! method.
Check out point 1.2 here in Rails validation guides ›
Related
I need to check if a similar record exist in database before save, if true then update the existing record without saving, else create a new one
In rails 5:
returning false in a hook method doesn't halt callbacks and "throw :abort" is used instead.
the problem is using "throw :abort" rolls back any changes made in the before_save callback.
what I am trying to do is to check for a similar recored in "before_save" and if a similar record exist I need to update the current record and stop saving the new one.
I used
before_save :check
def check
if (similar record exist..)
update current...
return false <==========
end
true
end
but this is not working any more in Rails 5 so returning false doesn't stop it from saving the new record too.
and I tried
before_save :check
def check
if (exist..)
update current...
throw :abort <========
end
true
end
this stops saving current record to db but it perform "rollback" so the updated recored is missed !!
how can I do that ?
I think this is one possible way. This example if with a Product model looking for same name.
before_create :rollback_if_similar_exists
after_rollback :update_existing_record
def rollback_if_similar_exists
throw :abort if Product.exists? name: self.name
end
def update_existing_record
# do here what you need
puts name
puts "find the existing record"
puts "update data"
end
Here is a slightly different approach you could take:
Instead of using before_save, create your own validation and use assign_attributes instead of update or create since assign_attributes won't actually write to the database. You could then call the valid? function on your record to execute your validations. If you get a duplicate record error from the validation you defined, then have your code handle updating the existing record in the logic of your error handling.
Something like this in your controller:
#record.assign_attributes(my_parameter: params[:my_parameter])
if #record.valid?
#record.save
else
puts #record.errors.messages.inspect
#update your existing record instead.
end
Then in your model:
validate :my_validation
def my_validation
if record_already_exists
return errors.add :base, "Your custom error message here."
end
end
I'd recommend using #find_or_initialize_by or #find_or_create_by to instantiate your instances. Instead of placing record swapping code inside a callback. This means you'll do something like this (example controller create):
class Post < ApplicationController
def create
#post = Post.find_or_initialize_by(title: param[:title])
if #post.update(post_params)
redirect_to #post
else
render :new
end
end
end
Pair this with a validation that doesn't allow you to create double records with similar attributes and you're set.
create_table :posts do |t|
t.string :title, null: false
t.text :body
end
add_index :posts, :title, unique: true
class Post < ApplicationRecord
validates :title, presence: true, uniqueness: true
end
I don't recommend the following code, but you could set the id of your instance to match the record with similar data. However you'll have to bypass persistence (keeps track of new and persistent records) and dirty (keeps track of attribute changes). Otherwise you'll create a new record or update the current id instead of the similar record id:
class Post < ApplicationRecord
before_save :set_similar_id
private
def set_similar_id
similar_record = Post.find_by(title: title)
return unless similar_record
#attributes['id'].instance_variable_set :#value, similar_record.id
#new_record = false
end
end
Keep in mind that only changes are submitted to the database when creating a new record. For new records these are only the attributes of which the attributes are set, attributes with value nil are not submitted and will keep their old value.
For existing records theses are the attributes that are not the same as there older variant and the rule old_value != new_value (not actual variable names) applies.
Suppose there is a Rails model with a custom setter/accessor and a uniqueness constraint on the name column:
class Person < ActiveRecord::Base
validates :name, presence: true, uniqueness: true
def name=(name)
# Example transformation only.
# Could be substituted for a more complex operation/transformation.
title_cased = name.titleize
self[:name] = title_cased
end
end
Now, consider the following:
Person.create! name: "John Citizen"
Person.find_or_create_by! name: "john citizen" # Error: validation fails
The find operation will not find any results, since there are no entries that match "john citizen". Then, the create! operation will throw an error as there is already an existing entry "John Citizen" (create! creates a new record and raises an exception if the validation fails).
How do you elegantly prevent such errors from occurring? For loose coupling and encapsulation purposes, is it possible to not transform names (to titlecase, in this case) before I perform operations like find_or_create_by! or other operations like find_by?
EDIT:
As #harimohanraj alludes to, the issue seems to be around equivalence. Should the model transparently deal with the understanding/translating input to its boiled-down, canonical state. Or should this be the responsibility of consumers of the class/model?
Also, is active record callbacks a recommended approach to this kind of scenario?
If you have defined a custom setter method, the implicit decision that you have made is: values for the name attribute, no matter what form they come in (eg. a user's input in a text field), should be handled in titleized form in your DB. If that's the case, then it makes sense that find_or_create_by! name: 'john citizen' fails! In other words, your custom setter method represents your decision that "John Citizen" and "john citizen" are one and the same.
If you find yourself wanting to store John Citizen and john citizen in your DB, then I would revisit your decision to create a custom setter method. One cool way to achieve "loose coupling" is to put all of the logic that sanitizes data (ex. data from a user filling out a form) into a separate Ruby object.
There isn't much context in the question, so here is a bit of an abstract example to demonstrate what I mean.
# A class to house the logic of sanitizing your parameters
class PersonParamsSanitizer
# It is initialized with dirty user parameters
def initialize(params)
#params = params
end
# It spits out neat, titleized params
def sanitized_params
{
name: #params[:name].titleize
}
end
end
class PersonController < ApplicationController
def create
# Use your sanitizer object to convert dirty user parameters into neat
# titleized params for your new perons
sanitized_params = UserParamsSanitizer.new(params).sanitized_params
person = Person.new(sanitized_params)
if person.save
redirect_to person
else
render :new
end
end
end
This way, you don't override the setter method in your User model, and are free to use find_or_create_by! fearlessly if you so choose!
You can set a validation to be case-insensitive by using:
class Person < ActiveRecord::Base
validates :name,
presence: true,
uniqueness: { case_sensitive: false }
end
However you also need a case-insensitive database index backing it since just using a validation in Rails will lead to race conditions. How to achieve that depends on the RBDMS.
Which leaves the issue of querying. The classic way of performing a intensive search is by WHERE LOWER(name) = LOWER(?). Although Postgres lets you use WHERE ILIKE name = ?.
If you want to encapsulate this into the model which is a good idea you would create a scope:
class Person
scope :find_by_name, lambda{ |name| where('LOWER(name) = LOWER(?)', name) }
end
However, you cannot use .find_or_create_by! in this case as the query not just a hash. Instead you would call .first_or_create.
Person.find_by_name("John Citizen").first_or_create(attrs)
see also
PostgreSQL: How to make "case-insensitive" query
The problem is the find_or_create_by and similar methods are already not tansforming the name... as you say there is no record "john citizen" but to work properly you'd need to titleize it for the find_or_create_by, find_or_create_by!, or find_by
(you don't need this solution for find as that only retrieves record by primary key)
so...
def self.find_or_create_by(options)
super(rectify_options(options))
end
def self.find_or_create_by!(options)
super(rectify_options(options))
end
def self.find_by(options)
super(rectify_options(options))
end
private
def self.rectify_options(options)
options[:name] = (new.name = options[:name]) if options[:name]
options
end
I have two models, a Parent and a Child (as outlined below). The child model has a before_save callback to handle some external logic, and if it encounters any errors, the callback invalidates that model being saved.
class Parent < ActiveRecord::Base
has_one :child
accepts_nested_attributes_for :child
validates :child, :presence => true
validates_associated :child
end
class Child < ActiveRecord::Base
belongs_to :parent
before_save :external_logic
validates :parent, :presence => true
def external_logic
begin
# Some logic
rescue
#Invalidate child model
errors.add(:base, "external logic failed")
return false
end
end
end
The problem that I'm running into is that the Child model instance is created as through the nested attributes of the Parent model. When the external logic fails, I want the child model AND the parent model to not be saved, but instead the parent model is being saved on its own. How can I achieve this?
Please note, I am aware of validation callbacks, but they are not suitable in this case. The child model callback has to be a before_save.
EDIT #1
I already know about transactions, and don't consider someone telling me "hey, wrap it around a transaction externally" to be a valid response. This question is explicitly about how to solve this issue through a before_save call.
Why I can't use validations on create - as mentioned in the comments, the external bit of logic needs to be guaranteed to run ONLY before a database save. Validation calls can happen multiple times with or without altering the database record, so that's an inappropriate place to put this logic.
EDIT #2
Ok, apparently having the before_save return false does prevent the parent being saved. I have verified that through the console and actually inspecting the database. However, my rspec tests are telling me otherwise, which is just odd. In particular, this is failing:
describe "parent attributes hash" do
it "creates new record" do
parent = Parent.create(:name => "name", :child_attributes => {:name => "childname"})
customer.persisted?.should be_false
end
end
Could that be an rspec/factory_girl bit of weirdness?
EDIT #3
The test error is because I'm using transactional fixtures in Rspec. That was leading to tests that incorrectly tell me that objects were being persisted in the database when they really weren't.
config.use_transactional_fixtures = true
Okay so your problem is with the ActiveRecord::Callbacks order.
As you can see on the linked page first validation is processed and if validation was successful then before_save callbacks are run. before_save is a place where you can assume every validation passed so you can manipulate a bit data or fill a custom attribute based on other attributes. Things like that.
So what you could do is just say for the Child model:
validate :external_logic and just remove the before_save :external_logic callback.
It's equivalent with what you want to do. When a Parent instance is created it will just error out if the Child object fails to validate, which will happen in your :external_logic validation method. This is a custom validation method technique.
After OP update:
Still you can use :validate method. You can set it to only run on create with:
validate :external_logic, :on => :create.
If you are running into issue that you need this to run on update as well, that is the default behavior. Validations are run on .create and .update only.
OR If you want to stick to before_save:
The whole callback chain is wrapped in a transaction. If any before callback method returns exactly false or raises an exception, the execution chain gets halted and a ROLLBACK is issued; after callbacks can only accomplish that by raising an exception.
I see you did return false so it should work as expected. How do you use Parent.create! method? What are the arguments there?
Make sure you are using it like (supposing .name is an attribute of Parent and Child):
Parent.create!{
:name => 'MyParent'
# other attributes for Parent
:child_attributes => {
:name => 'MyChild'
# other attributes for Child
}
}
This way it both the Parent and Child object will be created in the same transaction, so if your before_save method returns false Parent object will be rolled back.
OR
If you cannot use this format you could just try using pure transactions (doc, example in guides):
Parent.transaction do
p = Parent.create
raise Exception if true # any condition
end
Anything you do inside of this transaction will be rolled back if there is an exception raised inside the block.
This is my model:
class Goal < ActiveRecord::Base
belongs_to :user
validate :progress_is_less_than_max
private
def progress_is_less_than_max
if progress > max
errors.add(:progress, "should be less than max")
end
end
end
If I go into the console and do
some_user.goals.create! :name => 'test', :max => 10, :progress => 15, :unit => 'stuff'
it saves just fine, without any errors. What am I not doing right?
Well, that's not how you write a custom validator: your custom validator should inherit from ActiveModel::EachValidator.
See the bottom of this rails cast for an example of a customer validator: http://railscasts.com/episodes/211-validations-in-rails-3?view=asciicast
#jaydel is correct in that .create will return an instance of the model (regardless of if it is saved in the database or not).
Creates an object (or multiple objects) and saves it to the database, if validations pass. The resulting object is returned whether the object was saved successfully to the database or not.
However, calling .save! on the .create'd model or calling .create! to begin with will raise an exception if validations fail.
Creates an object just like ActiveRecord::Base.create but calls save! instead of save so an exception is raised if the record is invalid.
.save will run validations but returns false if they fail.
By default, save always run validations. If any of them fail the action is cancelled and save returns false. However, if you supply :validate => false, validations are bypassed altogether. See ActiveRecord::Validations for more information.
So I'm using ActiveRecord model validations to validate a form in a RESTful application.
I have a create action that does:
#association = Association.new
and the receiving end of the form creates a data hash of attributes from the form parameters to save to the database using:
#association = user.associations.create(data)
I want to simply render the create action if validation fails. The problem is that the .create (not !) method is throwing an exception in cases where the model validation fails. Example:
validates_format_of :url, :with => /(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$)/ix, :message => "Your url doesn't seem valid."
in the model produces:
ActiveRecord::RecordInvalid Exception: Validation failed: Url Your url doesn't seem valid.
I thought .create! is supposed throw an exception whereas .create is not.
Am I missing something here?
Ruby 1.8.7 patchlevel 173 & rails 2.3.3
Read the documentation of create and create! carefully.
Both create and create! check the callbacks (in your case validations).
create method return false if exception is raised and true if not while,
create! method raised exception if the record is invalid.
However, create can throw an ActiveRecord::RecordNotUnique if you have a unique index in the database and no validation set on the model. In this case, you should add validates :fieldname, uniqueness: true onto the model.