美文网首页
ruby on rails 事务处理

ruby on rails 事务处理

作者: 小新是个程序媛 | 来源:发表于2018-04-23 17:47 被阅读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

    相关文章

      网友评论

          本文标题:ruby on rails 事务处理

          本文链接:https://www.haomeiwen.com/subject/eqltlftx.html