Is save/save! synchronous?

1,097 views
Skip to first unread message

Chris Mayan

unread,
Feb 1, 2011, 12:36:48 AM2/1/11
to rails-...@googlegroups.com
Hi all,

Just quick query that's been bugging me this afternoon:

Is .save / .save! synchronous?
i.e. After I call that line - should the database row be created in
the DB before executing the next line?

I'm just puzzled as to why step by step debugging, I don't see the DB
row created at all even several lines afterwards.

Is this because the .save! is inside a transaction block, so Rails on
purpose does not actually do any DB actions until the very end of the
transaction block?
(I always though it committed, and then does a rollback if the
transaction fails... meaning I should still see that DB entry in there
after the .save line is executed irrespective of the transaction
block)

Thanks,
Chris

Ivan Vanderbyl

unread,
Feb 1, 2011, 12:45:40 AM2/1/11
to rails-...@googlegroups.com
Hi Chris,

Yes they are synchronous, one thing which might stop it saving is validations failing, .save should return true if it works, otherwise check .errors

-IV

> --
> You received this message because you are subscribed to the Google Groups "Ruby or Rails Oceania" group.
> To post to this group, send email to rails-...@googlegroups.com.
> To unsubscribe from this group, send email to rails-oceani...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/rails-oceania?hl=en.
>

Ben Hoskings

unread,
Feb 1, 2011, 12:48:29 AM2/1/11
to rails-...@googlegroups.com
On 1 February 2011 16:36, Chris Mayan <chris...@gmail.com> wrote:
Hi all,

Just quick query that's been bugging me this afternoon:

Is .save / .save! synchronous?
i.e. After I call that line - should the database row be created in
the DB before executing the next line?

I'm just puzzled as to why step by step debugging, I don't see the DB
row created at all even several lines afterwards.

Is this because the .save! is inside a transaction block, so Rails on
purpose does not actually do any DB actions until the very end of the
transaction block?

It's definitely a synchronous call, in that the database has received the INSERT/UPDATE command before #save[!] returns.

But, if you're in a transaction you won't be able to see the data in any other context until the database receives COMMIT when the transaction block closes.

—Ben

Daniel N

unread,
Feb 1, 2011, 12:50:04 AM2/1/11
to rails-...@googlegroups.com
On 1 February 2011 16:36, Chris Mayan <chris...@gmail.com> wrote:
Hi Chris,

The transactions kinda work that way. Your database server will write to the db in a non-commited way while you're inside a transaction block. This means that if you're trying to read the database from another connection, like the cli or navicat, the data won't be there until the transcaction block is all done. It's done this way to prevent reads happening, and then the transaction fails. The transaction is only 'pre' written inside the transaction block (i.e. it's written as far as that connection is concerned) and then when it's done it's finally committed...

hth
Daniel

Bodaniel Jeanes

unread,
Feb 1, 2011, 12:51:47 AM2/1/11
to rails-...@googlegroups.com
Are you using MySQL? If so, the default setup will give you some woes if you try to do something like:

transaction do
� task = Task.new(...)
� tasks = Task.all
� task.save
� Task.all # This will return the same as `tasks`, and won't include your new task
end

There is a way to change that behaviour, I believe...


Ben Hoskings wrote:
�Ben

Bodaniel Jeanes

unread,
Feb 1, 2011, 12:52:59 AM2/1/11
to rails-...@googlegroups.com
Clarification from my last post: the reason that you might not see the new row even from WITHIN the same transaction is because the result of a query is essentially cached. I.e. if you took out the first Task.all count from my example, the second one WOULD include the new row.

Clifford Heath

unread,
Feb 1, 2011, 12:55:12 AM2/1/11
to rails-...@googlegroups.com
On 01/02/2011, at 4:50 PM, Daniel N wrote:
> The transactions kinda work that way. Your database server will
> write to the db in a non-commited way while you're inside a
> transaction block. This means that if you're trying to read the
> database from another connection, like the cli or navicat, the data
> won't be there until the transcaction block is all done. It's done
> this way to prevent reads happening, and then the transaction fails.
> The transaction is only 'pre' written inside the transaction block
> (i.e. it's written as far as that connection is concerned) and then
> when it's done it's finally committed...

And until it's committed, if you try to read it, you will *block* or
fail on a timeout/deadlock.
What *won't* happen is that you get to see the data in either the
before or after state.

Clifford Heath.

Daniel N

unread,
Feb 1, 2011, 12:57:28 AM2/1/11
to rails-...@googlegroups.com
On 1 February 2011 16:52, Bodaniel Jeanes <m...@bjeanes.com> wrote:
Clarification from my last post: the reason that you might not see the new row even from WITHIN the same transaction is because the result of a query is essentially cached. I.e. if you took out the first Task.all count from my example, the second one WOULD include the new row.

This is the case inside a controller, the AR cache is turned on, you can turn this off for situations like this with

Task.uncached do
  Task.all
  task.save!
  Task.all
end

Daniel N

unread,
Feb 1, 2011, 1:02:53 AM2/1/11
to rails-...@googlegroups.com
How do you mean Cliff? Do you mean from the connection inside the transaction that it can't be read?

Clifford Heath

unread,
Feb 1, 2011, 1:08:40 AM2/1/11
to rails-...@googlegroups.com
On 01/02/2011, at 5:02 PM, Daniel N wrote:
> And until it's committed, if you try to read it, you will *block* or
> fail on a timeout/deadlock.
> What *won't* happen is that you get to see the data in either the
> before or after state.
> How do you mean Cliff? Do you mean from the connection inside the
> transaction that it can't be read?

No, I mean that you can's see the change from any other transaction,
say a
command-line SQL command or a database query tool. You'll only see the
results from within the transaction that created them.

Clifford Heath.

Daniel N

unread,
Feb 1, 2011, 1:09:35 AM2/1/11
to rails-...@googlegroups.com
Cool, thanks for the clarification :D

Chris Mayan

unread,
Feb 1, 2011, 1:19:15 AM2/1/11
to rails-...@googlegroups.com
Ok thanks

> It's definitely a synchronous call, in that the database has received the
> INSERT/UPDATE command before #save[!] returns.
>
> But, if you're in a transaction you won't be able to see the data in any
> other context until the database receives COMMIT when the transaction block
> closes.


So just confirming:

Suppose i Have

ARObject, ARObjectObserver, and ARObjectHistory (which is an audit
recording all history changes of an ARObject)

So in rough pseudo code,

#==ARObject
class ARObject < ActiveRecord::Base
has_many ARObjectHistories

def action()
ARObject.transaction do
self.status = UPDATED_STATUS
self.save!
self.ar_object_histories(true).find(# latest one just created)
# Can't find it... Even in NaviCat, even with the reload of
associations...
end
end

def add_history!(blah)
obj_hist = ARObjectHistory.new(blah)
obj_hist.ar_object = self
obj_hist.save!
end
end


# ==ARObjectObserver
class ARObjectObserver < ActiveRecord::Observer
def before_update(record)
blah = # History info etc..
record.add_history!(blah)
end
end

Is that right? I won't be able to see the latest history just created
from within the same transaction until it completes?

Thanks
Chris

P.s. yes to using MySQL :(

Xavier Shay

unread,
Feb 1, 2011, 2:05:11 AM2/1/11
to rails-...@googlegroups.com
On 1/02/11 5:19 PM, Chris Mayan wrote:
> Ok thanks
>
>> It's definitely a synchronous call, in that the database has received the
>> INSERT/UPDATE command before #save[!] returns.
>>
>> But, if you're in a transaction you won't be able to see the data in any
>> other context until the database receives COMMIT when the transaction block
>> closes.
>
>
> So just confirming:
>
> Suppose i Have
>
> ARObject, ARObjectObserver, and ARObjectHistory (which is an audit
> recording all history changes of an ARObject)
>
> So in rough pseudo code,
In real code:
https://gist.github.com/8e2403de7ad332c3c20b [1]

> Is that right? I won't be able to see the latest history just created
> from within the same transaction until it completes?

Not quite, you won't be able to see it in *other* transactions.

Running the above script says you can see your own inserts, as long as
you explicitly reload your association (since it gets cached). There are
circumstances where you won't be able to see them across transactions,
but that's perhaps out of scope for this question.

This also gives you a minimal test case to play around with to see
exactly when, what and where is executed.

Cheers,
Xavier

[1]
require 'logger'
require 'active_record'
require 'mysql2'

ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.configurations = {'development' => {'adapter' =>
'mysql2', 'user' => 'root', 'database' => 'test'}}
ActiveRecord::Base.establish_connection('development')

ActiveRecord::Schema.define :version => 0 do
create_table :ar_objects, :force => true do |t|
t.string :status
end

create_table :ar_object_histories, :force => true do |t|
t.references :ar_object
t.string :desc
end
end


class ArObject < ActiveRecord::Base
has_many :ar_object_histories

def action
transaction do
puts ar_object_histories.inspect
self.status = "actioned"
self.save
add_history!("Actioning")
puts ar_object_histories.inspect
puts ar_object_histories(true).inspect
end
end

def add_history!(desc)
obj_hist = ArObjectHistory.new(:desc => desc)


obj_hist.ar_object = self
obj_hist.save!
end
end

class ArObjectHistory < ActiveRecord::Base
belongs_to :ar_object
end

class ArObjectObserver < ActiveRecord::Observer
observe :ar_object

def before_update(record)
record.add_history!("observer yeah")
end
end

ActiveRecord::Base.observers = [ArObjectObserver]
ActiveRecord::Base.instantiate_observers


obj = ArObject.create!(:status => 'new')

obj.action

Chris Mayan

unread,
Feb 1, 2011, 2:54:51 AM2/1/11
to rails-...@googlegroups.com
:) :)

So in rough pseudo code,
In real code:
https://gist.github.com/8e2403de7ad332c3c20b [1]


Thanks Xavier , I'll have a play and see if the differences might be something else altogether.

 
Running the above script says you can see your own inserts, as long as you explicitly reload your association (since it gets cached). There are circumstances where you won't be able to see them across transactions, but that's perhaps out of scope for this question.

This also gives you a minimal test case to play around with to see exactly when, what and where is executed.



Cheers
Chris

Simon Russell

unread,
Feb 1, 2011, 6:16:34 PM2/1/11
to rails-...@googlegroups.com
I'm reasonably sure that depending on your MySQL transaction isolation
level, if you run the same query twice inside a transaction, it'll
return the same results -- even if you've inserted data that should
appear in the second query.

Look at repeatable read here:
http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html

(unless I'm just misinterpreting things)

Simon.

Xavier Shay

unread,
Feb 1, 2011, 6:31:01 PM2/1/11
to rails-...@googlegroups.com

On 2/02/11 10:16 AM, Simon Russell wrote:
> I'm reasonably sure that depending on your MySQL transaction isolation
> level, if you run the same query twice inside a transaction, it'll
> return the same results -- even if you've inserted data that should
> appear in the second query.
>
> Look at repeatable read here:
> http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
>
> (unless I'm just misinterpreting things)

You are not quite correct - you will always see data that you have
inserted, regardless of your isolation level [1].

Isolation levels are only relevant when talking about isolation between
different transactions.

Also repeatable read does allow you to see data from other transactions
in some circumstances (phantom reads), for full protection you need to
bump up to SERIALIZABLE (side note: this is why acts_as_list is broken
by default with mysql + rails). Please see the blog post I linked up in
a previous reply for more detail.

I encourage everyone to try out these different scenarios using minimal
test cases, it really helps you get your head around it.

Cheers,
Xavier

[1]
mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)

mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM users;
+----+
| id |
+----+
| 1 |
+----+
1 row in set (0.00 sec)

mysql> INSERT INTO users VALUES (2);
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM users;
+----+
| id |
+----+
| 1 |
| 2 |
+----+
2 rows in set (0.00 sec)

mysql> ROLLBACK;
Query OK, 0 rows affected, 1 warning (0.00 sec)

Xavier Shay

unread,
Feb 1, 2011, 6:36:50 PM2/1/11
to rails-...@googlegroups.com
P.S. I've been half thinking about running another "DB Is Your Friend"
course in Melbourne. Email me if you would be interested.

http://www.dbisyourfriend.com/

Xavier

Chris Mayan

unread,
Feb 1, 2011, 7:31:13 PM2/1/11
to rails-...@googlegroups.com
Hi All,

Just an update on this...

In the end there wasn't actually a problem (don't you hate that!)

So Save/Save! is most definitely synchronous - what was wrong was my ability to understand that inside a transaction the database state won't be visible to anything else _outside_ of that executing transaction lines of code (including your debugger!)...

So I use RadRails, and had mistakenly forgotten that under all the gloss and coat, the IDE itself when placing a watch variable _also_ can't see the changes as it is effectively outside the transaction. (Just like using NaviCat or another client on a different thread effectively)

I was a little puzzled by Shay's example (because his works), and mine didn't until I realised Shay was getting feedback as to what was happening by puts statements from within the transaction, whereas I was getting my feedback from the RadRails IDE which wasn't inside the transaction.

So all in all, a night's sleep and all your help made all the difference! Thanks everyone!

:)

Cheers
Chris

Simon Russell

unread,
Feb 1, 2011, 8:40:51 PM2/1/11
to rails-...@googlegroups.com
Yeah, you're right actually. Which makes sense, I didn't really look
into it much because I've generally used more isolated isolation
levels.
Reply all
Reply to author
Forward
0 new messages