On Saturday, February 28, 2015 at 8:49:10 AM UTC-8, Tim Bates wrote:
>
> A number of discussions on this mailing list centre around how to manage
> data integrity around multiple related objects. The classic example is a
> transaction in a double-entry accounting system, where a transaction must
> have at least two entries and all the entries of a transaction must sum to
> zero:
>
>
> class Transaction < Sequel::Model
> one_to_many :entries
>
> def validate
> super
> validates_min_length 2, :entries
> errors.add(:entries, "must sum to zero") if
> entries.inject(0){|s,e|s+e.amount} != 0
> end
> end
>
> class Entry < Sequel::Model
> many_to_one :transaction
> end
>
>
> This won't work because the entries can't be created until the transaction
> is saved (we need a primary key for the transaction to associate the
> entries). The usual recommendation is to use the 'nested_attributes' plugin
> to create the transaction and the entries all together, but this is only
> part of the story.
>
> I would like to be able to do things like the following:
>
>
> trans = Transaction.first(...)
> DB.transaction do
> trans.entries[0].amount += 10
> trans.entries[0].save
> trans.entries[1].amount -= 10
> trans.entries[1].save
> end
>
You have a double entry accounting system where you allow modifications to
transactions? In any serious accounting system, transactions are
immutable. Auditors would definitely frown on any accounting system that
allowed you to modify committed transactions. You don't modify existing
transactions, you add new transactions.
Even if you wanted the ability to update transactions, it's a really bad
idea to do what you are doing with models, since models deal with values.
It's likely there is a race condition in the code you posted. If you want
atomic updates, you should update using an expression:
DB.transaction do
trans.entries[0].this.update(:amount=>Sequel.expr(:amount) + 10)
trans.entries[1].this.update(:amount=>Sequel.expr(:amount) - 10)
raise Sequel::ValidationFailed unless
trans.entries_dataset.sum(:amount) == 0
end
There's two important differences in the above code. First is that you are
updating using amount = amount + 10 and amount = amount - 10, which
guarantees an atomic update. Second is that you are checking the balance
via a database query, instead of looking at cached information in the
associations.
I would like the (database) transaction to be rolled back if and only if
> the amounts don't balance, and the only time to check this is at the point
> that the transaction is about to be committed, because until then Sequel
> doesn't know if there are more changes to come.
>
> Hence my suggestion / feature request is to have a 'before_commit' hook
> that allows me to implement deferred validations, that are checked once all
> changes have been made to a set of related objects and are about to be
> committed. This then allows Sequel to mirror the behaviour of deferred
> constraints, in databases that support them, or emulate them in databases
> that don't.
>
Implementation of this would probably not be too difficult via a database
extension. Overriding Database#commit_transaction, executing the
before_commit if the database doesn't support savepoints or the
savepoint_level is 1, then calling super should probably do it. You may
want to give that a shot.
I'm against adding model support for this. I regret adding the model
after_commit/after_rollback hooks, they should just be database hooks, not
model hooks.
An alternative approach is just to add a method that checks that the
transaction is balanced:
def DB.balanced_transaction(trans)
transaction do
yield
raise Sequel::ValidationFailed unless
trans.entries_dataset.sum(:amount) == 0
end
end
That's a simpler approach, and unless you plan on using before_commit for a
lot of different things, it's probably the better approach.
Another possible use case for a 'before_commit' hook might be to
> automatically save any unsaved objects when exiting a transaction block, to
> avoid the need to explicitly call 'save'. I'm not sure yet whether this is
> a good idea but having a 'before_commit' hook would make it easy to
> experiment with.
>
I don't think you could do this without an identity map, that every new
model object registers with, which is not how Sequel::Model currently
works, or how I want it to work.
Thanks,
Jeremy
--
You received this message because you are subscribed to the Google Groups
"sequel-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/sequel-talk.
For more options, visit https://groups.google.com/d/optout.