Em 24-04-2012 16:24, Jeremy Evans escreveu:
On Tuesday, April 24, 2012 11:11:58 AM UTC-7, Rodrigo Rosenfeld Rosas wrote:

    No, I start my specs with some populated data for some tables that
    shouldn't change unless in some specific examples, but I'd like
    them to rollback to the original state at the end of the examples.
    I do really want to use savepoints instead of a truncate strategy
    or I'd be already using DatabaseCleaner for that.


Is there a non-performance related reason you couldn't just leave the database empty and only add the same prepopulated data inside each spec (using before(:each))? I only mention it because I use that approach in other specs, and it works out fine even if it isn't very fast.

Performance is a major reason indeed. I don't *need* to keep the prepopulated data and since they're currently pretty simple ones (just a few fixed possible user roles) I could recreate them on each test requiring a user (most of them). But another issue is that I cache those roles once I read them from the database in the application level because they'll never change. I'm using standalone-migrations for managing my databases:

https://github.com/thuss/standalone-migrations

Usually I use a copy of a freshly created database by using "rake db:setup" which will also fill the "role" table.

    Your suggested approach wouldn't work pretty well as there are
    some database constraints that would require a certain order in
    the tables as well. This is pretty hard to maintain anyway and it
    is much slower than "truncate table1, table2, ...".


Barring circular references, ordering the tables in topological order so that referenced tables come after referencing tables is simple. Also, unless there are large numbers of records in the table, delete is usually not much slower than truncate. As you are doing this around multiple specs and not every spec, the performance difference is unlikely to matter.

That was not my experience when I tried that approach on a Grails application some years ago. For example, calling truncate in all tables at once was much faster than calling truncate on each separate table in dependency order.

    By design, there is no method that allows you to checkout a
    connection without forcing a checkin later.  With transactions,
    you need to ensure that all database queries use the same
    connection (the one that started the transaction).

    Yes, I've realized that too. This makes it really hard to me to
    manually create the savepoints by myself while still using Sequel
    transaction support in the around(:each) block.


This shouldn't be "really hard":

  conn = DB.synchronize{|c| c}

Humm, this is what I was looking for. I didn't realize how to get the connection from the pool without releasing it at the end of the block. I thought that synchronized would release the connection, but if it doesn't, this is great!

  DB.send(:add_transaction, conn, {})
  DB.send(:begin_transaction, conn, {})
  DB.transaction(:savepoint=>true){}
  DB.send(:begin_transaction, conn, :savepoint=>true)
  DB.send(:rollback_transaction, conn, :savepoint=>true)
  DB.send(:remove_transaction, conn, false)
  DB.send(:begin_transaction, conn, :savepoint=>true)
  DB.send(:begin_transaction, conn, :savepoint=>true)
  DB.transaction(:savepoint=>true, :rollback=>:always){}
  DB.send(:rollback_transaction, conn, :savepoint=>true)
  DB.send(:remove_transaction, conn, false)
  DB.send(:rollback_transaction, conn, :savepoint=>true)
  DB.send(:remove_transaction, conn, false)
  DB.send(:rollback_transaction, conn, {})
  DB.send(:remove_transaction, conn, false)

The hard part is the error handling. :)

Shouldn't be that hard in my case I guess. I'll give it a try as soon as I find some time to play with this.

    The way to do that is checking out a connection at the start of
    the transaction block, and checking in the connection at the end
    of the transaction block.  You cannot just checkout a connection
    to issue the BEGIN and then check the connection back in, as the
    connection code theoretically be used by other threads that
    should not be operating inside the transaction.

    Sorry this is where I got confused. Could you please give me a
    concrete example?


Thread 1: get connection from pool, issue BEGIN query, return connection to pool

This is what I find weird. In what situation Thread 1 would return the connection to the pool before finishing the transaction logic? That doesn't make any sense to me in real application scenarios.

Thread 2: get connection from pool (same as just used by Thread 1), run a DELETE, return connection to pool Thread 1: get connection from pool, issue ROLLBACK query, return connection to pool.

Result: DELETE gets rolled back, even though thread 2 didn't expect it to.

Also, you have to realize that:

  DB.synchronize{|conn| conn} == DB.synchronize{|conn| conn}

is not necessarily true.

But in your example above:

  conn = DB.synchronize{|c| c}
  DB.send(:add_transaction, conn, {})
  DB.send(:begin_transaction, conn, {})
  DB.transaction(:savepoint=>true){}

Will this last statement use the same connection as conn? Also, will DB.transaction be aware that the depth level is now 1 instead of 0? Or will it try to perform another BEGIN?

You are free to implement such things yourself if you want. In single threaded mode, you could probably get something
        that works OK.

        Sorry, but I couldn't understand what is the thread-safety
        role here.


    I'm sure if you read the connection pool and transaction related
    code, you probably would.  You could then probably also figure
    out how to implement what you want yourself.

    I've read the code and I understand why it is difficult to allow
    the usage I proposed in my previous message. But I couldn't figure
    it out how to implement what I want by myself.


Hopefully the code above will help. Handling errors correctly is up to you. :)

    To hopefully be more clear: what you want is not implemented
    because it is a bad idea, not because it is difficult to implement.

    Then please point me to some example on how I could do that
    manually as I'm having troubles on understanding how to do that by
    myself.

    For example, I'd need to issue the BEGIN in the before(:all) to
    finally issue the ROLLBACK TO SAVEPOINT in the after(:all).


I don't think this is what you want. Your before(:all) and after(:all) code would not use a SAVEPOINT, it would BEGIN/ROLLBACK a separate transaction. Your around(:each) code would use :savepoint=>true to use SAVEPOINT/ROLLBACK TO SAVEPOINT. Hopefully the difference there is obvious.

Yeah, sorry, I've sent the correct flow in my previous message examples.

    But I need a connection from the pool for being able to do that,
    and I can't get one because I have to use it inside a block.


Well, you can cheat as I did above without negative effects, but only in single threaded code.

Thanks, I didn't know that before.

    Also when the before(:each) is called, Sequel.transaction wouldn't
    know what is the current depth level.


Calling the internal transaction methods lets Sequel keep track of the depth.

You mean DB.send(:_transaction), right?

    So, as you can see, I'm totally lost on how to approach that.

    If I was going to run my specs concurrently, I'd make sure that a
    different connection would be used for each "describe" block and I
    wouldn't run multiple examples of the same outer-most block
    concurrently.


My approach is incompatible with concurrent specs. You really do need to implement around(:all) for that.

Yeah, I'll try to worry about that when I need to. Another approach would be use multiple processes and different databases to run the specs in parallel but still using a single thread per process so I could work around this.

    Could you please give me some hint on how to achieve that with Sequel?


Hopefully there's enough information here to do what you want. If not, feel free to ask more detailed questions.

Thank you very much Jeremy. I guess the "hardest" part was "conn = DB.syncronize{|c| c}" :)

I'll let you know if I find any troubles with that approach, thanks! :)

Cheers,
Rodrigo.

--
You received this message because you are subscribed to the Google Groups 
"sequel-talk" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/sequel-talk?hl=en.

Reply via email to