1. Giớ thiệu
Nếu như trước phiên bản Rails 5.2, để thực hiện việc upload file thì hầu hết ta luôn cần đến thư viện bên thứ 3, một trong số đó nổi tiếng nhất là gem CarrierWave – đó là một gem rất bền vững và được cộng đồng tin dùng nhiều nhất. Nhưng từ Rails 5.2 trở đi thì ta có thêm một lựa chọn khác là Active Storage. Không giống như CarrierWave, Active Storage được tích hợp sẵn trong Rails hay nói cách khác nó được phát triển bỡi Rails, vì vậy ta hoàn toàn yên tâm khi sữ dụng
2. Cài đặt
Migration
Mặc dù Active Storage được tích hợp sẵn trong Rails nhưng default nó chưa được install khi chạy rails new. Để install Active Storage ta chạy câu lênh sau:
1 2 3 | rails active_storage<span class="token symbol">:install</span> rails db<span class="token symbol">:migrate</span> |
Sau khi chạy hai câu lệnh trên thì nó sẽ tạo cho ta hai table trong DB có tên active_storage_blobs và active_storage_attachments. Về mặc lưu trữ dữ liệu thực chất Active Storage sữ dụng quan hệ Polymorphic trong rails, active_storage_blobs chứa thông tin của files, còn active_storage_attachments là một join table polymorphic lưu class name của model.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <span class="token keyword">class</span> <span class="token class-name">CreateActiveStorageTables</span> <span class="token operator"><</span> <span class="token constant">ActiveRecord</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Migration</span><span class="token punctuation">[</span><span class="token number">5.2</span><span class="token punctuation">]</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">change</span></span> create_table <span class="token symbol">:active_storage_blobs</span> <span class="token keyword">do</span> <span class="token operator">|</span>t<span class="token operator">|</span> t<span class="token punctuation">.</span>string <span class="token symbol">:key</span><span class="token punctuation">,</span> null<span class="token punctuation">:</span> <span class="token boolean">false</span> t<span class="token punctuation">.</span>string <span class="token symbol">:filename</span><span class="token punctuation">,</span> null<span class="token punctuation">:</span> <span class="token boolean">false</span> t<span class="token punctuation">.</span>string <span class="token symbol">:content_type</span> t<span class="token punctuation">.</span>text <span class="token symbol">:metadata</span> t<span class="token punctuation">.</span>bigint <span class="token symbol">:byte_size</span><span class="token punctuation">,</span> null<span class="token punctuation">:</span> <span class="token boolean">false</span> t<span class="token punctuation">.</span>string <span class="token symbol">:checksum</span><span class="token punctuation">,</span> null<span class="token punctuation">:</span> <span class="token boolean">false</span> t<span class="token punctuation">.</span>datetime <span class="token symbol">:created_at</span><span class="token punctuation">,</span> null<span class="token punctuation">:</span> <span class="token boolean">false</span> t<span class="token punctuation">.</span>index <span class="token punctuation">[</span> <span class="token symbol">:key</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> unique<span class="token punctuation">:</span> <span class="token boolean">true</span> <span class="token keyword">end</span> create_table <span class="token symbol">:active_storage_attachments</span> <span class="token keyword">do</span> <span class="token operator">|</span>t<span class="token operator">|</span> t<span class="token punctuation">.</span>string <span class="token symbol">:name</span><span class="token punctuation">,</span> null<span class="token punctuation">:</span> <span class="token boolean">false</span> t<span class="token punctuation">.</span>references <span class="token symbol">:record</span><span class="token punctuation">,</span> null<span class="token punctuation">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> polymorphic<span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> index<span class="token punctuation">:</span> <span class="token boolean">false</span> t<span class="token punctuation">.</span>references <span class="token symbol">:blob</span><span class="token punctuation">,</span> null<span class="token punctuation">:</span> <span class="token boolean">false</span> t<span class="token punctuation">.</span>datetime <span class="token symbol">:created_at</span><span class="token punctuation">,</span> null<span class="token punctuation">:</span> <span class="token boolean">false</span> t<span class="token punctuation">.</span>index <span class="token punctuation">[</span> <span class="token symbol">:record_type</span><span class="token punctuation">,</span> <span class="token symbol">:record_id</span><span class="token punctuation">,</span> <span class="token symbol">:name</span><span class="token punctuation">,</span> <span class="token symbol">:blob_id</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> name<span class="token punctuation">:</span> <span class="token string">"index_active_storage_attachments_uniqueness"</span><span class="token punctuation">,</span> unique<span class="token punctuation">:</span> <span class="token boolean">true</span> t<span class="token punctuation">.</span>foreign_key <span class="token symbol">:active_storage_blobs</span><span class="token punctuation">,</span> column<span class="token punctuation">:</span> <span class="token symbol">:blob_id</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Setting
Chúng ta thực hiện Khai báo các dịch vụ Active Storage trong config/storage.yml. Đối với mỗi dịch vụ mà ứng dụng của ta sử dụng, hãy cung cấp tên và cấu hình cần thiết. Ví dụ dưới đây khai báo ba dịch vụ có tên local, test và amazon:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | local<span class="token punctuation">:</span> service<span class="token punctuation">:</span> <span class="token constant">Disk</span> root<span class="token punctuation">:</span> <span class="token operator"><</span><span class="token string">%= Rails.root.join("storage") %> test: service: Disk root: <%=</span> <span class="token constant">Rails</span><span class="token punctuation">.</span>root<span class="token punctuation">.</span>join<span class="token punctuation">(</span><span class="token string">"tmp/storage"</span><span class="token punctuation">)</span> <span class="token operator">%</span><span class="token operator">></span> amazon<span class="token punctuation">:</span> service<span class="token punctuation">:</span> <span class="token constant">S3</span> access_key_id<span class="token punctuation">:</span> <span class="token string">""</span> secret_access_key<span class="token punctuation">:</span> <span class="token string">""</span> bucket<span class="token punctuation">:</span> <span class="token string">""</span> region<span class="token punctuation">:</span> <span class="token string">""</span> <span class="token comment"># e.g. 'us-east-1'</span> |
Để cho Active Storage biết dịch vụ nào được sử dụng bằng cách thiết lập Rails.application.config.active_storage.service tương ứng với mỗi môi trường bởi vì mỗi môi trường có thể sẽ sử dụng một dịch vụ khác nhau.
Ví dụ để sử dụng dịch vụ Disk trong môi trường development, ta sẽ thêm phần sau vào config/environment/development.rb:
1 2 | config<span class="token punctuation">.</span>active_storage<span class="token punctuation">.</span>service <span class="token operator">=</span> <span class="token symbol">:local</span> |
Hay sữ dụng s3 ở production thì ta thiết lập ở config/environments/production.rb:
1 2 | config<span class="token punctuation">.</span>active_storage<span class="token punctuation">.</span>service <span class="token operator">=</span> <span class="token symbol">:amazon</span> |
3. Sử dụng
Active Storage cho phép chúng ta có thể attach một hoặc nhiều files cho một record bằng cách sử dụng các macro tương ứng là has_one_attached và has_many_attached.
Để minh họa ta xét ví dụ sau, một user chỉ có 1 avatar hoặc một product có nhiều image. Để thực hiện ví dụ đó ta cần tạo các model User và Product như sau:
1 2 3 4 | rails g model <span class="token constant">User</span> name<span class="token symbol">:string</span> email<span class="token symbol">:string</span> rails g model <span class="token constant">Product</span> name<span class="token symbol">:string</span> price<span class="token symbol">:integer</span> rails db<span class="token symbol">:migrate</span> |
has_one_attached
Bên trong User model:
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> has_one_attached <span class="token symbol">:avatar</span> <span class="token keyword">end</span> |
Bên trong controller và view
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token keyword">class</span> <span class="token class-name">UsersController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">new</span></span> <span class="token variable">@user</span> <span class="token operator">=</span> <span class="token constant">User</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">create</span></span> user <span class="token operator">=</span> <span class="token constant">User</span><span class="token punctuation">.</span>create params<span class="token punctuation">.</span><span class="token keyword">require</span><span class="token punctuation">(</span><span class="token symbol">:user</span><span class="token punctuation">)</span><span class="token punctuation">.</span>permit<span class="token punctuation">(</span><span class="token symbol">:name</span><span class="token punctuation">,</span> <span class="token symbol">:email</span><span class="token punctuation">,</span> <span class="token symbol">:avatar</span><span class="token punctuation">)</span> redirect_to user <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">show</span></span> <span class="token variable">@user</span> <span class="token operator">=</span> <span class="token constant">User</span><span class="token punctuation">.</span>find params<span class="token punctuation">[</span><span class="token symbol">:id</span><span class="token punctuation">]</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <span class="token comment"># views/users/new.html.erb</span> <span class="token operator"><</span><span class="token string">%= form_for(@user) do |f| %> <div class=</span><span class="token string">"field"</span><span class="token operator">></span> <span class="token operator"><</span><span class="token string">%= f.label "Name" %><br /> <%=</span> f<span class="token punctuation">.</span>text_field <span class="token symbol">:name</span> <span class="token string">%> </div></span> <span class="token operator"><</span>div <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"field"</span><span class="token operator">></span> <span class="token operator"><</span><span class="token string">%= f.label "Email" %><br /> <%=</span> f<span class="token punctuation">.</span>email_field <span class="token symbol">:email</span> <span class="token string">%> </div></span> <span class="token operator"><</span>div <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"field"</span><span class="token operator">></span> <span class="token operator"><</span><span class="token string">%= f.label "Avatar" %><br /> <%=</span> f<span class="token punctuation">.</span>file_field <span class="token symbol">:avatar</span> <span class="token string">%> </div></span> <span class="token operator"><</span><span class="token operator">%</span><span class="token operator">=</span> f<span class="token punctuation">.</span>submit <span class="token string">"submit"</span> <span class="token string">%> <% end %></span> |
1 2 3 4 5 | <span class="token comment"># views/users/show.html.erb</span> <span class="token constant">Name</span><span class="token punctuation">:</span> <span class="token operator"><</span><span class="token string">%= @user.name %><br> Email: <%=</span> <span class="token variable">@user</span><span class="token punctuation">.</span>email <span class="token string">%><br></span> <span class="token constant">Avatar</span><span class="token punctuation">:</span> <span class="token operator"><</span><span class="token operator">%</span><span class="token operator">=</span> image_tag <span class="token variable">@user</span><span class="token punctuation">.</span>avatar <span class="token operator">%</span><span class="token operator">></span> |
Như vậy là ta có thể tạo avatar cho user mà ko cần tạo thêm field avatar trong bảng users hoặc bảng nào khác. Ngoài ra, để attach một avatar tới một user đã tồn tại trước đó ta có thể dùng user.avatar.attach(params[:avatar]) hoặc kiểm tra user đã được attach avatar chưa bằng cách user.avatar.attached?
has_many_attached
Product có nhiều images:
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">Product</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> has_many_attached <span class="token symbol">:images</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token keyword">class</span> <span class="token class-name">ProductsController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">new</span></span> <span class="token variable">@product</span> <span class="token operator">=</span> <span class="token constant">Product</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">create</span></span> product <span class="token operator">=</span> <span class="token constant">Product</span><span class="token punctuation">.</span>create<span class="token operator">!</span><span class="token punctuation">(</span>params<span class="token punctuation">.</span><span class="token keyword">require</span><span class="token punctuation">(</span><span class="token symbol">:product</span><span class="token punctuation">)</span><span class="token punctuation">.</span>permit<span class="token punctuation">(</span><span class="token symbol">:name</span><span class="token punctuation">,</span> images<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> redirect_to product <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">show</span></span> <span class="token variable">@product</span> <span class="token operator">=</span> <span class="token constant">Product</span><span class="token punctuation">.</span>find params<span class="token punctuation">[</span><span class="token symbol">:id</span><span class="token punctuation">]</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <span class="token comment"># views/products/new.html.erb</span> <span class="token operator"><</span><span class="token string">%= form_for(@product) do |f| %> <div class=</span><span class="token string">"field"</span><span class="token operator">></span> <span class="token operator"><</span><span class="token string">%= f.label "Name" %><br /> <%=</span> f<span class="token punctuation">.</span>text_field <span class="token symbol">:name</span> <span class="token string">%> </div></span> <span class="token operator"><</span>div <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"field"</span><span class="token operator">></span> <span class="token operator"><</span><span class="token string">%= f.label "Image" %><br /> <%=</span> f<span class="token punctuation">.</span>file_field <span class="token symbol">:images</span><span class="token punctuation">,</span> multiple<span class="token punctuation">:</span> <span class="token boolean">true</span> <span class="token string">%> </div></span> <span class="token operator"><</span><span class="token operator">%</span><span class="token operator">=</span> f<span class="token punctuation">.</span>submit <span class="token string">"submit"</span> <span class="token string">%> <% end %></span> |
1 2 3 4 5 6 7 8 9 | <span class="token comment"># views/products/show.html.erb</span> <span class="token constant">Name</span><span class="token punctuation">:</span> <span class="token operator"><</span><span class="token string">%= @product.name %><br> Images:<br> <% @product.images.each do |image| %> <%=</span> image_tag image <span class="token string">%> <br></span> <span class="token operator"><</span><span class="token operator">%</span> <span class="token keyword">end</span> <span class="token operator">%</span><span class="token operator">></span> |