最近因為專案的關係還有老闆的堅持,讓我重新好好的認識 Active Storage 這個 Rails 內建的圖片上傳工具,以及我遇到的坑然後怎樣解決的心得分享。
首先我們來談談好處吧(這樣才能騙你們一起用啊)
Active Storage 的安裝在 rails guide 或者是上 Youtube 上面搜尋就有一堆教學影片了(客倌自便)
這邊就不在囉唆,當你裝完之後呢, Active Storage 會開兩張 Table 一張是 Attachment 是儲存被夾帶檔案的那筆資料以及檔案本體之間的關聯表,而另外一張叫做 Blob 是儲存檔案本體,之後在想要影片或圖片上傳的 model 定義好關聯以及虛擬欄位的名子之後就可以使用了,然後會根據你取的虛擬欄位的名子會記錄在 Attachment 這張表裡面,之後只要有任何需要檔案上傳的地方只要在 model 定義好就一切沒問題了,然後你不管在哪個 model 上傳的檔案通通都會被歸到Attachment 以及 Blob 這兩張表裡面進而達到集中管理的效果。(難怪我老闆這麼愛)
那也因為是 Rails 內建的工具,所以其實未來在專案升級上面沒有相容性的問題,雖然我還沒碰過不過常聽到前輩們在哭天喊地 (哭夭) 說版本升級的話其實周邊的 gem 可能會因為相容性問題壞光光,最後一個好處就是 ... 安裝步驟簡單大概不到五分鐘就可以實現圖片上傳的功能了(符合快速開發的時代啊)
以下的用血與淚交織的故事,如果有更好的解法,在我的下面留言讓我知道一下
其實在 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 文件的時候了,可以用下列兩個方法解決
# 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
# app/controllers/users_controller.rb
class UsersController < ApplicationRecord
def index
@users = User.paginate(page: params[:page]).with_attached_avatar
end
end
其實兩個方法都達到一樣的效果,這樣我們天生 N + 1 的問題就解決啦!
當你某筆資料已經有上傳好的圖片而你想在加幾張圖片的時候,問題就出現了!已經存在的圖片會先被刪除在上傳你後來想加上的圖片,導致你之前的圖片不見了,這是為什麼?讓我們看看 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 裡面設定
很重要所以說三次!
加上這條之後終於一切都完美了!
以上就是目前工作上使用 ActiveStorage 遇到的問題,其實都有解決的方法,只期望你會發現它們而已XD,如果你!也有什麼血淋淋的故事的歡迎在下面留言讓我知道一下!