Rails 3 and Mongoid: Embedded documents validation - ruby-on-rails

So, I am having some issues with user authentication in embedded documents. I have two documents, one embedded in the other. A business has many members. The models look like this:
class Member
include Mongoid::Document
field :username, type: String
field :password, type: String
embedded_in :business
validates :username, :presence => true, :uniqueness => true, :length => 5..60
end
class Business
include Mongoid::Document
field :name, type: String
embeds_many :members
end
The problem is that it isn't validating the username's uniqueness in each model. When I save a member within a business, I can save a thousand of the same name. This of course is not going to work for a good authentication system. I am using Mongoid 2, Rails 3, and Ruby 1.9

This is a normal behavior when using embedded documents as explained here: MongoID validation
validates_uniqueness_of
Validate that the field is unique in the database: Note that for
embedded documents, this will only check that the field is unique
within the context of the parent document, not the entire database.
I think you want to try to create an Index in the username field that would ensure uniqueness among all the objects of that collection. Something like this:
ensureIndex({username:1},{unique:true});
EDIT: If you want Mongo to throw exception if a document with the same index value exists, you must avoid Mongo to do the “fire and forget” pattern. This means that the database will not wait for a response when you perform an update/write operation on a document.
And you want to pass this parameter: safe:true. By doing so Mongo should raise an exception if for any reason the document can't be inserted.

Related

Rails and mongo embed relations

I'm trying to create some relations on Mongoid but when I try to save the inner object or add it to the user.personal_accounts collection I get the following error
NoMethodError: undefined method `bson_type' for #<Bank:0x71c01a8>
My Object in rails console is correct
#<PersonalAccount _id: 56e87f669c27691be0d3041b, number: "55", active: true, bank: #<Bank _id: 56d74cdb9c27692fb4bd4c6d, code: 123, name: "Bradesco", country: "USA">>
My mappings
class PersonalAccount
include Mongoid::Document
field :number, type: String
field :active, type: Boolean
field :bank, type: Bank
embedded_in :user
end
class User
include Mongoid::Document
field :first_name, type: String
field :last_name, type: String
embeds_many :personal_accounts
end
class Bank
include Mongoid::Document
field :code, type: Integer
field :name, type: String
field :country, type: String
end
The mapping that I was expecting is:
User
PersonalAccounts
Bank
Bank
As I have read that I need to copy the outer bank to each PersonalAccount.
I have already tried the following Link
Versions installed:
bson (4.0.2)
bson_ext (1.5.1)
mongoid (5.0.2)
mongo (2.2.4)
The root of your problem is right here:
field :bank, type: Bank
MongoDB doesn't know how to store a Bank so Mongoid will try to convert it to something that MongoDB will understand while Mongoid is preparing the data for the database, hence the NoMethodError.
Presumably you want Bank to exist as its own collection and then each PersonalAccount would refer to a Bank. That would be a standard belongs_to setup:
class PersonalAccount
#... but no `field :bank`
belongs_to :bank
end
That will add a field :bank_id, :type => BSON::ObjectId to PersonalAccount behind the scenes and hook up accessor (bank) and mutator (bank=) methods for you.
Normally you'd want the other half of the relation in Bank:
class Bank
#...
has_many :personal_accounts
end
but that won't work (as you found out) because PersonalAccount is embedded inside User so Bank can't get at it directly. Keep in mind that embeds_one is just a fancy of wrapping the Mongoid machinery around a Hash field in a document and embeds_many is just a fancy way of wrapping the Mongoid machinery around an array of hashes inside another document; embedded documents don't have an independent existence, they're just a part of their parent.

ActiveRecord validates inclusion in list - list isn't updated after new associated model created

I have a Company model and an Employer model. Employer belongs_to :company and Company has_many :employers. Within my Employer model I have the following validation:
validates :company_id, inclusion: {in: Company.pluck(:id).prepend(nil)}
I'm running into a problem where the above validation fails. Here is an example setup in a controller action that will cause the validation to fail:
company = Company.new(company_params)
# company_params contains nested attributes for employers
company.employers.each do |employer|
employer.password = SecureRandom.hex
end
company.employers.first.role = 'Admin' if client.employers.count == 1
company.save!
admin = company.employers.where(role: 'Admin').order(created_at: :asc).last
admin.update(some_attr: 'some_val')
On the last line in the example code snippet, admin.update will fail because the validation is checking to see if company_id is included in the list, which it is not, since the list was generated before company was saved.
Obviously there are ways around this such as grabbing the value of company.id and then using it to define admin later, but that seems like a roundabout solution. What I'd like to know is if there is a better way to solve this problem.
Update
Apparently the possible workaround I suggested doesn't even work.
new_company = Company.find(company.id)
admin = new_company.employers.where(role: 'Admin').order(created_at: :asc).last
admin.update
# Fails validation as before
I'm not sure I understand your question completely, but there is an issue in this part of the code:
validates :company_id, inclusion: {in: Company.pluck(:id).prepend(nil)}
The validation is configured on the class-level, so it won't work well with updates on that model (won't be re-evaluated on subsequent validations).
The docs state that you can use a block for inclusion in, so you could try to do that as well:
validates :company_id, inclusion: {in: ->() { Company.pluck(:id).prepend(nil) }}
Some people would recommend that you not even do this validation, but instead, have a database constraint on that column.
I believe you are misusing the inclusion validator here. If you want to validate that an associated model exists, instead of its id column having a value, you can do this in two ways. In ActivRecord, you can use a presence validator.
validates :company, presence: true
You should also use a foreign key constraint on the database level. This prevents a model from being saved if there is no corresponding record in the associated table.
add_foreign_key :employers, :companies
If it gets past ActiveRecord, the database will throw an error if there is no company record with the given company_id.

Preparing seed data as JSON or for Rails helpers

I have to prepare a rather large amount of seed data for a Rails application. Based on my limited experience, I know of two ways to do it but want to know which offers the most flexibility.
One, I could do it with Rails and just prepare the seed data the way it's used in the seeds.rb file
User.create!( name: "John" )
Or, I could create a json document of the data. For example, I know that Mongodb lets you import json documents directly into the database. I'm not sure about other databases...
It occurred to me that a json document might be the most flexible, because I suppose you could also use a regular expression script to turn the json into something like this User.create!( name: "John" )
However, I'm wondering if there's any other issues I should consider...
I'm a StackOverflow newbie, so can't reply to the comments above.
I've been trying to do something similar, the mass assignment protection issue came up for me. An idea that I came across in my research was to use:
without_protection: true
I found this worked using rails 3.2.3 For example:
json = ActiveSupport::JSON.decode(File.read('db/seeds/data.json'))
json.each do |a|
Country.create!(a['data'], without_protection: true)
end
See http://snippets.aktagon.com/snippets/401-How-to-seed-your-database-with-JSON-YAML-data-in-Rails
One issue to consider is that you can't always pass all the parameters through the constructor. Consider this:
class User
include Mongoid::Document
field :name
field :role
validates :name, presence: true
validates :role, presence: true
attr_accessible :name
end
If you have something like User.create({name: 'John', role: 'admin'}), then your seed will fail because role will not be able to be assigned.

Generate a ruby class in memory

I have a requirement to convert an ActiveRecord model class into a MongoDB Document class automatically. I am able to do so using a rails generator which will read the attributes of a model and generate the new document.rb.
If a ActiveRecord model class looks like below:
class Project < ActiveRecord::Base
attr_accessible :completed, :end_date, :name, :start_date
end
Then, a generated class confirming to Mongoid's structure will be as below:
class ProjectDocument
field :name, type: String
field :start_date, type: Date
field :end_date, type: Date
field :completed, type: Boolean
field :created_at, type: Time
field :updated_at, type: Time
end
But I don't want to store a different document files, one for each model. I want to be able to generate this document class on the fly, whenever the rails application is started.
Is this possible? Is this approach of generating and using classes from memory advised? I don't have constraints on changes to AR model structure; the document is flexible w.r.t data structure and changed columns will get added automatically.
My first attempt would look something like this:
klass = Project
new_class = Object.const_set(klass.name + "Document", Class.new)
klass.columns.each do |c|
new_class.class_eval do
field c.name.to_sym, type: c.type
end
end
You'll almost certainly have to do something more complicated to set the field type correctly, but this should give you a good starting point.

Rails: Validate unique combination of 3 columns

Hi I wan't to validate the unique combination of 3 columns in my table.
Let's say I have a table called cars with the values :brand, :model_name and :fuel_type.
What I then want is to validate if a record is unique based on the combination of those 3. An example:
brand model_name fuel_type
Audi A4 Gas
Audi A4 Diesel
Audi A6 Gas
Should all be valid. But another record with 'Audi, A6, Gas' should NOT be valid.
I know of this validation, but I doubt that it actually does what I want.
validates_uniqueness_of :brand, :scope => {:model_name, :fuel_type}
There is a syntax error in your code snippet. The correct validation is :
validates_uniqueness_of :car_model_name, :scope => [:brand_id, :fuel_type_id]
or even shorter in ruby 1.9.x:
validates_uniqueness_of :car_model_name, scope: [:brand_id, :fuel_type_id]
with rails 4 you can use:
validates :car_model_name, uniqueness: { scope: [:brand_id, :fuel_type_id] }
with rails 5 you can use
validates_uniqueness_of :car_model_name, scope: %i[brand_id fuel_type_id]
Depends on your needs you could also to add a constraint (as a part of table creation migration or as a separate one) instead of model validation:
add_index :the_table_name, [:brand, :model_name, :fuel_type], :unique => true
Adding the unique constraint on the database level makes sense, in case multiple database connections are performing write operations at the same time.
To Rails 4 the correct code with new hash pattern
validates :column_name, uniqueness: {scope: [:brand_id, :fuel_type_id]}
I would make it this way:
validates_uniqueness_of :model_name, :scope => {:brand_id, :fuel_type_id}
because it makes more sense for me:
there should not be duplicated "model names" for combination of "brand" and "fuel type", vs
there should not be duplicated "brands" for combination of "model name" and "fuel type"
but it's subjective opinion.
Of course if brand and fuel_type are relationships to other models (if not, then just drop "_id" part). With uniqueness validation you can't check non-db columns, so you have to validate foreign keys in model.
You need to define which attribute is validated - you don't validate all at once, if you want, you need to create separate validation for every attribute, so when user make mistake and tries to create duplicated record, then you show him errors in form near invalid field.
Using this validation method in conjunction with ActiveRecord::Validations#save does not guarantee the absence of duplicate record insertions, because uniqueness checks on the application level are inherently prone to race conditions.
This could even happen if you use transactions with the 'serializable' isolation level. The best way to work around this problem is to add a unique index to the database table using ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the rare case that a race condition occurs, the database will guarantee the field's uniqueness.
Piecing together the other answers and trying it myself, this is the syntax you're looking for:
validates :brand, uniqueness: { scope: [:model_name, :fuel_type] }
I'm not sure why the other answers are adding _id to the fields in the scope. That would only be needed if these fields are representing other models, but I didn't see an indication of that in the question. Additionally, these fields can be in any order. This will accomplish the same thing, only the error will be on the :model_name attribute instead of :brand:
validates :model_name, uniqueness: { scope: [:fuel_type, :brand] }

Resources