-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
29 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
name: Run tests | ||
name: all tests | ||
on: push | ||
|
||
jobs: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,75 +1,47 @@ | ||
# Unconstrained | ||
|
||
Unconstrained for Active Record converts exceptions raised because of | ||
underlying database constraints to appropriate ActiveModel::Errors | ||
underlying foreign key constraints to appropriate ActiveModel::Errors | ||
|
||
## Usage | ||
|
||
In your Gemfile: | ||
For example, if trying to insert a record in a model `Book`, which | ||
`belongs_to :author` using an invalid `author_id`, instead of getting | ||
an exception such as: | ||
|
||
```ruby | ||
gem 'unconstrained' | ||
``` | ||
|
||
## Notes | ||
|
||
Only constraints that have to do with referential integrity are handled. | ||
This is intentional, as these require a roundrip to the database anyway. | ||
Constraints that can be enforced using validations in the application | ||
should be checked that way. | ||
|
||
Currently only handles PostgreSQL. | ||
|
||
## Reasoning | ||
|
||
Rails supports the `dependent` option on `has_one` / `has_many` | ||
associations, which can be used to a similar effect, ie: | ||
|
||
```ruby | ||
has_many :children, dependent: :restrict_with_error | ||
PG::ForeignKeyViolation: ERROR: insert or update on table "books" | ||
violates foreign key constraint "fk_rails_xxxxxxxxxx" | ||
DETAIL: Key (author_id)=(xxxxxxxxxx) is not present in table "authors". | ||
``` | ||
|
||
However, this has drawbacks. In the simple case, it offers no particular | ||
advantage, since it still needs a roundrip to the database, and since no | ||
locking takes place, there exists the possibility of an error in a | ||
system under heavy use. | ||
you will now get a validation error on the field `author_id` with the | ||
message `"author is invalid"` | ||
|
||
In more complex scenarios, errors are bound to occur. For example, | ||
consider the following scenario: | ||
Conversely, when if trying to delete an author but books exist, instead of: | ||
|
||
``` | ||
parents | ||
| | ||
| <- dependent: :destroy | ||
| | ||
---children | ||
| | ||
| <- dependent: :restrict_with_error | ||
| | ||
---grandchildren | ||
PG::ForeignKeyViolation: ERROR: update or delete on table "authors" | ||
violates foreign key constraint "fk_rails_xxxxxxxxxx" on table "books" | ||
DETAIL: Key (id)=(xxxxxxxxxx) is still referenced from table "books". | ||
``` | ||
|
||
Here ActiveRecord will never have an efficient way to proceed. It would | ||
either have to delete the children en masse, leading to database errors | ||
such as: | ||
you will now get a `base` validation error on the record with the | ||
message `"Cannot delete record because dependent books exist"` | ||
|
||
``` | ||
ActiveRecord::InvalidForeignKey: PG::ForeignKeyViolation: ERROR: | ||
update or delete on table "children" violates foreign key constraint | ||
"fk_rails_xxxxxxxxxx" on table "grandchildren" | ||
DETAIL: Key (id)=(xxxxxxxxxx) is still referenced from table "grandchildren". | ||
``` | ||
![all tests](https://github.com/agios/unconstrained/actions/workflows/run-tests.yml/badge.svg?branch=master) | ||
|
||
Otherwise ActiveRecord would need to load and check every child record, | ||
which would be extremely inefficient. On the other hand, a relational | ||
database, with the appropriate constraints defined, would handle the | ||
operation swiftly and efficiently. | ||
## Usage | ||
|
||
Now that Rails 4.2 supports foreign key constraints, one would simply | ||
define in the migration: | ||
In your Gemfile: | ||
|
||
```ruby | ||
add_foreign_key :children, :parents, on_delete: :cascade | ||
add_foreign_key :grandchildren, :children | ||
gem 'unconstrained' | ||
``` | ||
and then the database exceptions would be handled by Unconstrained. | ||
|
||
## Notes | ||
|
||
Only constraints that have to do with referential integrity are handled. | ||
This is intentional, as these require a roundrip to the database anyway. | ||
Constraints that can be enforced using validations in the application | ||
should be checked that way. | ||
|
||
Currently only handles PostgreSQL. |