Something has been bothering me about model validation: how do I know that
the database has not changed in between when I validate my model's data, and
when it actually gets saved to the database? As a simple example, say I have
a User model like:
class User
validates_uniqueness_of :username
end
When this gets saved to the database, the validation code checks that there is
not already a row containing the same value for username as in our object.
The object is then saved to the database and everyone is happy. But what if
there is another user trying to register with the same username at the same
time? I am assuming that with load balancing situations is it possible that
two rails processes could try to save a User object at the same time. So
what happens if the User object in each process checks that its username is
unique, and then each User object does an insert into the database. Is the
uniqueness constraint not violated?
One option would be to have a database-level uniqueness constraint, but I've
heard that this is not the "rails way", and in any case, this is just a
simple example. In general the validation code could be arbitrarily complex.
The other option that I can see is to use a transaction around the save
operation, and define an after_save callback that re-validates the data in
the database. If (for the username example above) there is more than one
identical username, the after_save callback could throw an exception which
(as I understand it) would back out the transaction. Although if this is
done, perhaps it is not worth validating the model before saving it in the
first place, since we would catch any errors in after_save.
Is this the way that this problem is usually handled? Or am I seeing a
problem where one does not really exist?
Something has been bothering me about model validation: how do I know that
the database has not changed in between when I validate my model's data, and
when it actually gets saved to the database? As a simple example, say I have
a User model like:
class User
validates_uniqueness_of :username
end
When this gets saved to the database, the validation code checks that there is
not already a row containing the same value for username as in our object.
The object is then saved to the database and everyone is happy. But what if
there is another user trying to register with the same username at the same
time? I am assuming that with load balancing situations is it possible that
two rails processes could try to save a User object at the same time. So
what happens if the User object in each process checks that its username is
unique, and then each User object does an insert into the database. Is the
uniqueness constraint not violated?
One option would be to have a database-level uniqueness constraint, but I've
heard that this is not the "rails way", and in any case, this is just a
simple example. In general the validation code could be arbitrarily complex.
The other option that I can see is to use a transaction around the save
operation, and define an after_save callback that re-validates the data in
the database. If (for the username example above) there is more than one
identical username, the after_save callback could throw an exception which
(as I understand it) would back out the transaction. Although if this is
done, perhaps it is not worth validating the model before saving it in the
first place, since we would catch any errors in after_save.
Is this the way that this problem is usually handled? Or am I seeing a
problem where one does not really exist?
I think I'd put a unique constraint in the database and be done with it. Treat the problem at the source so to speak...
Rails shouldn't care if it's there... it will fail on save and you can deal with it at that point...
It's kind of like javascript form validation... it's nice, but you also need to do it on the server if you care about it at all...
This is a problem with an validation which fetches data before the save. Without a lock or serializing transactions, the validation is not a guarantee. Use a database constraint as a fallback.
> Something has been bothering me about model validation: how do I know
> that the database has not changed in between when I validate my model's
> data, and when it actually gets saved to the database?
...
> One option would be to have a database-level uniqueness constraint, but
> I've heard that this is not the "rails way", and in any case, this is
> just a simple example. In general the validation code could be
> arbitrarily complex.
I think I'd put a unique constraint in the database and be done with it.
Treat the problem at the source so to speak...
Rails shouldn't care if it's there... it will fail on save and you can
deal with it at that point...
That's true, but as I mentioned, I have other cases where I need to do more
complex validations, which would require the use of stored procedures (I
think -- I've never written one) if I wanted to validate at the database
level, and I think this would be highly undesirable.
I suppose the problem is that there are two types of validation, which I'll
call "local" and "global". Local validation doesn't care about any data
which is already stored in the database (e.g. validates_length_of,
validates_presence_of), and can safely be handled in isolation by validating
before saving. Global validation is concerned with how the data in a model
fits in with the existing data in the database (e.g. a custom validation that
ensures that person is not an ancestor of himself) and cannot be handled in
isolation. All the examples of Rails code that I have seen, however, treat
"global" validation problems as "local" ones, at least to the extent that
they don't check the integrity of the database after the update. Admittedly
I haven't seen that much Rails code. So if anyone has a "best practice"
counter example, I'd love to see it.
If more databases had coherent, parseable error messages, we could catch the database exception and populate record.errors. Otherwise we could establish a naming convention for database constraints so we can deduce which validation failed. I tried these with PostgreSQL but was dissatisfied with its error reporting and naming conventions are a pain without migrations support to do it for you.