transaction的巧用
前几天有个已经上线的项目出现了 exception
,问题出现在创建用户的时候。该项目是使用微信登录的。是利用微信的回调数据来创建新用户。项目的用户模型分了 User
和 Profile
,用户的详细信息都记录在 Profile
里。
用户创建的部分代码如下:
unionid = auth.extra.raw_info.unionid
user = User.find_or_create_by! wechat_unionid: unionid
unless user.profile
info = auth.info
user.create_profile! name: info.nickname, remote_avatar_url: info.headimgurl
end
其中 auth.extra.raw_info
就是微信回调的用户信息。
这个 exception
出现在创建 profile
时,由于某种原因,头像 avatar
验证失败了。
ActiveRecord::RecordInvalid (验证失败: Avatar trying to download a file which is not served over HTTP):
这就导致了一个问题,这位用户的 user
已经成功创建,但相应的 profile
却创建失败,而用户名、用户头像等信息都是存在 profile
里的,就相当于出现了一个信息不完整的空用户,导致前台有些页面报错了。
查了很久原因,因为只有一位用户出现这种情况,有一个可能的原因是那时微信服务器出现问题,返回的 info.headimgurl
地址有问题,导致头像保存失败,进而使得 profile
创建失败。
问题如何解决呢?
避免错误再次发生
如果真的是微信服务器的问题,这是我们无法控制的。但问题出现的真正原因在于,user
创建成功而 profile
创建失败。user
和 profile
应该是相互依存的,即要么同时创建成功,要么同时创建失败。所以,避免问题再次发生的办法就是用 transaction
把这个两个创建过程包起来。
ActiveRecord::Base.transaction do
user = User.find_or_create_by! wechat_unionid: unionid
unless user.profile
info = auth.info
user.create_profile! name: info.nickname, remote_avatar_url: info.headimgurl
end
end
这样一来,不管由于什么原因,profile
创建失败时,也会导致 user
创建失败,就避免了出现空数据的 user
。
删除已产生的空数据
项目的数据库中已经产生一个 user
的空数据,当这位用户想要登录的时候,会出现错误,但又无法重新创建新用户。所以,要把这条数据删掉,好让他再次登录时可以重新创建。
想当然地,我就登录服务器后台,准备用 destroy!
方法直接把那条数据删除。但是,一位同事走过来阻止了我,抢过我的键盘,默默地敲了这几行代码,然后又默默地回到自己的位置上去了。
这几行代码是这样的
ActiveRecord::Base.transaction { u.destroy!; raise }
得到的结果是
(0.3ms) BEGIN
UserProfile Load (0.4ms) SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = $1 LIMIT $2 [["user_id", 208], ["LIMIT", 1]]
UserWechatInfo Load (0.2ms) SELECT "user_wechat_infos".* FROM "user_wechat_infos" WHERE "user_wechat_infos"."user_id" = $1 [["user_id", 208]]
UserAssignmentAnswer Load (0.3ms) SELECT "user_assignment_answers".* FROM "user_assignment_answers" WHERE "user_assignment_answers"."user_id" = $1 [["user_id", 208]]
SQL (2.0ms) DELETE FROM "users" WHERE "users"."id" = $1 [["id", 208]]
(0.3ms) ROLLBACK
RuntimeError:
from (irb):13:in `block in irb_binding'
from (irb):13
然后再敲
ActiveRecord::Base.transaction { u.destroy! }
这才成功把数据删除了。
我坐在那里,看了好一会这两条命令,才明白过来。对于上线项目,删除数据要慎之又慎。必须确保删除的数据不会引起其他数据的丢失,才可以删除。这位有经验的同事,在真正删除数据之前,实际上是用 transaction
方法做了一次模拟删除。
在第一行命令里,把 u.destroy!
和 raise
这两个操作包在了同一个 transaction
里。实际上,raise
这个命令是无效操作,目的就是要让这个 transaction
最后操作失败,然后再 rollback
回到原始状态。这个时候,就能从返回结果里看到 u.destroy!
这个命令所引起的所有数据库变动。
从上面的例子可以看出,删除这个用户信息时,可能会引起变动的 model 还有 UserProfile
、UserWechatInfo
、UserAssignmentAnswer
。
当确保这个删除操作不引起其他问题之后,在来执行 u.destroy!
。
总结
- 用
transaction
将两个相互依存的操作包起来,可以避免一个操作成功而另一个操作失败的情况; - 用
transaction
将一个操作和另一个无效操作包起来,可以达到模拟操作的效果,用来检查该操作是否安全。