ruby on rails 事务处理

2018-04-23  本文已影响312人  小新是个程序媛

Rails中的事务

1. 使用事务的原因

保证应用中数据一致性,在有多条sql语句需要执行的时候,可以确保要么全部执行要么不执行,原理是事务中的语句如果异常就会重置所有操作
(一个功能点可能涉及到对多个表的操作,期间可能会发生某一个操作报错导致数据的不一致性,这时就可以使用事务来确保数据的一致性,常见使用场景如银行转账,伪代码如下)

ActiveRecord::Base.transaction do
  david.withdrawal(100) #从转账人账户-100
  mary.deposit(100) #从接收人账户+100
end

在rails中实现事务可以通过ActiveRecord对象的类方法或实例方法

#类方法
Client.transaction do
  @client.users.create!
  @user.clients(true).first.destroy!
  Product.first.destroy!
end

#实例方法
@client.transaction do
  @client.users.create!
  @user.clients(true).first.destroy!
  Product.first.destroy!
end

可以看到上面的例子中,每个事务中均含有多个不同的 model 。在同一个事务中调用多个 model 对象是常见的行为,因为事务是和一个数据库连接绑定在一起的,而不是某个 model 对象;而同时,也只有在对多个纪录进行操作,并且希望这些操作作为一个整体的时候,事务才是必要的.
另外,Rails 已经把类似 #save 和 #destroy 的方法包含在一个事务中了,因此,对于单条数据库记录来说,不需要再使用显式的调用了。
[意思是当事务只包含一个操作时(例如删除一条记录),可以不用按如上格式写,因为rails中默认删除一条记录语句就是一个只包含一条操作的事务]

image.png

上图表示一条创建语句其实就是一个事务,被包含在BEGIN 和 COMMIT中

2. 触发事务回滚

事务通过回滚重置记录状态,在rails中回滚只会被异常触发,这很关键,很多事务中的代码出错也不会触发异常,所以即使出错也不会回滚,例如

ActiveRecord::Base.transaction do 
  david.update_attribute(:amount, david.amount -100)
  mary.update_attribute(:amount, 100)
end

原因是在rails中update_attribute方法在调用失败时也不会触发异常而是返回false,因此要保证transaction中的语句出错时会抛出异常,可以改写如下

ActiveRecord::Base.transaction do 
  david.update_attribute!(:amount, david.amount -100)
  mary.update_attribute!(:amount, 100)
end

注:带感叹号的方法一般为爆炸方法,在失败时会抛出异常

同时在一些代码中会看到使用find_by方法,实际上find_by是魔术方法,当找不到记录时会返回nil,而使用find方法再找不到记录时才会抛出异常(ActiveRecord::RecordNotFound)

ActiveRecord::Base.transaction do
  david = User.find_by_name("david")
  if(david.try(:id) != john.id)
    john.update_attributes!(:amount => -100)
    mary.update_attributes!(:amount => 100)
  end
end

如上事务处理的代码就有错误,find_by_name方法即使没有这条记录也不会抛出异常,而是返回nil,而try方法会使调用即使为nil的对象时也不会抛出异常而是返回nil,所以会导致记录没有被找到的错误被隐藏,并且下面的代码被错误的执行.因此这就意味着在某些情况下,需要手动抛出异常,改写代码如下

ActiveRecord::Base.transaction do
  david = User.find_by_name("david")
  raise ActiveRecord::RecordNotFound if david.blank?
  if(david.try(:id) != john.id)
    john.update_attributes!(:amount => -100)
    mary.update_attributes!(:amount => 100)
  end
end

Object#try 它有什么作用昵,它可以让我们调用一个对象不用担心这个对象是否为 nil,因此抛出异常。 如何使用它,如下

"HELLO WORLD".try(:downcase)
=> "hello world"

3. 特殊异常

ActiveRecord::Rollback
当它被抛出时,事务本身会回滚,但是它并不会被重新抛出,因此你也不需要在外部进行 catch 和处理。

4. 嵌套事务

正常嵌套事务

User.transaction do
  User.create!(:user_name => 'Kotori')
  User.transaction do
    User.create!(:user_name => 'Nemu')
    raise Exception
  end
end
=>两个都不会被创建
=>抛Exception 异常时,子事务被回滚;同时父事务也能捕捉到此异常,因此父事务也被回滚了;

错误使用或者过多使用嵌套异常是比较常见的错误。当你把一个 transaction 嵌套在另外一个事务之中时,就会存在父事务和子事务,这种写法有时候会导致奇怪的结果,如下

User.transaction do
  User.create!(:user_name => 'Kotori')
  User.transaction do
    User.create!(:user_name => 'Nemu')
    raise ActiveRecord::Rollback
  end
end
=>Nemu会创建,Kotori也会创建
=>抛ActiveRecord::Rollback时不会传播到上层的方法中去,因此这个例子中,父事务并不会收到子事务抛出的异常。因为子事务块中的内容也被合并到了父事务中去,因此这个例子中,两条 User 记录都会被创建!

为了保证一个子事务的 rollback 被父事务知晓,必须手动在子事务中添加 :require_new => true 选项

User.transaction do
  User.create!(:user_name => 'Kotori')
  User.transaction(:requires_new=>true) do
    User.create!(:user_name => 'Nemu')
    raise ActiveRecord::Rollback
  end
end
=>Nemu会创建,Kotori不会创建,虽然使用了requires_new=>true
=>抛ActiveRecord::Rollback异常时子事务被回滚;但是异常不会被父事务捕捉,父事务正常的执行了;
User.transaction do
  User.create!(:user_name => 'Kotori')
  User.create!(:user_name => 'Nemu')
  raise ActiveRecord::Rollback
  end
end
=>Nemu不会创建,Kotori也不会创建
=>没有嵌套,事务全部回滚;

5. 数据库绑定

事务是跟当前的数据库连接绑定的,因此,如果你的应用同时向多个数据库进行写操作,那么必须把代码包裹在一个嵌套事务中去。比如:

Client.transaction do
  Product.transaction do
    product.buy(@quantity)
    client.update_attributes!(:sales_count => @sales_count + 1)
  end
end

6. 事务回调

上面提到 save 和 destroy 方法被自动包裹在一个事务中,因此相关的回调,比如 after_save 仍然属于事务的一部分,因此回调代码也有可能被回滚(回调代码失败也使事务进行回滚)。如果希望代码在事务外部执行的话可以使用after_commit或after_rollback这样的回调函数

7.事务陷阱

不要在事务内部去捕捉 ActiveRecord::RecordInvalid 异常。因为某些数据库下,这个异常会导致事务失效,比如 Postgres。一旦事务失效,要想让代码正确工作,就必须从头重新执行事务。

另外,测试回滚或者事务回滚相关的回调时,最好关掉 transactional_fixtures 选项,一般的测试框架中,这个选项是打开的。

8.常见的用以避免的反模式

a. 单条记录操作时使用事务
b. 不必要的使用嵌套式事务s
c. 事务中的代码不会导致回滚
d. 在 controller 中使用事务

参考:Transactions in Rails

上一篇下一篇

猜你喜欢

热点阅读