Rails 3 - 事务和锁定

发布于 2024-11-03 14:20:22 字数 4234 浏览 1 评论 0原文

我是 Rails 新手,有一个需要处理事务的系统。用户可以输入与多个用户相关联的交易。这些用户欠进行交易的人一定数额的钱。例如,Bill 可能为 4 个朋友买午餐,账单为 125 美元。他们决定将账单分成 5 种,因此每人欠 25 美元。 Bill 将输入总计 125 美元,并输入每个朋友(包括他自己)因该交易欠下的 25 美元。 我的控制器和模型中有实现此目标的代码,但我真的不知道我是否正确使用事务和锁定。另外,这是在控制器中保存此信息的最终方式吗?我正在使用事务,因为所有这些操作必须一起发生或失败(原子性),并且我需要锁定,以防多个用户尝试在同时(隔离)。也许我应该让后端的数据库处理锁定?它已经这样做了吗——比如MySQL?谢谢。

trans_controller.rb

class TransController < ApplicationController
    # POST trans/
    def create
        @title = "Create Transaction"
        trans_successful = false
        
        # Add the transaction from the client
        @tran = Tran.new(params[:tran])

        # Update the current user
        @tran.submitting_user_id = current_user.id
        
        # Update the data to the database
        # This call commits the transaction and transaction users 
        # It also calls a method to update the balances of each user since that isn't
        # part of the regular commit (why isn't it?)
        begin 
            @tran.transaction do
                @tran.save! 
                @tran.update_user_balances
                trans_successful = true
            end 
        rescue
            
        end
        
        # Save the transaction
        if trans_successful
            flash[:success] = 'Transaction was successfully created.'
            redirect_to trans_path
        else
            flash.now[:error] = @tran.errors.full_messages.to_sentence          
            render 'new'
        end
    end

tran.rb

class Tran < ActiveRecord::Base
    has_many :transaction_users, :dependent => :destroy, :class_name => 'TransactionUser'
    belongs_to :submitting_user, :class_name => 'User'
    belongs_to :buying_user, :class_name => 'User'
    
    accepts_nested_attributes_for :transaction_users, :allow_destroy => true

    validates :description, :presence => true,
                            :length => {:maximum => 100 }
    #validates :total,      :presence => true
    validates_numericality_of :total, :greater_than => 0
    
    validates :submitting_user_id,      :presence => true               
    validates :buying_user_id,          :presence => true   
            
    #validates_associated :transaction_users
    
    validate :has_transaction_users?
    validate :has_correct_transaction_user_sum?
    validate :has_no_repeat_users?
    
    def update_user_balances
        # Update the buying user in the transaction
        self.buying_user.lock!
        # Update the user's total, since they were in the transction
        self.buying_user.update_attribute :current_balance, self.buying_user.current_balance - self.total
        # Add an offsetting transaction_user for this record
        buying_tran_user = TransactionUser.create!(:amount => -1 * self.total, :user_id => self.buying_user_id, :tran => self)
        #if buying_tran_user.valid?
        #   raise "Error"
        #end
        
        # Loop through each transaction user and update their balances.  Make sure to lock each record before doing the update.
        self.transaction_users.each do |tu|
            tu.user.lock!
            tu.user.update_attribute :current_balance, tu.user.current_balance + tu.amount
        end
    end
    
    def has_transaction_users?
        errors.add :base, "A transcation must have transaction users." if self.transaction_users.blank?
    end
    
    def has_correct_transaction_user_sum?
        sum_of_items = 0;
        
        self.transaction_users.inspect
        self.transaction_users.each do |key|
            sum_of_items += key.amount if !key.amount.nil?
        end
        
        if sum_of_items != self.total
            errors.add :base, "The transcation items do not sum to the total of the transaction." 
        end 
    end
     
    def has_no_repeat_users?
        user_array = []
        self.transaction_users.each do |key|
            if(user_array.include? key.user.email) 
                errors.add :base, "The participant #{key.user.full_name} has been listed more than once."
            end
        
            user_array << key.user.email
        end
    end 
end

I am new to Rails and have a system that needs to process transactions. A user can enter a transaction to which one more users are tied. These users owe some amount of money to the person making the transaction. For example, Bill might buy lunch for 4 friends and the bill is $125. They decide to split the bill 5 ways, so each owes $25. Bill would enter a total of $125 and enter each friend (including himself) as owing $25 to the transaction. I have code in my controller and in my model to accomplish this goal, but I don't really know if I am using transactions and locking correctly. Also, is this the entended way to have this information in the controller? I am using a transaction since all of these actions must occur together or fail (atomicity) and I need locking in case multiple users try to submit at the same time (isolation). Maybe I should let the db on the backend handle locking? Does it do that already - say, MySQL? Thanks.

trans_controller.rb

class TransController < ApplicationController
    # POST trans/
    def create
        @title = "Create Transaction"
        trans_successful = false
        
        # Add the transaction from the client
        @tran = Tran.new(params[:tran])

        # Update the current user
        @tran.submitting_user_id = current_user.id
        
        # Update the data to the database
        # This call commits the transaction and transaction users 
        # It also calls a method to update the balances of each user since that isn't
        # part of the regular commit (why isn't it?)
        begin 
            @tran.transaction do
                @tran.save! 
                @tran.update_user_balances
                trans_successful = true
            end 
        rescue
            
        end
        
        # Save the transaction
        if trans_successful
            flash[:success] = 'Transaction was successfully created.'
            redirect_to trans_path
        else
            flash.now[:error] = @tran.errors.full_messages.to_sentence          
            render 'new'
        end
    end

tran.rb

class Tran < ActiveRecord::Base
    has_many :transaction_users, :dependent => :destroy, :class_name => 'TransactionUser'
    belongs_to :submitting_user, :class_name => 'User'
    belongs_to :buying_user, :class_name => 'User'
    
    accepts_nested_attributes_for :transaction_users, :allow_destroy => true

    validates :description, :presence => true,
                            :length => {:maximum => 100 }
    #validates :total,      :presence => true
    validates_numericality_of :total, :greater_than => 0
    
    validates :submitting_user_id,      :presence => true               
    validates :buying_user_id,          :presence => true   
            
    #validates_associated :transaction_users
    
    validate :has_transaction_users?
    validate :has_correct_transaction_user_sum?
    validate :has_no_repeat_users?
    
    def update_user_balances
        # Update the buying user in the transaction
        self.buying_user.lock!
        # Update the user's total, since they were in the transction
        self.buying_user.update_attribute :current_balance, self.buying_user.current_balance - self.total
        # Add an offsetting transaction_user for this record
        buying_tran_user = TransactionUser.create!(:amount => -1 * self.total, :user_id => self.buying_user_id, :tran => self)
        #if buying_tran_user.valid?
        #   raise "Error"
        #end
        
        # Loop through each transaction user and update their balances.  Make sure to lock each record before doing the update.
        self.transaction_users.each do |tu|
            tu.user.lock!
            tu.user.update_attribute :current_balance, tu.user.current_balance + tu.amount
        end
    end
    
    def has_transaction_users?
        errors.add :base, "A transcation must have transaction users." if self.transaction_users.blank?
    end
    
    def has_correct_transaction_user_sum?
        sum_of_items = 0;
        
        self.transaction_users.inspect
        self.transaction_users.each do |key|
            sum_of_items += key.amount if !key.amount.nil?
        end
        
        if sum_of_items != self.total
            errors.add :base, "The transcation items do not sum to the total of the transaction." 
        end 
    end
     
    def has_no_repeat_users?
        user_array = []
        self.transaction_users.each do |key|
            if(user_array.include? key.user.email) 
                errors.add :base, "The participant #{key.user.full_name} has been listed more than once."
            end
        
            user_array << key.user.email
        end
    end 
end

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

書生途 2024-11-10 14:20:22

我会避免手动执行锁定,因为 mysql 将在事务内正确处理必要的行级锁定。在这种情况下使用事务是正确的。我要避免的是创建一个局部变量来跟踪事务是否完成而没有错误:

def create
    @title = "Create Transaction"

    # Add the transaction from the client
    @tran = Tran.new(params[:tran])

    # Update the current user
    @tran.submitting_user_id = current_user.id

    # Update the data to the database
    # This call commits the transaction and transaction users 
    # It also calls a method to update the balances of each user since that isn't
    # part of the regular commit (why isn't it?)
    begin 
        @tran.transaction do
            @tran.save! 
            @tran.update_user_balances
            trans_successful = true
        end 
    rescue
        flash.now[:error] = @tran.errors.full_messages.to_sentence          
        render 'new'
    else
        flash[:success] = 'Transaction was successfully created.'
        redirect_to trans_path
    end
end

I would avoid doing the locks manually as mysql will handle the necessary row level locking correctly inside the transaction. Using the transaction is correct in this case. What I would avoid is creating a local variable to keep track of the whether the transaction was completed without errors:

def create
    @title = "Create Transaction"

    # Add the transaction from the client
    @tran = Tran.new(params[:tran])

    # Update the current user
    @tran.submitting_user_id = current_user.id

    # Update the data to the database
    # This call commits the transaction and transaction users 
    # It also calls a method to update the balances of each user since that isn't
    # part of the regular commit (why isn't it?)
    begin 
        @tran.transaction do
            @tran.save! 
            @tran.update_user_balances
            trans_successful = true
        end 
    rescue
        flash.now[:error] = @tran.errors.full_messages.to_sentence          
        render 'new'
    else
        flash[:success] = 'Transaction was successfully created.'
        redirect_to trans_path
    end
end
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文