Rails: handling nullable attribute in the model - ruby-on-rails

I have a model that has a polymorphic association.
class User
belongs_to :address, polymorphic: true, optional: true
end
I call user.address.street on many place of the code, when address is nil I have to use a default address, so I need to check if nil in many parts.
I'd like to find an alternative to prevent this check if nil

you can create a migration to set street as default value as below:
class ChangeStreetDefaultValue < ActiveRecord::Migration
def change
change_column(:table_name, :street, :text, default: DEFAULT_STREET)
ModelName.where(street: nil).update_all(street: DEFAULT_STREET)
end
end
or
you can run the migration to update the default street and run the script on the console separately to update existing records.

Related

When we have a new range for one field in ActiveRecord, can we change the validates code immediately?

Say, if we have
class SomeData < ActiveRecord::Base
validates :foo, inclusion: 33..99
and now, we say, we will make the range narrower, to 66..99, but there are many values in the Database that are still between 33 and 65, can we change the above code to
class SomeData < ActiveRecord::Base
validates :foo, inclusion: 66..99
immediately? Is it a problem if the data is just read into the system? (even in earlier version of Rails such as 3.2?)
Changing a validation does not actually directly effect the existing data in your database in any way. Validations are only run when #valid? is called on the model.
This happens implicitly when you call:
.create and .create!
#save and #save!
#update and #update!
And it causes these methods to bail so that either a rollback occurs or no db query is fired in the first place.
Can we change the validation immediately?
Yes. The validation will only really kick in if you try to update an existing record. In which case a previously valid record may now be invalid. This will not permit an update unless the value is updated to be in the new permitted range.
Dealing with legacy data
If you want to let users still update existing records with what is invalid data according to the new rules there are serveral tricks.
The if: and unless: options
class SomeData < ActiveRecord::Base
validates :foo, inclusion: 33..99, if: :legacy_record?
validates :foo, inclusion: 66..99, unless: :legacy_record?
end
There are several ways to implement :legacy_record? like a boolean flag in the database. Note that these should not be confused with the ruby keywords. They are just hash options.
custom validation methods:
class SomeData < ActiveRecord::Base
validate :my_validation_method
def my_validation_method
rng = legacy_record? ? 33..99 : 66..99
errors.add(:foo, "out of range") unless rng.cover?(foo)
end
end
Single Table Inheritance
class Thing < ActiveRecord::Base
validates :foo, inclusion: 66..99
end
class LegacyThing < ActiveRecord::Base
self.table_name = "things"
validates :foo, inclusion: 33..99
end
In this example you would add a things.type varchar column and update the existing rows with things.type = "LegacyThing". This isn't really STI - it just uses the mechanism built into ActiveRecord.

rails_admin how to include a child attribute in create form

I'm trying to make a form to create new record for a model user which has one billing_information. Billing_information has an attribute account_name that I want to include in the form. I tried using the delegate method but it's not working. It produces :-
error: unknown attribute 'billing_information_account_name' for User.
class User < ActiveRecord::Base
accepts_nested_attributes_for :billing_information
has_one :billing_information, inverse_of: :user
delegate :account_name, to: :billing_information, allow_nil: true
rails_admin do
create do
field :name
field :email
field :billing_information_account_name do
def value
bindings[:object].account_name
end
end
end
end
end
Does anyone has a better solution? Thank you.
Sadly, you won't get help from rails admin in this case, but it can be done.
You have to add a new virtual field and handle in a setter the input. Take a look at this example.
class User < ApplicationRecord
has_one :billing_information, inverse_of: :user
# A getter used to populate the field value on rails admin
def billing_information_account_name
billing_information.account_name
end
# A setter that will be called with whatever the user wrote in your field
def billing_information_account_name=(name)
billing_information.update(account_name: name)
end
rails_admin do
configure :billing_information_account_name, :text do
virtual?
end
edit do
field :billing_information_account_name
end
end
end
You can always create the full billing_information using the nested attributes strategy, meaning add the billing_information field and you'll get a nice form to fill all the information.

Make sure has_many :through association is unique on creation

If you are saving a has_many :through association at record creation time, how can you make sure the association has unique objects. Unique is defined by a custom set of attributes.
Considering:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
before_validation :ensure_unique_roles
private
def ensure_unique_roles
# I thought the following would work:
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but the above results in duplicate, and is also kind of wonky because it goes through ActiveRecord assignment operator for an association (which is likely the cause of it not working correctly)
# I tried also:
self.user_roles = []
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but this is also wonky because it clears out the user roles which may have auxiliary data associated with them
end
end
What is the best way to validate the user_roles and roles are unique based on arbitrary conditions on an association?
The best way to do this, especially if you're using a relational db, is to create a unique multi-column index on user_roles.
add_index :user_roles, [:user_id, :role_id], unique: true
And then gracefully handle when the role addition fails:
class User < ActiveRecord::Base
def try_add_unique_role(role)
self.roles << role
rescue WhateverYourDbUniqueIndexExceptionIs
# handle gracefully somehow
# (return false, raise your own application exception, etc, etc)
end
end
Relational DBs are designed to guarantee referential integrity, so use it for exactly that. Any ruby/rails-only solution will have race conditions and/or be really inefficient.
If you want to provide user-friendly messaging and check "just in case", just go ahead and check:
already_has_role = UserRole.exists?(user: user, role: prospective_role_additions)
You'll still have to handle the potential exception when you try to persist role addition, though.
Just do a multi-field validation. Something like:
class UserRole < ActiveRecord::Base
validates :user_id,
:role_id,
:project_id,
presence: true
validates :user_id, uniqueness: { scope: [:project_id, :role_id] }
belongs_to :user, :project, :role
end
Something like that will ensure that a user can have only one role for a given project - if that's what you're looking for.
As mentioned by Kache, you probably also want to do a db-level index. The whole migration might look something like:
class AddIndexToUserRole < ActiveRecord::Migration
def change
add_index :user_roles, [:user_id, :role_id, :project_id], unique: true, name: :index_unique_field_combination
end
end
The name: argument is optional but can be handy in case the concatenation of the field names gets too long (and throws an error).

Reset PK number based on association

I have a Post and Comments table.
Post has many comments, and Comment belongs to a post.
I want to have primary keys which start at 1 when I create a comment for a Post, so that I can access comments in a REST-ful manner, e.g:
/posts/1/comments/1
/posts/1/comments/2
/posts/2/comments/1
/posts/2/comments/2
How can I achieve that with Rails 3?
I am using MySQL as a database.
Bonus: I am using the Sequel ORM; an approach compatible with Sequel, not only ActiveRecord, would be awesome.
Well, you can't use id for this, as id is a primary key here. What you can do is to add an extra field to your database table like comment_number and make it unique in the scope of the post:
#migration
def change
add_column :comments, :comment_number, :integer, null: false
add_index :comments, [:post_id, :comment_number], unique: true
end
#Class
class Comment < ActiveRecord::Base
belongs_to :post
validates :post_id, presence: true
validates :comment_number, uniqueness: { scope: :post_id }
end
Now with this in place you need to ensure this column is populated:
class Comment < ActiveRecord::Base
#...
before_create :assign_comment_number
private
def assign_comment_number
self.comment_number = (self.class.max(:comment_number) || 0) + 1
end
end
Last step is to tell rails to use this column instead of id. To do this you need to override to_param method:
class Comment < ActiveRecord::Base
#...
def to_param
comment_number
end
end
Update:
One more thing, it would be really useful to make this field read-only:
class Comment < ActiveRecord::Base
attr_readonly :comment_id
end
Also after rethinking having uniqueness validation on comment_number makes very little sense having it is assigned after validations are run. Most likely you should just get rid of it and rely on database index.
Even having this validation, there is still a possible condition race. I would probably override save method to handle constraint validation exception with retry a couple of time to ensure this won't break application flow. But this is a topic for another question.
Another option without changing models:
get 'posts/:id/comments/:comment_id', to: 'posts#get_comment'
And in the posts controller:
def get_comment
#comment = post.find(params[:id]).comments[params[:comment_id] -1]
end
Asumptions: Comments bookmarks might change if coments deletion is allowed.

Rails: default attribute value conflicting with `validates_inclusion_of`

I have something like this:
class AddTestToPeople < ActiveRecord::Migration
def change
add_column :people, :status, :string, default: "normal"
end
end
class Person < ActiveRecord::Base
validates_inclusion_of :status, in: [ "normal", "super" ]
end
...and the default status value of "normal" doesn't pass the validation when a new record is created. I guess I could just drop the default value, but I'm curious why this doesn't work. Can someone enlighten me?
Default value is set in database.
When you try to insert a record in people table with status attribute set as nil, only then the default value normal would be inserted in the database against status column.
If you are not passing any value to status attribute while saving a new record, its value would be nil. Hence, the validation won't pass. Status would only be set to "normal" at the time of inserting the record.
I would suggest you to modify the model as below, database would take care of the default value:
class Person < ActiveRecord::Base
validates_inclusion_of :status, in: [ "super" ], allow_nil: true
end
Or
Second option would be, as Danny suggested, set up an after_initialize callback and set the default value of status when its not specified. If you take up this option then I don't think that you need a default value at DB level as it status field would always be set from Model.
class Person < ActiveRecord::Base
after_initialize :init_status, if: :new_record?
validates_inclusion_of :status, in: [ "normal","super" ]
def init_status
self.status ||= "normal"
end
end

Resources