-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding transaction raise methods #232
Adding transaction raise methods #232
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of small stylistic comments. Thanks for the work on this.
src/granite/transactions.cr
Outdated
@@ -1,4 +1,6 @@ | |||
module Granite::Transactions | |||
class TransactionFailed < Exception; end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TransactionError
instead
src/granite/transactions.cr
Outdated
def create!(args : Hash(Symbol | String, DB::Any) | JSON::Any) | ||
instance = create(args) | ||
|
||
raise Granite::Transactions::TransactionFailed.new("Could not create #{self.name}") unless instance.errors.empty? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is very long also I dont think you need to specify Granite::Transactions::
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, looks like Java 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, i worked on validation helper last time and all methods were one liners so i removed my multiline if statement for this one. But i will change it. I will remove the prefix module names
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just one thing i noticed.
src/granite/transactions.cr
Outdated
@@ -158,6 +179,10 @@ module Granite::Transactions | |||
end | |||
true | |||
end | |||
|
|||
def destroy! | |||
save || raise Granite::Transactions::TransactionError.new("Could not destroy #{self.name}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be destroy || raise Granite::Transactions::TransactionError.new("Could not destroy #{self.name}")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your contribution, and especially for writing nice tests around the added functionality.
I left a couple style comments but the custom exception gives me pause.
src/granite/transactions.cr
Outdated
def create!(args : Hash(Symbol | String, DB::Any) | JSON::Any) | ||
instance = create(args) | ||
|
||
if !instance.errors.empty? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would read better as if instance.errors.any?
src/granite/transactions.cr
Outdated
@@ -1,4 +1,6 @@ | |||
module Granite::Transactions | |||
class TransactionError < Exception; end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really want to create a custom exception class for this? I doubt it, but I'd like to hear from @drujensen on this.
If so, it needs to be named differently. This file is called 'transactions' but it doesn't yet do anything with the SQL concept of a transaction. It's not really a problem because the filename isn't presented to an appdev.
This exception will be handled and used directly by an appdev, and the name is misleading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really want to create a custom exception class for this? I doubt it, but I'd like to hear from @drujensen on this.
@robacarp yeah, I think we do. We have Granite::Error
class to handle errors so I think having our own exception makes sense. I would rename this to Granite::Exception
. Not sure I would place it here in this file. Maybe in its own file /src/granite/exception.cr
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went looking for the way ActiveRecord does this and here's what I found.
- ActiveRecordError - Generic error class and superclass of all other errors raised by Active Record.
- RecordInvalid - raised by ActiveRecord::Base#save! and ActiveRecord::Base.create! when the record is invalid.
- RecordNotFound - No record responded to the ActiveRecord::Base.find method. Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions.
- StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message.
After thinking on it, as an application developer, when I write the save! method, I want to be able to capture the specific exception that is going to be raised by save! and not other random exceptions.
I retract my statement about removing this custom exception. Instead I think that it should be a Granite::InvalidRecord
exception. It's more helpful and explicit, and allows use of the save! / create! methods without opening up to a category of bugs about catching the wrong exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
( Rails destroy!
raises a ActiveRecord::RecordNotDestroyed )
src/granite/transactions.cr
Outdated
@@ -143,6 +159,11 @@ module Granite::Transactions | |||
true | |||
end | |||
|
|||
|
|||
def save! | |||
save || raise Granite::Transactions::TransactionError.new("Could not save #{self.name}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this "save || raise" format better than the previous "raise unless save", but as someone else said, the explicit prefix Granite::Transactions
can be dropped from these.
describe "{{ adapter.id }} .create!" do | ||
it "creates a new object" do | ||
parent = Parent.create!(name: "Test Parent") | ||
parent.id.should_not be_nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These would read better as parent.persisted.should be_true
@@ -55,5 +55,37 @@ module {{adapter.capitalize.id}} | |||
end | |||
end | |||
end | |||
|
|||
class ParentWithCallback < Granite::Base |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a model in spec/spec_models.cr that I think satisfies this already called CallbackWithAbort
found.should be_nil | ||
end | ||
|
||
it "does not destroy an invalid object but raise an exception" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure the object validity has anything to do with the reason for not being destroyed. It could have already been destroyed, or a callback halted destroy, etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree on that but i could not find an way to raise an database exception to verify this behaviour. Granite will generate the following query "DELETE FROM "parents" WHERE "id"=$1". If the record does not exist it will still run the query and succeed. So i could not raise an "DB::Error" so i was verify the behaviour with an "Granite::Callbacks::Abort" error. I verified that the delete query never runned. I will add an test that verifies that the record is still there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great explanation. To be clear, I don't have any problem with the way you're triggering the error condition, I was just commenting on the description text of the test itself. :]
src/granite/exceptions.cr
Outdated
@@ -0,0 +1,13 @@ | |||
module Granite | |||
class RecordInvalid < ::Exception |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this be more grammatically correct as "InvalidRecord"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I followed the Rails convention you mentioned in the comment. So i introduced RecordNotDestroyed and RecordInvalid into the stack. Do you want me to change it to InvalidRecord instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally with Amber I'm not as interested in following in Rails' footsteps as I am in producing something that feels natural. Sometimes that means features similar to Rails, sometimes not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking great. I left a few comments, but nothing major. Thank you for adding the exceptions and specs to go along with them.
After discussion with @robacarp on gitter we decided that we are going to change "RecordInvalid" to "RecordNotSaved". We are going to add an model property on the exception so people who are catching the specific exception are having access to the instance. |
@Thellior great work on this! 🥇 |
class RecordInvalidParent; end | ||
class Granite::RecordInvalidParent; end | ||
{% for adapter in GraniteExample::ADAPTERS %} | ||
module {{adapter.capitalize.id}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
Can we merge this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@drujensen Should we release granite v0.12.2 or v0.13.0? 😅 |
What is done
Added "helper" methods that raises exceptions on errors. The idea is that sometimes you want to be sure that an transaction method has succeeded. If the method did not complete successfully than it will raise an exception.Methods