I'm trying to manage a HABTM relationship with a uniqueness constraint.
ie. I want my User to
has_and_belongs_to_many :tokens
But I don't want the same token to be associated with a given user more than once.
I put a unique index on the join table
add_index users_tokens [:user_id, :token_id], unique: true
which correctly results in a ActiveRecord::RecordNotUnique exception being thrown if the code tries to add the same token to a given user more than once.
In my code I was hoping to just silently catch/swallow this exception, something like this:
begin
user << token
rescue ActiveRecord::RecordNotUnique
# nothing to do here since the user already has the token
end
However, I'm running into a problem where the RecordNotUnique exception gets thrown much later in my code, when my user object gets modified for something else.
So some code calls something like
...
# The following line throws ActiveRecord::RecordNotUnique
# for user_tokens, even though
# we are not doing anything with tokens here:
user.update_counters
It's as if the association remembers that it's 'dirty' or unsaved, and then tries to save the record that didn't get saved earlier, and ends up throwing the exception.
Any ideas where to look to see if the association actually thinks it's dirty, and/or how to reset its 'dirty' state when I catch the exception?
ActiveRecord maintains in the application layer an object representation of the records in the database including relationships to other objects, and endevours to keep the application layer data representation in sync with the database. When you assign the token to the user like this:
user.tokens << token
first ActiveRecord looks for any application-level validations that would prevent the assignment, finding none it links the token to the user in the application layer, then it goes on to issue the DB request necessary to also make this connection in the DB layer. The DB has a constrant that prevents it, so an error is raised. You rescue from the error and continue, but the application level connection of the two objects is still in place. The next time that you make any edit to that same user object through ActiveRecord it will again try to bring the DB into sync with how the object is represented in the application, and since the connection to the token is still there it will make another attempt to insert this connection in the DB, but this time there is no rescue for the error that arises.
So when you do rescue from the database error you must also undo the application level change like this:
begin
user.toekns << token
rescue ActiveRecord::RecordNotUnique
user.tokens.delete(token)
end
Related
I have a model named Company along with many associations, and it's an association having many more. (with dependent destroy)
com = Company.find_by(name: 'ABC')
I then destroy the entry by
Company.find_by(name: 'ABC').destroy
Now 'com' has the record, and when I perform com.save the object does not get stored along with its association.
Note
I am trying to debug as to where my destroy query is taking time (it has hundreds of associations), so I don't want to lose the data, I want to save it in the console after debugging.
The best approach would be to, one way or another, wrap the action in a database transaction. You can then rollback the transaction to restore all the data to its original state.
To write this explicitly, you could do something like:
ActiveRecord::Base.transaction do
Company.find_by(name: 'ABC').destroy
# Any debugging can be done here, or above
raise ActiveRecord::Rollback
end
In fact, rails actually provides a debugging tool for precisely this use case: You can make any changes to the database and have everything rolled back (inside a transaction, like above) by running:
rails console --sandbox
I find myself in this situation very often. Sometimes I just take for granted that the record will be saved correctly if I'm in a rush, but I feel as that not being a good practice. I see sometimes placing the if save condition. The question arises here: what are the situations where a record cannot be saved?
what are the situations where a record cannot be saved?
If any of your validations fail. (Or of course HW failure, database connection loss etc occurs).
Should I throw an exception if an item cannot be saved?
If you want an invalid record to result in an exception being thrown, you don't need to do it yourself. Rails can already do it:
If you have a User model with a couple of validations (email and name must be present), you could:
user.save!
With save! validations always run. If any of them fail ActiveRecord::RecordInvalid gets raised.
But you probably don't want an exception to be raised in such a case. Because it is rather "common" for a user to not enter a valid password, for example. But
you should handle errors and the way this is commonly done is:
if user.save
#
else
# handle error
end
By default, save always run validations. If any of them fail the action is cancelled and save returns false.
As a general guideline for choosing between conditionals and exceptions I like this statement from DHH:
Why would the delivery of the emails fail? Because your SMTP server is down? That's an exceptional state, handle it with exceptions -- not with conditions.
What's the correct way to rescue an exception and simply continue processing? I have an app that has Folders and Items, with a habtm relationship through a join table called folders_items. That table has a unique constraint ensuring that there are no duplicate item/folder combinations. If the user tries to add an item to the same folder several times, I obviously don't want the additional rows added; but I don't want to stop processing, either.
Postgres automatically throws an exception when the unique constraint is violated, so I tried to ignore it in the controller as follows:
rescue PG::Error, :with => :do_nothing
def do_nothing
end
This works fine on single insertions. The controller executes the render with a status code of 200. However, I have another method that does bulk inserts in a loop. In that method, the controller exits the loop when it encounters the first duplicate row, which is not what I want. At first, I thought that the loop must be getting wrapped in a transaction that's getting rolled back, but it isn't -- all the rows prior to the duplicate get inserted. I want it to simply ignore the constraint exception and move to the next item. How do I prevent the PG::Error exception from interrupting this?
In general, your exception handling should be at the closest point to the error that you can do something sensible with the exception. In your case, you'd want your rescue inside your loop, for example:
stuff.each do |h|
begin
Model.create(h)
rescue ActiveRecord::RecordNotUnique => e
next if(e.message =~ /unique.*constraint.*INDEX_NAME_GOES_HERE/)
raise
end
end
A couple points of interest:
A constraint violation inside the database will give you an ActiveRecord::RecordNotUnique error rather than the underlying PG::Error. AFAIK, you'd get a PG::Error if you were talking directly to the database rather than going through ActiveRecord.
Replace INDEX_NAME_GOES_HERE with the real name of the unique index.
You only want to ignore the specific constraint violation the you're expecting, hence the next if(...) bit followed by the argumentless raise (i.e. re-raise the exception if it isn't what you're expecting to see).
If you put a Rails validator on your model, then you can control your flow without throwing an exception.
class FolderItems
belongs_to :item
belongs_to :folder
validates_uniqueness_of :item, scope: [:folder], on: :create
end
Then you can use
FolderItem.create(folder: folder, item: item)
It will return true if the association was created, false if there was an error. It will not throw an exception. Using FolderItem.create! would throw an exception if the association is not created.
The reason you are seeing PG errors is because Rails itself thinks that the model is valid on save, because the model class does not have a uniqueness constraint in Rails. Of course, you have a unique constraint in the DB, which surprises Rails and causes it to blow up at the last minute.
If performance is critical then perhaps ignore this advice. Having a uniqueness constraint on a Rails model causes it to perform a SELECT before every INSERT in order for it to do uniqueness validation at the Rails level, potentially doubling the number of queries your loop is performing. Just catching the errors at the database level like you are doing might be a reasonable trade of elegance for performance.
(edit) TL;DR: Always have the unique constraint in the DB. Also having a model constraint will allow ActiveRecord/ActiveModel validation before the DB throws an error.
Consider the following scenario:
You have an account model
You have an external service which manages subscriptions (such as CheddarGetter).
You do not want to create a customer on CG unless the data entered passed your own validations, and likewise you don't want to save the customer down to your own database unless CG accepts the customer record and payment details.
However, you want any validation errors from either side to be made available to the user.
So, how would you go about this? Validating either side is simple, but getting both sides working together seems difficult.
I have found a way of achieving this.
Local validations are carried out as normal.
External validations are carried out in a before_create callback:
def save_customer_on_cheddargetter
begin
external_api_stuff
rescue => error
errors.add :base, error.message
return false
end
true
end
As long as the callback returns false for an invalid record, and adds errors to base, the user sees one validation, and also blocks saves to the database should the API return an invalid record.
I am trying to write an application to track legal case requests. The main model is Case, which has_many Subjects, Keywords, Notes, and Evidences (which, in turn, has_many CustodyLogs). Since the application is legal-related, there are some requirements that are out of the ordinary:
CRUD operations must be logged, including what the operation was, who the actor was, and when the operation occurred
There needs to be some way to validate the data (i.e. recording MD5 checksums of records)
Some data should be write-once (i.e. the app can create an audit log entry, but that log cannot be edited or deleted from within the application thereafter)
Changes to associated objects probably should be logged throughout the nesting. For example, adding a CustodyLog to a piece of Evidence should have a log for itself, a log for it's Evidence, and a log for the parent Case. This is to ensure that the last update timestamp for the Case accurately reflects the real last update, and not just the last time that the Case model data itself changed.
I've got bits of this working, but I'm running into a problem. Authentication is being handled by an external web single-sign-on service, so the only visibility to the ID of the logged in user is in a request variable. If I put audit logging in the model, through a callback, for example, I can be fairly sure that all data modifications are logged, but the model has no visibility to the request variables, so I can't log the user ID. This also ensures that changes to the state machine (currently using state_machine plugin) get logged.
If, on the other hand, I put the audit logging in the application controller, I lose the ability to be sure that all CRUD operations are logged (code in the Case model calling Subject.create, for example, wouldn't be logged). I also think that I'd lose state changes.
Is there a way to be sure that all CRUD operations are logged throughout the association tree such that the user ID of the logged in user is recorded?
CRUD operations must be logged, including what the operation was, who the actor was, and when the operation occurred
This can be addressed with an ActiveRecord::Callbacks and an attr_accessor field.
In any of the models that need to be logged add the following.
attr_accessor :modifier_id, :modifier
valiadate :valid_user
before_validate :populate_modifier
before_save :write_save_attempted_to_audit_log
after_save :write_save_completed_to_audit_log
def populate_modifier
self.modifier = User.find_by_id(modifier_id) unless modifier
end
def valid_user
unless modifier
errors.add(:modifiers_user_id, "Unknown user attempted to modify this record")
write_unauthorized_modification_to_audit_log
end
end
def write_save_attempted_to_audit_log
# announce that user is attempting to save a record with timestamp to audit log
# use ActiveRecord::Dirty.changes to show the change the might be made
end
def write_save_competed_to_audit_log
# announce that user has successfully changed the record with timestamp to audit log
end
def write_unauthorized_modification
# announce that a change was attempted without a user
end
Because you're likely to use this in a few models you can abstract it into a plugin, and add it only with needed with a method call like audit_changes. See any of the acts_as plugins for inspiration on how to accomplish this.
In the controllers you will need to remember to add #thing.modifier = #current_user before attempting to save.
There needs to be some way to validate the data (i.e. recording MD5 checksums of records)
As for a checksum... of an operation? You could override inspect to print a string containing all the information in the record in a consistent fashion and then generate a checksum for that. While you're at it, might as well add it to the to the audit log as part of the writing to log methods.
Some data should be write-once (i.e. the app can create an audit log entry, but that log cannot be edited or deleted from within the application thereafter)
Write each access log as a separate file with a deterministic name ("/logs/audits/#{class}/#{id}/#{timestamp}") and remove the write permission once it's saved. File.chmod(0555, access_log_file)
Changes to associated objects probably should be logged throughout the nesting. For example, adding a CustodyLog to a piece of Evidence should have a log for itself, a log for it's Evidence, and a log for the parent Case. This is to ensure that the last update timestamp for the Case accurately reflects the real last update, and not just the last time that the Case model data itself changed.
As for the 4th requirement. That will automatically get rolled into my solution for the first if you use accepts_nested_attributes_for on any of your nested relationships. And :autosave => true for belongs_to relationships.
If you're saving checksums into audit logs, you can roll a check into the before_save method to ensure the object you're working on, was has not been tampered with. Just by checking the latest audit log for the object and matching up the checksums.