描述一下我的需求: 假设场景为: 多张银行卡的金额和我唯一的存折金额是关联的,也就是如果我往银行卡 A 里存 100 元,储存成功后,存折帐号加 100 元。
然后我做这样的设计
class 银行卡
after_create :存折金额加入
def 存折金额加入
存折金额 += 银行卡存入金额
存折金额.save!
end
end
这样的写法可以确保存折存入金额后在执行存折金额的调整。
但是如果因为某些问题发生,存折金额存储失败了!也就是存折金额.save!
将会抛出异常,一旦异常是不是银行卡的存储也会失败?
也就是一旦存折存储出问题,银行卡的存储也必须返回。不然银行卡里存了钱,却在存折中不显示了。
请问正常的设计思路是这样的吗?
@QueXuQ after_create 这个方法本身,跟 create 这个行为,处于同一个数据库的 Transaction,也就是说,如果 after_create 执行失败,抛出异常的话,整个 create 这个行为的 Transaction 就会 rollback。这是有意这么设计的。具体这里会有详细的说明 http://rails-bestpractices.com/posts/695-use-after_commit
链接中解释了为什么用 after_commit 而不是 after_create 的原因。after_create 抛出异常的话,create 行为会 rollback,什么都不会保存。
这个在 console 下面非常容易佐证,生成一个 scaffold,几行代码,然后你自己在 console 测试一下便知。
Started POST "/products" for 127.0.0.1 at 2012-11-03 22:53:04 +0800
Processing by ProductsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"s0THldTdG2+EO+2fiU3yHH49A6LaYiktf9iBTxJVXRg=", "product"=>{"title"=>"Text", "description"=>"text"}, "commit"=>"Create Product"}
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
(0.1ms) begin transaction
SQL (9.8ms) INSERT INTO "products" ("created_at", "description", "title", "updated_at", "user_id") VALUES (?, ?, ?, ?, ?) [["created_at", Sat, 03 Nov 2012 14:53:04 UTC +00:00], ["description", "text"], ["title", "Text"], ["updated_at", Sat, 03 Nov 2012 14:53:04 UTC +00:00], ["user_id", 1]]
(0.2ms) rollback transaction
Completed 500 Internal Server Error in 21ms
RuntimeError ():
app/models/product.rb:8:in `judge_create'
app/controllers/products_controller.rb:54:in `block in create'
app/controllers/products_controller.rb:53:in `create'
确实是会 rollback,我想请问一下,怎么捕获这个异常呢?刚刚试一下:
after_create :judge_create
def judge_create
begin
raise
rescue
puts "Hello!"
end
end
如果这样捕获,就会让他顺利存下来了,可是异常是他这里出现的。 怎么样才可以使它
if @product.save
format.html { redirect_to @product, notice: 'Product was successfully created.' }
format.json { render json: @product, status: :created, location: @product }
else
format.html { render action: "new" }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
运行 else 那段,而不是页面出现异常呢?
create 方法设计成这样,不知道好不好?
def create
@product = @user.products.build(params[:product])
begin
@product.save
redirect_to @product, notice: 'Product was successfully created.'
rescue
render action: "new", notice: 'Product was failure created.'
end
end
@jjym 很想了解,这个大哥说的好方法是指怎么设计呢?
这种业务最好是加入一个 Transaction 表,里面记录进账,出帐,时间,等等相关信息,每一笔都有据可查。 而且这种业务性比较强的操作,个人认为最好不要放在 call back 里面。
有几个建议:
1, 不要用 ruby 来计算价格,用 SQL 银行卡.where(:id => 银行卡#id).update_all('存折金额 = 存折金额 + 银行卡存入金额')
2, 有 Transaction 表
3, 有 monitor, 定时检查价格对的上对不上
#14 楼 @zhangjinzhu 哦。如果用 SQL 进行计算是不是可以更加确保安全性? Transaction 表和 monitor 指的是什么东西呢?第一次接触,可以给些相关文献看看吗?
#16 楼 @zhangjinzhu 哦。那不怕 SQL 执行会失败吗?就像存折金额.save
出错而导致存储失败。
像你说的,update_all 也应该需要一个失败的 rollback 吧?
monitor 就是加上个监控,这个好像很复杂,是用 ruby 去写吗?实在没有接触过。
最近也有个类似的项目,也是一个扣款操作,在金额扣减上直接采用数据库来计算,但涉及到 N 个表的顺序保存,如果任何一个保存出错了,都要回滚。继续留意此帖。