Ruby on Rails and NoSQL, adding fields - ruby-on-rails

I'm just diving into Mongodb and MongoID with Rails and I find it awesome. One thing the NoSQL helps is when I can add extra fields to my model without any extra effort whenever I want:
class Page
include Mongoid::Document
include Mongoid::MultiParameterAttributes
field :title, :type => String
field :body, :type => String
field :excerpt, :type => String #Added later
field :location, :type => String #Added later
field :published_at, :type => Time
validates :title, :presence => true
validates :body, :presence => true
validates :excerpt, :presence => true
end
And this works perfectly as it should. But my question is, (sorry if this is trivial) the existing entries are blank and have no defined value for the newly added field. For example, in a sample blog application, after I've published two posts, I decide to add an excerpt and a location field to my database (refer code above). Any blog post that is published after the addition of these new fields can be made sure to have a value filled in for the excerpt field. But the posts published prior to the addition of these two new fields have null values (which is understandable why) which I cannot validate. Is there an elegant solution for this?
Thank you.

There are three basic options:
Update everything inside MongoDB to include the excerpt.
Use an after_initialize hook to add a default excerpt to existing objects when you pull them out of MongoDB.
Kludge your validation logic to only check for the existence of excerpt on new objects.
(1) requires a (possible large) time hit when you make the change but it is just a one time thing and you don't have to worry about it after that. You'd pull every Page out of MongoDB, do page.excerpt = 'some default excerpt', and then save it back to MongoDB. If you have a lot of Pages you'll want to process them in chunks of, say, 100 at a time. If you do this, you'll be able to search on the excerpt without worrying about what you should do with nulls. You can also do this inside MongoDB by sending a JavaScript fragment into MongoDB:
connection.eval(%q{
db.pages.find({}, { _id: true }).forEach(function(p) {
db.pages.update(
{ _id: p._id },
{ $set: { excerpt: 'some default excerpt' } }
);
});
})
(2) would go something like this:
after_initialize :add_default_excerpt, :unless => :new_record?
#...
private
def add_default_excerpt
self.excerpt = 'some default excerpt' unless self.excerpt.present?
end
You could move the unless self.excerpt up to the :unless if you didn't mind using a lambda:
after_initialize :add_default_excerpt, :unless => ->{ |o| o.new_record? || o.excerpt.present? }
#...
private
def add_default_excerpt
self.excerpt = 'some default excerpt'
end
This should be pretty quick and easy to set up but there are downsides. First of all, you'd have a bunch of nulls in your MongoDB that you might have to treat specially during searches. Also, you'd be carrying around a bunch of code and logic to deal with old data but this baggage will be used less and less over time. Furthermore, the after_initialize calls do not come for free.
(3) requires you to skip validating the presence of the excerpt for non-new Pages (:unless => :new_record?) or you'd have to find some way to differentiate new objects from old ones while also properly handling edits of both new and old Pages. You could also force people to supply an excerpt when they change a Page and leave your validation as-is; including a :default => '' on your field :excerpt would take care of any nil issues in views and such.
I'd go with (1) if possible. If the update would take too long and you wanted the site up and running while you were fixing up MongoDB, you could add a :default => '' while updating and then remove the :default option, restart, and manually patch up any strays that got through.

Related

Rails merge duplicates if value is matching or nil

I'm trying to perform a clean up of some data.
I have details in various forms with various duplicates.
models/object.rb
attr_accessible :name, :email, :assoc_id
I want to merge duplicates where the name is matching and the email is either matching or nil, and the assoc_id is either matching or nil.
Not sure how I write the query to bring back groups of objects that are either matching or nil..
i.e.
grouped_objects = Object.group_by{|o| [o.name]}
brings me grouped just on the name
grouped_objects = Object.group_by{|o| [o.name, o.email]}
brings me grouped on name and email.
the issue is that many of the objects have missing data.
Just want a quick and dirty so that, in the absence of other information, I'll merge the records together.
However, if there's someone with a different email, or a different assoc_id I won't merge that. Appreciate that there'll be some false records, but what we'll end up with will be an improvement
How do I write that activerecord query?
grouped_objects = Object.group_by{|o| [o.name, o.email || o.email == nil]}
Hope that makes sense,
I think a better way is too make your model intolerant with duplication. You can prevent duplication directly in the model. So when your controller try to create an object, it checks before if it doesn't exist by some element you decide.
So if you want your object be unique by some element, better do something like that (assuming you want the uniqueness from name and email field) in MyModel.rb :
class MyModel < ActiveRecord::Base
attr_accessible :name, :email, :assoc_id
validates_uniqueness_of :name
validates_uniqueness_of :email, :allow_nil => true # or :allow_blank => true
# Your code...
end
You can also use :case_sensitive => false if you don't want upper case be differenciated from lower case.
Hope this is what you are looking for !

Is ok to create a custom validator that should run a query to validate?

I need to validate the membership card number of a table to be unique only for current year, so I must build a custom validator for my model column.
My questions are:
Is heavy to make a query to check for uniqueness in current year every time I update/create a single row? How am I expected to deal
with this?
I used custom validators only on CakePHP, can you post a stupid example only to show me a syntax example?
Edit 1:
Notice that the current year is stored in a column called expire_date which is a year-month-day date format.
The check should be performed only for the year however.
How can I deal with this? That's why I suppose I should use a custom validator, I think scope shouldn't work in this case.
Edit 2:
I just noticed a :if option, I'll check if I can implement this through it.
Rails 3 provides syntax sugar, so you can do something like this:
validates :car_number, :uniqueness => {:scope => :expire_year, :message => 'has already been taken this year'}, :on => :update
def expire_year
expire_date.strftime("%Y")
end
How the query heavy is depends on DB schema and load. Don't forget to setup an index on year column and check the rails logs.
Note this will work for update only, because new object doesn't have the timestamps populated until save.
ActiveRecord already have that!
validates_uniqueness_of :car_number, :scope => :year
Docs for validates_uniqueness_of

Validations that rely on associations being built in Rails

A Course has many Lessons, and they are chosen by the user with a JS drag-n-drop widget which is working fine.
Here's the relevant part of the params when I choose two lessons:
Parameters: {
"course_lessons_attributes"=>[
{"lesson_id"=>"43", "episode"=>"1"},
{"lesson_id"=>"44", "episode"=>"2"}
]
}
I want to perform some validations on the #course and it's new set of lessons, including how many there are, the sum of the lessons' prices and other stuff. Here's a sample:
Course Model
validate :contains_lessons
def contains_lessons
errors[:course] << 'must have at least one lesson' unless lessons.any?
end
My problem is that the associations between the course and the lessons are not yet built before the course is saved, and that's when I want to call upon them for my validations (using course.lessons).
What's the correct way to be performing custom validations that rely on associations?
Thanks.
looks like you don't need a custom validation here, consider using this one:
validates :lessons, :presence => true
or
validates :lessons, :presence => {:on => :create}
You can't access the course.lessons, but the course_lessons are there, so I ended up doing something like this in the validation method to get access to the array of lessons.
def custom validation
val_lessons = Lesson.find(course_lessons.map(&:lesson_id))
# ...
# check some things about the associated lessons and add errors
# ...
end
I'm still open to there being a better way to do this.

How to restrict values for a column

I want to restrict available values for a field. So the value of the column must be from specified set of values. Is it possible using migration/models? Or I have to do it manually in my DB?
You'll use validations for this. There's a whole Rails guide on the topic. The specific helper you're looking for in this case is :inclusion, e.g.:
class Person < ActiveRecord::Base
validates :relationship_status,
:inclusion => { :in => [ 'Single', 'Married', 'Divorced', 'Other' ],
:message => "%{value} is not a valid relationship status" }
end
Edit Aug. 2015: As of Rails 4.1, you can use the enum class method for this. It requires that your column be an integer type:
class Person < ActiveRecord::Base
enum relationship_status: [ :single, :married, :divorced, :other ]
end
It automatically defines some handy methods for you, too:
p = Person.new(relationship_status: :married)
p.married? # => true
p.single? # => false
p.single!
p.single? # => true
You can read the documentation for enum here: http://api.rubyonrails.org/v4.1.0/classes/ActiveRecord/Enum.html
It depends on the amount of confidence you need. You could just add a validator to your model to restrict it to those values but then you wont be sure that existing data will match (and will cause subsequent saves to fail because of validation) and also that other changes could be made by other apps/raw sql that would get around it.
If you want absolute confidence, use the database.
Here's what you might want to use if you do it in the database (which is quite limited compared to what a rails validator could do: http://www.w3schools.com/sql/sql_check.asp

How do I define synthetic attributes for an ActiveRecord model?

I have an ActiveRecord model whose fields mostly come from the database. There are additional attributes, which come from a nested serialised blob of stuff. This has been done so that I can use these attributes from forms without having to jump through hoops (or so I thought in the beginning, anyway) while allowing forwards and backwards compatibility without having to write complicated migrations.
Basically I am doing this:
class Licence < ActiveRecord::Base
attr_accessor :load_worker_count
strip_attributes!
validates_numericality_of :load_worker_count,
:greater_than => 2, :allow_nil => true, :allow_blank => true
before_save :serialise_fields_into_properties
def serialise_fields_into_properties
...
end
def after_initialize
...
end
...
end
The problem I noticed was that I can't get empty values in :load_worker_count to be accepted by the validator, because:
If I omit :allow_blank, it fails validation complaining about it being blank
If I put in :allow_blank, it converts the blank to 0, which when fails on the :greater_than => 2
In tracking down why these blank values are getting to the validation stage in the first place, I discovered the root of the problem: strip_attributes! only affects actual attributes, as returned by the attributes method. So the values which should be nil at time of validation are not. So it feels like the root cause is that the synthetic attributes I added in aren't seen when setting which attributes to strip, so therefore I ask:
Is there a proper way of creating synthetic attributes which are recognised as proper attributes by other code which integrates with ActiveRecord?
I assume you are talking of the strip_attributes plugin; looking at the code, it uses the method attributes, defined in active_record/base.rb, which uses #attributes, which is initialized (in initialize) as #attributes = attributes_from_column_definition.
Maybe it's possible to hack ActiveRecord::Base somehow, but it would be a hard work: #attributes is also used when getting/putting stuff from/to db, so you would have to do a lot of hacking.
There's a much simpler solution:
before_validate :serialise_fields_into_properties
...
def serialise_fields_into_properties
if load_worker_count.respond_to? :strip
load_worker_count = load_worker_count.blank? ? nil : load_worker_count.strip
end
...
end
After all, this is what strip_attributes! does.
Wouldn't it be easier to just use Rails' serialize macro here?
class License < ActiveRecord::Base
serialize :special_attributes
end
Now you can assign a hash or array or whatever you need to special_attributes and Rails will serialize it a text field in the database.
license = License.new
license.special_attributes = { :beer => true, :water => false }
This will keep your code clean and you don't have to worry about serializing/deserializing attributes yourself.

Resources