Stocker et accéder à des paramètres JSONB dans PostgreSQL et Rails
PostgreSQL 9.4 et supérieures permettent de stocker des données JSON nativement, et Rails simplifie l’utilisation de ce format.
Le format JSONB
peut être indexé et cherché, avec des performances sensiblement similaires que du JSON
stocké tel quel, c’est donc le format à privilégier.
Je recommande fortement la lecture du billet de Nando Vieira, Using PostgreSQL and jsonb with Ruby on Rails (utiliser PostgreSQL et JSONB
avec Ruby on Rails) pour mieux connaître l’impact sur les performances et les différentes manières de requetter la base en utilisant des attributs stockés dans des colonnes JSONB
.
La macro store_accessor
définie dans le module ActiveRecord::Store::ClassMethods
, qui est automatiquement inclus par ActiveRecord
.
Cette macro utilise le nom de colonne, les clés, et d’éventuels préfixes ou suffixes pour les méthodes générées.
Le code généré permet d’appliquer très facilement des validations ou des callbacks aux colonnes stockées en JSON
, ou de suivre les modifications. Grâce à cela, les données stockées dans une colonne JSON/JSONB soint aussi faciles à utiliser que des colonnes normales.
Comme toujours, il vous appartient de peser le pour et le contre avant de décider s’il est pertinent de stocker des données dans une colonne au format JSON
au lieu de colonnes dédiées, mais voilà une situation qui me semble particulièrement appropriée : les réglages liés à l’instance.
Pourquoi stocker les réglages d’instance en JSONB
Pour une application liée à un seul utilisateur (tenant), il paraît superflu voire inapproprié de créer une colonne pour chaque paramètre, sachant que cette table ne contiendra qu’une seule ligne.
Et pour une application liée à plusieurs utilisateurs, cela permet de stocker des réglages spécifiques sans devoir ajouter une colonne pour chaque personnalisation demandée.
Dans les deux cas, stocker ces valeurs dans une colonne JSONB
permet d’enregistrer tous types de données sans jamais avoir besoin d’opérer de migration, c’est donc simple et particulièrement adapté.
Voici le code lié aux réglages de mon modèle Tenant
:
# app/models/tenant.rb
class Tenant < ApplicationRecord
KEYS = [:name, :blog_url, :contact_email].freeze
store_accessor :settings, *KEYS
validates :name, presence: true
validates :contact_email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
class << self
def fields
@fields ||= KEYS.collect { |key| [key, key.match(/(?:.*)_(.*)/) { |type| type[1].to_sym } || :text] }
end
end
end
La première ligne contient la liste des champs stockées en JSON
. J’en fais une constante puisque c’est une valeur utilisée à plusieurs endroits dans la classe.
Je passe ensuite cette liste de champs à la macro store_accessor
pour pouvoir interagir avec ces champs comme s’il s’agissait de colonnes classiques.
Je peux ensuite définir des validations et des callbacks comme on le fait d’habitude.
Pour finir, j’ajoute la méthode de classe #fields
qui liste chaque champ avec le type associé. Celui-ci est déduit du suffixe du champ (url
, email
, etc). Il suffit ensuite d’itérer sur ces différents types pour générer un formulaire permettant de saisir chaque réglage :
# app/views/tenants/_form.html.erb
<%= form_with(model: tenant) do |f| %>
<% Tenant.fields.each do |key, type| %>
<div class="field">
<%= f.label key %>
<%= f.text_field key, type: type %>
</div>
<% end %>
<%= f.button "Save" %>
<% end %>