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.

Reply via email to