Hello les petits Biscuits !
Bienvenue sur la 34Úme édition de Ruby Biscuit.
Vous ĂȘtes maintenant 585 abonnĂ©s đ„ł
Maintenant Ruby biscuit, câest aussi votre meilleur alliĂ© pour recruter des devs Ruby !
Si vous nâavez pas encore rejoint le club, RDV sur https://recrutement.rubybiscuit.fr
Bonne lecture
Active Storage Versus Shrine: le match
Ce mois-ci, nous abordons la gestion des fichiers dans Ruby on Rails, une fonctionnalité indispensable pour que vos utilisateurs puissent téléverser (uploader) des photos, des vidéos ou des documents PDF. Découvrez deux gems populaires utilisées dans la communauté: ActiveStorage et Shrine.
Partie Théorique
Avant de nous lancer, faisons un bref rappel sur ce que font ces gems :
Télécharger des fichiers depuis un navigateur vers un service de stockage de fichiers
Faire le lien entre ces fichiers et vos instances Active Record
Que ce soit pour Shrine ou Active Storage, l'installation se fait trÚs facilement. Pour les 2 gems, vous trouverez la plupart des réponses à vos questions dans la documentation et en particulier comment les installer.
TL;DR
Les différences entre Shrine et Active Storage sont généralement mineures, mais votre choix dépendra surtout de votre usage :
Si vous gérez principalement un seul fichier par modÚle, Shrine offre une approche plus flexible et explicite
Si vous devez gĂ©rer plusieurs fichiers par modĂšle ou que vous souhaitez une solution prĂȘte Ă lâemploi et intĂ©grĂ©e Ă Rails, Active Storage sera souvent plus pratique.
Base de données
Pour commencer, je voudrais attaquer la comparaison par le prisme de la base de données. Quelles sont les différences d'architecture et qu'est-ce que ça implique sur les migrations par exemple ?
Avec Shrine, c'est simple, chaque nouveau fichier nécessite une migration sur le modÚle.
class AddCoverPictureToProjects < ActiveRecord::Migration
def change
add_column :projects, :cover_picture_data, :text
end
end
Ensuite on va mettre Ă jour notre modĂšle
class Project < ApplicationRecord
include ImageUploader::Attachment(:cover_picture)
end
Pour Active Storage, c'est encore plus simple, on va créer 2 tables au début et puis c'est tout:
active_storage_blobs
active_storage_attachments
Ensuite, il suffira de mettre Ă jour son modĂšle Ă chaque fois qu'on veut lier un nouveau fichier Ă un modĂšle.
class Project < ApplicationRecord
has_one_attached :cover_picture
end
Sous cette simplicité apparente se cache une réalité bien plus complexe. Par exemple, quand on appelle la photo dans la vue, voici ce que fait AST sous le capot :
Il cherche dans la table
active_storage_attachments
record_type: "Project"
record_id: 1
name : "cover_picture"
Ensuite il va chercher dans la table
active_storage_blobs
Enfin il génÚre l'URL
Vous voyez venir le problĂšme. En appelant une image (ou un document) dans un index, on obtient non pas une N+1 mais bel et bien une 2N + 1 ! Alors que sur Shrine, il n'y a aucune requĂȘte SQL supplĂ©mentaire.
Pour la petite histoire, j'ai dĂ©jĂ travaillĂ© sur une page d'accueil avec 160 requĂȘtes SQL pour aller rĂ©cupĂ©rer et afficher des images ! Un simple includes a rĂ©solu le problĂšme
Avec notre exemple d'aujourd'hui, et une syntaxe plus moderne, ça donnerait ça dans le controller.
@projects = Project.with_attached_cover_picture
Un des avantages avec l'architecture d'AST, c'est qu'on peut facilement ajouter plusieurs fichiers dans notre modĂšle. Par exemple pour ajouter des photos Ă un projet :
class Project < ApplicationRecord
has_many_attached :photos
end
Alors que dans Shrine, ça passe forcément par créer un nouveau modÚle.
class Photo < ApplicationRecord
belongs_to :project
include ImageUploader::Attachment(:file)
end
class Project < ApplicationRecord
has_many :photos
end
Les formulaires
Quand un utilisateur a plusieurs photos à ajouter sur un projet dans un formulaire, ce qui est pratique de pouvoir les sélectionner toutes ensemble.
Pour AST, il suffit simplement d'ajouter ça à votre formulaire dans la vue
<%= form.file_field :photos, multiple: true %>
Ensuite dans le controller
class ProjectsController < ApplicationController
def create
@project = Project.new(project_params)
if @project.save
redirect_to project_path(@project)
else
render :new
end
end
private
def project_params
params.require(:project).permit(photos: [])
end
end
Ce fut ma plus grosse frustration en passant de AST à Shrine que de ne pas pouvoir faire ça aussi simplement .
Pour avoir l'équivalent, j'ai dû rajouter quelques lignes de code dans le modÚle
class Photo < ApplicationRecord
belongs_to :project
include ImageUploader::Attachment(:file)
end
class Project < ApplicationRecord
has_many :photos
accepts_nested_attributes_for :photos
attr_accessor :new_photos
def new_photos=(files)
files.each do |file|
photos.build(file: file)
end
end
end
Et maintenant dans ma vue je peux utiliser ça
<%= f.file_field :new_photos, multiple: true %>
Dans le controller il faut juste mettre Ă jour cette ligne
params.require(:project).permit(new_photos: [])
Mon plus gros bug (Ă cause du polymorphisme)
La flexibilitĂ© qu'amĂšne le polymorphisme sur AST vient avec son lot de piĂšge. Ăa a causĂ© une des plus grosses frayeurs de code.
AprÚs avoir changé le nom d'un modÚle pour une refacto, je pousse mon code en production, et là surprise, tous les fichiers avaient disparu !
L'explication peut paraitre simple à posteriori mais sur le moment je n'y avais pas pensé. Je vous explique : dans la table active_storage_attachments
la colonne record_type
qui stocke le nom du modĂšle doit ĂȘtre mise Ă jour en mĂȘme temps que le changement de nom du modĂšle.
Il n y'a aucune vérification, ni de la base de données, ni de l'application, que le record (combinaison record_id et record_type) existe bel et bien. C'est uniquement sur vos petites épaules que reposent la concordance des noms. Alors que sur Shrine, il n'y a besoin de rien faire.
Tests
Une autre erreur qui m'est arrivé sur AST, c'est d'oublier de supprimer les fichiers aprÚs les tests. On se retrouve en local avec une taille projet qui augmente de maniÚre exponentielle et fait saturer le disque dur. En faisant une analyse rapide (avec la commande $ du
), je me suis vite rendu compte que mon dossier tmp/storage faisait plusieurs giga octet.
Pourtant, c'est bien marqué dans la documentation !!! (RTFM)
Voici la solution pour Rspec
config.after(:suite) do
path = "#{Rails.root}/tmp/storage"
if Dir.exist?(path)
FileUtils.remove_dir(path)
end
end
J'ai une petite prĂ©fĂ©rence pour ce que fait Shrine qui permet de mettre les fichiers dans la RAM. C'est plus rapide et plus simple Ă gĂ©rer đ
Shrine.storages[:store] = Shrine::Storage::Memory.new
Les variantes (AST) ou dérivatives (Shrine)
Ce sont des images modifiées à une taille spécifique pour avoir des images plus légÚres ou qui s'insÚrent facilement dans du code html. Les 2 utilisent image_magick ou vips. Attention pour les 2 gems aussi, la génération des images se fait cÎté back-end ce qui peut considérablement ralentir votre serveur. L'avantage de AST c'est que si vous définissez vos variantes dans le modÚle, elles sont créées dans un background Job.
Conclusion : Comparaison n'est pas raison
Comme souvent en informatique, quand on veut comparer, la rĂ©ponse va ĂȘtre : "ça dĂ©pend du contexte". Pour une application de logistique, qui doit rĂ©cupĂ©rer beaucoup de photos par commande pour vĂ©rifier le bon dĂ©roulement d'une livraison (mon prĂ©cĂ©dent job), AST sera plus adaptĂ©. En revanche, pour une application qui a une image par projet et quelques PDF Ă sauvegarder, Shrine convient trĂšs bien.
Il faut juste avoir conscience qu'avec AST, il faudra faire attention aux N+1 au risque de ralentir votre application. Sur Shrine, il est plus rare de voir des requĂȘtes SQL incontrollĂ©es.
Enfin AST étant nativement dans Rails, il est plus rassurant sur le long-terme mais pas forcément plus simple à maintenir.
â Alexandre
Une de mes frustrations perso c'est qu'il n'y aie pas de comportement natif pour prĂ©visualiser et supprimer facilement les fichiers uploadĂ©s via un champ file_field. Et aucune gem toute prĂȘte pour ça non plus. Câest pourtant un besoin hyper courant đ€·ââïž