Skip to content
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

Merged
merged 17 commits into from
Jun 21, 2018
Merged

Adding transaction raise methods #232

merged 17 commits into from
Jun 21, 2018

Conversation

Thellior
Copy link
Contributor

@Thellior Thellior commented Jun 17, 2018

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

  • Added update method that accepts args that will set the attributes of the model and tries to save it
  • Added update! method that accepts args that will set the attribute of the model and tries to save! it
  • Added save! method that will call the save method and raises an exception if it failed
  • Added create! method that will call the create method and raises an exception if it failed
  • Added destroy! method that will call the destroy method and raises an exception if it failed
  • Added custom exceptions for the methods so an application developer is able to catch specific errors

@Thellior Thellior changed the title [WIP] Adding transaction raise methods Adding transaction raise methods Jun 17, 2018
@faustinoaq faustinoaq requested a review from a team June 17, 2018 09:03
Copy link
Contributor

@eliasjpr eliasjpr left a 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.

@@ -1,4 +1,6 @@
module Granite::Transactions
class TransactionFailed < Exception; end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TransactionError instead

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?
Copy link
Contributor

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::

Copy link
Contributor

@faustinoaq faustinoaq Jun 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, looks like Java 😅

Copy link
Contributor Author

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

Copy link
Contributor

@Blacksmoke16 Blacksmoke16 left a 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.

@@ -158,6 +179,10 @@ module Granite::Transactions
end
true
end

def destroy!
save || raise Granite::Transactions::TransactionError.new("Could not destroy #{self.name}")
Copy link
Contributor

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}")

Copy link
Member

@robacarp robacarp left a 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.

def create!(args : Hash(Symbol | String, DB::Any) | JSON::Any)
instance = create(args)

if !instance.errors.empty?
Copy link
Member

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?

@@ -1,4 +1,6 @@
module Granite::Transactions
class TransactionError < Exception; end
Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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 )

@@ -143,6 +159,11 @@ module Granite::Transactions
true
end


def save!
save || raise Granite::Transactions::TransactionError.new("Could not save #{self.name}")
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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

Copy link
Contributor Author

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

Copy link
Member

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. :]

@@ -0,0 +1,13 @@
module Granite
class RecordInvalid < ::Exception
Copy link
Member

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"?

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Member

@robacarp robacarp left a 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.

@robacarp robacarp dismissed eliasjpr’s stale review June 18, 2018 22:33

issues resolved

@Thellior
Copy link
Contributor Author

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.

@eliasjpr
Copy link
Contributor

@Thellior great work on this! 🥇

class RecordInvalidParent; end
class Granite::RecordInvalidParent; end
{% for adapter in GraniteExample::ADAPTERS %}
module {{adapter.capitalize.id}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

@Thellior
Copy link
Contributor Author

Can we merge this?

Copy link
Contributor

@faustinoaq faustinoaq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robacarp 🔔 😅

@Thellior Thank you! 👍

@faustinoaq
Copy link
Contributor

@drujensen Should we release granite v0.12.2 or v0.13.0? 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants