Ruby On Rails and Transactions?

I read Greg Luck's interesting blog entry about issues in Ruby On Rails framework. Most of the issues in this article discussed  non functional problems in Ruby On Rails, like performance, scaleability, lack of prepared statemens etc. Although these facts are criticial in production, the Ruby On Rails platform will surely get improved over time. I evaluated Ruby On Rails in march this year and found a more interesting issue:
The Rail's persistence is based on the Active Record Pattern. An object consists of state and behavior and is mapped directly to database. The specific implementation in Rail's seems not to have the first level cache (FLC). FLC is used in the most persistence frameworks like Hibernate, CMP 2.0, JDO etc. to ensure consistency inside a transaction.
Without FLC you will receive everytime a new copy of an object inside the SAME transaction:

tx.begin()
instance1= read(pk1)
instance2=read(pk2)
instance3=read(pk3)
tx.commit()
// instance1, instance2, instance3 are copies of the same record in the database.

This is not a big problem in "read only" or master data management applications. But in case the business logic becomes more complex, you have to track the instances inside a transaction, otherwise the data becomes inconsistent. "Lost Updates" can happen inside a transaction.

tx.begin()
instance1= read(pk1)
instance1.setA(1);
update(instance1);

instance2=read(pk2)
instance2.setB(2)
update(instance2) // overrides the A with old data

instance3=read(pk3)
instance3.setC(3)
update(instance3); //overrides the A and B with the initial (old) data
tx.commit()

This is can occur, in case more developers are working on the same component. There are some workarounds which ensure consistency inside a transaction:

  • transactions are sorted: You have first to read, then to write (in real world hard to establish)
  • intra-transactional cache has to be implemented: In this case every read object has to be registered in this cache (actually our FLC). The read operation read first the cache then the persistent store. Problem: what happens in case someone changes the database data directly? (Answer: your cache becomes stale :-))
I saw this problems in J2EE projects, where developers built their own persistence frameworks, instead of using exisiting one. The insonsistencies were hard to find and debug.
I wonder whether these problems can also occur in Ruby On Rails, or are specific to the J2EE(JEE) platform?

Comments:

Your best bet currently seems to be to rely on locking:

http://api.rubyonrails.com/classes/ActiveRecord/Locking.html

Obviously, this is not really a solution, but rather a workaround. Alternatively, you could directly update the attributes in the DB and rely on your DB's isolation level support to do things right for you.

Posted by Stefan Tilkov on July 28, 2006 at 03:17 PM CEST #

Stefan,

thank you very much. I agree it is a workaround, which also increases the complexity, because now you have to deal with Optimistic Concurrency inside a transaction. This problem cannot occur in JEE environment with CMP 3, JDO or Hibernate. But I saw exactly this problem in many custom frameworks which wasn't maintainable at the end of the project.

Seeing it strictly, things get even worse: Rails do not provide the ACID quality of a transaction.

I think Rails is usable, but I wondered how it is possible to build complex applications easy, without ACID.

Every workaround increases the overall complexity,

Thank you again,

adam

Posted by Adam Bien on July 28, 2006 at 03:53 PM CEST #

I'd love to see DHH's response to this. He has said that you could easily do banking with rails.

Posted by Chris on August 14, 2006 at 05:33 PM CEST #

No one is forcing you to fetch multiple instances of the same object from within the same transaction. Please do post a realistic example of that happening and I'm sure we can refactor it in a way that doesn't require multiple fetches and saves of the same instance.

BUT. If you actually do require that, you can either guard yourself with locking, or you can inject a thin wrapper to provide FLC in your application. I believe the Robot Co-op has some work doing a FLC with memcached that you'd might like to look into.

Posted by DHH on August 14, 2006 at 10:43 PM CEST #

I followed the link to Greg Luck's blog and got this:
"We see you're using Internet Explorer, which is not compatible with this site. We strongly suggest downloading Firefox. We think you'll like it better:"

And, of course, my access was blocked. That blog sure must be worth reading... by himself only.

Posted by Ervin on August 14, 2006 at 10:51 PM CEST #

How would locking solve this problem?

You lock a record, and then you read it twice into two variables. Are they referring to the same instance now?

Or does the second read block, now causing a different kind of problem?

Posted by Bob on August 15, 2006 at 02:52 AM CEST #

DHH,

"...
No one is forcing you to fetch multiple instances of the same object from within the same transaction. Please do post a realistic example of that happening and I'm sure we can refactor it in a way that doesn't require multiple fetches and saves of the same instance.
..."

No one force me, but it simple happens in projects where more developers are involved... I had already many refactorings in JEE world, because of this problem - so I'm glad that the persistence tier handles the transactions for me.

In Java EE/Hibernate/JDO/TopLink world you will always get the same instance in a transaction, regardless what you are doing. Locking is another story. Regardless whether you are using locking or not, you ALWAYS HAVE to see your own changes in the database/persistence tier.
Locking, IsolationLevels etc. only configure the concurrency between different transactions. I'm talking here about a single transaction.

Posted by Adam Bien on August 15, 2006 at 01:23 PM CEST #

I don't quite understand your examples. This is not 'lost update' phenomenon. You are using different pks ( pk1, etc ) and only one tx. What would you expect?

Regards,
Horia

Posted by Horia Muntean on August 17, 2006 at 06:55 PM CEST #

Your point is clear, currently Rails has no identity map for model objects.
As to the site that bans Internet Explorer users - well, I deeply sympathize.

Posted by Julik on August 19, 2006 at 12:44 AM CEST #

By the way, there is a reason to that - Rails AR has completely dynamic attributes. To explain that - if you fetch, say, 4 additional fields (that you have acquired by aggregation or other SQL magic) they will be connected to the resulting recordset rows and will be available as record attributes. In case of an identity map, you wild have a great deal of frustration if the records already fetched would acquire these attributes as well.

Posted by Julik on August 19, 2006 at 12:50 AM CEST #

Horia,

I'm using the same PKs in the same transaction.
@Julik: thank you for the explanation

Posted by Adam Bien on August 20, 2006 at 12:39 PM CEST #

I still don't understand your code.

There is no need for FLC or ORMs in order to avoid in-transaction 'lost update'. It will not happen:

<pre>
import groovy.sql.Sql;

sql = Sql.newInstance('jdbc:db2://192.168.254.80:50001/arena','*****','*****', 'com.ibm.db2.jcc.DB2Driver');
sql.connection.setAutoCommit(false)

println sql.firstRow('select id, name from arena.t1 where id=?',[1])
sql.execute('update arena.t1 set name=? where id=?',['John',1])

println sql.firstRow('select id, name from arena.t1 where id=?',[1])

sql.commit()
sql.close()
</pre>

will print:
<pre>
{ID=1, NAME=Mike}
{ID=1, NAME=John}
</pre>

So, there is no lost update here and the same pk is read inside the same transaction.

Regards,
Horia

Posted by Horia Muntean on September 07, 2006 at 02:23 PM CEST #

Horia,

Suppose that you have loaded an object from the database using an ORM tool and have modified one of its fields:

Employee e = ORM.findEmployee(1);
e.setSalary(e.getSalary() * 1.1);

Now in the same transaction, suppose you have loaded the same employee once again, this time modifing another of its fields:

Employee e2 = ORM.findEmployee(1);
e2.setBonus(5000.0);

Now at the end of the transaction you save e and e2:

ORM.save(e);
ORM.save(e2);

Now depending on the ORM you use, the change to the salary field of the employee might be lost! (In RoR, it's lost, In EJB 3 is not lost)

Regards,
Behi

Posted by Behrang on September 07, 2006 at 06:59 PM CEST #

begin
Employee.transaction do
emp = Employee.new
emp.name = 'Kurz'
emp.surname = 'Martin'
emp.save!
emp2 = Employee.find(emp.id)
emp3 = Employee.find(emp.id)
emp2.surname = 'Marta'
emp3.name = 'Hürlimann'
puts "before emp2.save!"
emp2.save!
puts "before emp3.save!"
emp3.save!
emp.reload
pp emp
raise "Rollback"
end
rescue
puts $!
end

==> Attempted to update a stale object

Posted by sam on September 09, 2006 at 03:36 AM CEST #

Post a Comment:
  • HTML Syntax: NOT allowed
...the last 150 posts
...the last 10 comments
License