transaction的巧用

前几天有个已经上线的项目出现了 exception,问题出现在创建用户的时候。该项目是使用微信登录的。是利用微信的回调数据来创建新用户。项目的用户模型分了 UserProfile,用户的详细信息都记录在 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 创建失败。userprofile 应该是相互依存的,即要么同时创建成功,要么同时创建失败。所以,避免问题再次发生的办法就是用 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 还有 UserProfileUserWechatInfoUserAssignmentAnswer

当确保这个删除操作不引起其他问题之后,在来执行 u.destroy!

总结

  1. transaction将两个相互依存的操作包起来,可以避免一个操作成功而另一个操作失败的情况;
  2. transaction 将一个操作和另一个无效操作包起来,可以达到模拟操作的效果,用来检查该操作是否安全。
· rails