这几阵子我遇到的坑 For Active Storage

最近因为专案的关系还有老板的坚持,让我重新好好的认识 Active Storage 这个 Rails 内建的图片上传工具,以及我遇到的坑然後怎样解决的心得分享。

为什麽要使用 Active Storage 好处是?

首先我们来谈谈好处吧(这样才能骗你们一起用啊)

  1. 方便管理
  2. 专案升级没烦恼
  3. 简单好用
  4. 请大大想到在下面帮我补充

Active Storage 的安装在 rails guide 或者是上 Youtube 上面搜寻就有一堆教学影片了(客倌自便)
这边就不在罗唆,当你装完之後呢, Active Storage 会开两张 Table 一张是 Attachment 是储存被夹带档案的那笔资料以及档案本体之间的关联表,而另外一张叫做 Blob 是储存档案本体,之後在想要影片或图片上传的 model 定义好关联以及虚拟栏位的名子之後就可以使用了,然後会根据你取的虚拟栏位的名子会记录在 Attachment 这张表里面,之後只要有任何需要档案上传的地方只要在 model 定义好就一切没问题了,然後你不管在哪个 model 上传的档案通通都会被归到Attachment 以及 Blob 这两张表里面进而达到集中管理的效果。(难怪我老板这麽爱)

那也因为是 Rails 内建的工具,所以其实未来在专案升级上面没有相容性的问题,虽然我还没碰过不过常听到前辈们在哭天喊地 (哭夭) 说版本升级的话其实周边的 gem 可能会因为相容性问题坏光光,最後一个好处就是 ... 安装步骤简单大概不到五分钟就可以实现图片上传的功能了(符合快速开发的时代啊)

工作上的坑

以下的用血与泪交织的故事,如果有更好的解法,在我的下面留言让我知道一下

N + 1

其实在 model 设定关联的时候,如下:

# app/models/user.rb 
class User < ApplicationRecord
  has_one_attached :avatar, dependent: :destroy
end

在背後其实还有帮我们加上两条关联,如下:

has_one :avatar_attachment, -> { where(name: "avatar") }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy
has_one :avatar_blob, through: :avatar_attachment, class_name: "ActiveStorage::Blob", source: :blob

所以当我们要去捞 user 上传的图片的时候, Attachment 跟 blob 这两个 model 就会一起被查询

irb(main):031:0> User.first.avatar.filename
  User Load (3.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  ActiveStorage::Attachment Load (1.5ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ?  [["record_id", 2], ["record_type", "User"], ["name", "avatar"], ["LIMIT", 1]]
  ActiveStorage::Blob Load (0.4ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = ? LIMIT ?  [["id", 6], ["LIMIT", 1]]
=> #<ActiveStorage::Filename:0x00007feee8bd4758 @filename="dl.jpg">

而 N + 1 出现的时机是当你想要处理多笔资料的时候,如下:

# app/controllers/users_controller.rb 
class UsersController < ApplicationRecord
  def index
    @users = User.paginate(page: params[:page])
  end
end

# app/views/users/index.html.erb

<% @users.each do |user| %>
  <%= image_tag user.avatar %>    
<% end %>

当你这样做的时候,你的 log 应该会像是 ...

ActiveStorage::Attachment Load (0.8ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ?  [["record_id", 2], ["record_type", "User"], ["name", "avatar"], ["LIMIT", 1]]
    ↳ app/views/users/index.html.erb:2
    ActiveStorage::Blob Load (0.3ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = ? LIMIT ?  [["id", 6], ["LIMIT", 1]]
    ↳ app/views/users/index.html.erb:2
    ActiveStorage::Attachment Load (0.2ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ?  [["record_id", 3], ["record_type", "User"], ["name", "avatar"], ["LIMIT", 1]]
    ↳ app/views/users/index.html.erb:2
    ActiveStorage::Blob Load (0.3ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = ? LIMIT ?  [["id", 7], ["LIMIT", 1]]
    ↳ app/views/users/index.html.erb:2
    ActiveStorage::Attachment Load (0.1ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ?  [["record_id", 4], ["record_type", "User"], ["name", "avatar"], ["LIMIT", 1]]
    ↳ app/views/users/index.html.erb:2
    ActiveStorage::Blob Load (0.1ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = ? LIMIT ?  [["id", 8], ["LIMIT", 1]]
    ↳ app/views/users/index.html.erb:2
    ActiveStorage::Attachment Load (0.2ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ?  [["record_id", 5], ["record_type", "User"], ["name", "avatar"], ["LIMIT", 1]]
    ↳ app/views/users/index.html.erb:2
    ActiveStorage::Blob Load (0.2ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = ? LIMIT ?  [["id", 9], ["LIMIT", 1]]
    ↳ app/views/users/index.html.erb:2

这时候你心里就会 mur mur ,方便是方便但是这也太效能杀手了吧!不过凡事总是有解决方法的这时候就会考验 SQL 以及平常有没有认真看官方 API 文件的时候了,可以用下列两个方法解决

  1. includes
    可以在捞 user 的时候一并把图片或档案资料一起捞出来,像是
# app/controllers/users_controller.rb 
class UsersController < ApplicationRecord
  def index 
    @users = User.paginate(page: params[:page]).includes(avatar_attachment: :blob)
  end
end
# 如果你是 has_many_attached 的话,就在後面加个 s 就好了,像是 avatars_attachments
  1. with_attached_#{name}
    官方文件提供的方法,如下
# app/controllers/users_controller.rb 
class UsersController < ApplicationRecord
  def index 
    @users = User.paginate(page: params[:page]).with_attached_avatar
  end
end

其实两个方法都达到一样的效果,这样我们天生 N + 1 的问题就解决啦!

Replace Attachment

当你某笔资料已经有上传好的图片而你想在加几张图片的时候,问题就出现了!已经存在的图片会先被删除在上传你後来想加上的图片,导致你之前的图片不见了,这是为什麽?让我们看看 source code!

# active_storage/attached/model.rb

def #{name}=(attachables)
  if ActiveStorage.replace_on_assign_to_many
    attachment_changes["#{name}"] =
      if Array(attachables).none?
        ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
      else
        ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
      end
  else
     if Array(attachables).any?
       attachment_changes["#{name}"] =
         ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables)
     end
  end
end

可以看到上面的 source code 在 has_many_attached 的时候他有一套判断的机制,然後我们在看看 rails/application/configuration.rb 里面有一行是
active_storage.replace_on_assign_to_many = true

所以他永远都会跑 true 那条路把你的档案先删掉在上传,这个在任何文件里面都没提到过(至少目前我查到的都没有啦XD) 那解决的方法就是在我们的 cofig/application.rb 里面设定

active_storage.replace_on_assign_to_many = false

active_storage.replace_on_assign_to_many = false

active_storage.replace_on_assign_to_many = false

很重要所以说三次!
加上这条之後终於一切都完美了!
以上就是目前工作上使用 ActiveStorage 遇到的问题,其实都有解决的方法,只期望你会发现它们而已XD,如果你!也有什麽血淋淋的故事的欢迎在下面留言让我知道一下!


<<:  风险的决策应在投资评估过程中行使

>>:  NIST SP 800-88 R1媒体消毒准则(Guidelines for Media Sanitization)

Day 3 就是你了!

有时候,我们都太天真的想像着美好,然而降临我们面前的不只是美好,有时是想不到的冲突,或者双方同时出现...

[Day - 25] - Spring Reactor Processor 之交易所OrderBook实作与设计

Abstract 好的,我们已先行叙述过Flux及Mano两项角色套件,最後,我们开始进行介绍Rea...

既熟悉又陌生的字元集与比较规则

我们都知道电脑实际储存的是二进位资料,那是怎麽储存字元的呢? 可以想像的就是必须让字元映射成二进位资...

[Android Studio 30天自我挑战] 完赛心得

完赛心得 今天是铁人赛的最後一天, 一开始以为要写30篇的技术文章有点太难了, 为了写不一样的内容让...

Youtube API - 凡事趁早没有那麽多来日方长(总结)

『理工男嘛,就是有问题要解决问题,没问题那就创造问题』,在做为一个数据分析师之前,我本身的大学所学的...