Modèles et Validations : Gérer les Données avec Active Record
Introduction
Dans le monde du développement web, la gestion des données est primordiale. Ruby on Rails, avec son architecture MVC (Modèle-Vue-Contrôleur), place les modèles au cœur de cette gestion. C'est via les modèles que votre application interagit avec la base de données, manipule les informations et applique les règles métier.
Cette leçon vous plongera au cœur des Modèles Active Record et des Validations, deux piliers fondamentaux pour construire des applications Rails robustes, fiables et maintenables. Vous apprendrez comment les modèles représentent vos données, comment effectuer les opérations essentielles (CRUD), gérer les relations entre les données et, surtout, comment garantir l'intégrité de ces données grâce aux validations.
Le rôle des modèles dans Rails
Dans le cadre du modèle architectural MVC de Rails :
- Le Modèle est la couche responsable de la logique métier de l'application. Il gère les données, l'état de l'application, et les règles qui régissent la manipulation de ces données. Il représente généralement une table dans votre base de données.
- La Vue est l'interface utilisateur, responsable de l'affichage des données.
- Le Contrôleur agit comme un intermédiaire, traitant les requêtes des utilisateurs, interagissant avec le modèle, et préparant les données pour la vue.
Les modèles sont donc l'endroit où réside l'intelligence de vos données. Ils savent comment se connecter à la base de données, comment se sauvegarder, comment interagir avec d'autres modèles, et comment s'assurer que les données respectent certaines conditions avant d'être persistées.
Qu'est-ce qu'Active Record ?
Active Record est l'ORM (Object-Relational Mapping) par défaut de Ruby on Rails. Un ORM est une technique de programmation qui permet aux développeurs d'interagir avec une base de données relationnelle en utilisant le langage de programmation orienté objet de leur application (ici, Ruby), plutôt que d'écrire du SQL brut.
Principes clés d'Active Record :
- ORM puissant : Il mappe automatiquement les tables de votre base de données à des classes Ruby, et les lignes de ces tables à des objets Ruby. Chaque instance d'un modèle Active Record représente une ligne dans la base de données.
- Convention over Configuration : Active Record suit le principe de "convention plutôt que configuration". Par exemple :
- Une classe Ruby nommée
Usersera automatiquement mappée à une table de base de données nomméeusers. - Une colonne nommée
first_namedans la tableuserssera automatiquement disponible comme un attributuser.first_namesur l'objetUser. - Une colonne nommée
user_iddans une tablepostsindique par convention une relationbelongs_toavec le modèleUser. Cette approche réduit la quantité de code que vous devez écrire et rend les applications Rails plus cohérentes.
- Une classe Ruby nommée
- Héritage : Tous les modèles Active Record héritent de
ApplicationRecord(qui hérite lui-même deActiveRecord::Base). Cela leur confère toutes les fonctionnalités puissantes d'Active Record.
Les Modèles Active Record en Détail
La connexion avec la base de données
Quand vous générez un modèle Rails (ex: rails generate model Post title:string content:text), Rails crée un fichier de migration et un fichier de modèle.
Le fichier de migration (db/migrate/YYYYMMDDHHMMSS_create_posts.rb) est responsable de la création de la structure de la table posts dans votre base de données :
# db/migrate/YYYYMMDDHHMMSS_create_posts.rb
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.string :title
t.text :content
t.references :user, null: false, foreign_key: true # Ajoutons une référence à un utilisateur
t.timestamps
end
end
end
Le fichier de modèle (app/models/post.rb) est la classe Ruby qui interagit avec cette table :
# app/models/post.rb
class Post < ApplicationRecord
# Ici viendra la logique du modèle : associations, validations, méthodes personnalisées...
end
Active Record utilise les noms des colonnes de la table comme attributs de l'objet, ce qui permet d'accéder directement à post.title ou post.content.
Les opérations CRUD avec Active Record
CRUD est l'acronyme de Create, Read, Update, Delete (Créer, Lire, Mettre à jour, Supprimer) – les quatre opérations de base que vous pouvez effectuer sur des données dans une base de données. Active Record fournit des méthodes intuitives pour chacune d'elles.
Pour les exemples suivants, nous supposerons que nous avons un modèle Post (avec title et content) et un modèle User (avec name et email), et qu'un Post appartient à un User.
Création (Create)
Permet d'ajouter de nouvelles entrées dans la base de données.
-
Post.newetpost.save: La méthodenewcrée une instance du modèle en mémoire, mais ne la sauvegarde pas dans la base de données tant que la méthodesaven'est pas appelée.saveretournetrueen cas de succès etfalseen cas d'échec (par exemple, si les validations échouent).# Création d'un utilisateur (supposons qu'il existe) user = User.find_by(email: "john.doe@example.com") || User.create(name: "John Doe", email: "john.doe@example.com") # Création d'un nouvel article en mémoire post = Post.new(title: "Mon premier article Rails", content: "Ceci est un contenu passionnant.", user: user) # Sauvegarde de l'article dans la base de données if post.save puts "Article '#{post.title}' créé avec succès ! ID: #{post.id}" else puts "Erreur lors de la création de l'article : #{post.errors.full_messages.join(", ")}" end -
Post.create: Cette méthode est un raccourci qui combinenewetsave. Elle crée l'instance et tente de la sauvegarder immédiatement. Elle retourne l'objet créé (avec sonids'il a été sauvegardé avec succès) ou l'objet avec des erreurs s'il n'a pas pu être sauvegardé.post = Post.create(title: "Un autre article", content: "Un contenu très informatif.", user: user) if post.persisted? # Vérifie si l'objet a été sauvegardé dans la base de données puts "Article '#{post.title}' créé avec succès ! ID: #{post.id}" else puts "Erreur lors de la création de l'article : #{post.errors.full_messages.join(", ")}" end -
Post.create!: Similaire àcreate, mais lève une exception (ActiveRecord::RecordInvalid) si la sauvegarde échoue (par exemple, en cas d'erreurs de validation). Utile lorsque vous voulez que l'application s'arrête si la création échoue de manière inattendue.begin post = Post.create!(title: "Article obligatoire", content: "Ceci doit absolument être sauvegardé.", user: user) puts "Article '#{post.title}' créé avec succès ! ID: #{post.id}" rescue ActiveRecord::RecordInvalid => e puts "Échec de la création : #{e.message}" end
Lecture (Read)
Permet de récupérer des données de la base de données.
-
Post.all: Retourne une collection de tous les articles de la tableposts.all_posts = Post.all puts "Nombre total d'articles : #{all_posts.count}" all_posts.each { |p| puts "- #{p.title}" } -
Post.find(id): Récupère un article par son ID primaire. Lève une exception (ActiveRecord::RecordNotFound) si aucun article n'est trouvé.begin post_by_id = Post.find(1) # Tente de trouver l'article avec l'ID 1 puts "Article trouvé par ID : #{post_by_id.title}" rescue ActiveRecord::RecordNotFound puts "Aucun article trouvé avec l'ID 1." end -
Post.find_by(attribute: value): Récupère le premier article correspondant aux critères spécifiés. Retournenilsi aucun article n'est trouvé, sans lever d'exception.post_by_title = Post.find_by(title: "Un autre article") if post_by_title puts "Article trouvé par titre : #{post_by_title.content.truncate(30)}" else puts "Aucun article trouvé avec ce titre." end -
Post.where(condition): Retourne une collection de tous les articles qui satisfont une ou plusieurs conditions.# Articles avec un titre contenant "article" matching_posts = Post.where("title LIKE ?", "%article%") puts "\nArticles dont le titre contient 'article':" matching_posts.each { |p| puts "- #{p.title}" } # Articles d'un utilisateur spécifique john_posts = Post.where(user: user) # Ou user_id: user.id puts "\nArticles de John Doe :" john_posts.each { |p| puts "- #{p.title}" } -
Post.first,Post.last: Récupèrent respectivement le premier et le dernier article, selon l'ordre par défaut (généralement paridcroissant).puts "Premier article : #{Post.first&.title}" # Utilisation de l'opérateur de navigation sûre (&.) puts "Dernier article : #{Post.last&.title}"
Mise à jour (Update)
Permet de modifier des entrées existantes.
-
post.update(attributes): Met à jour un ou plusieurs attributs d'un objet et tente de le sauvegarder. Retournetrueoufalsecommesave.post_to_update = Post.find_by(title: "Un autre article") if post_to_update if post_to_update.update(title: "Titre mis à jour", content: "Nouveau contenu pour l'article.") puts "Article '#{post_to_update.id}' mis à jour avec succès !" else puts "Erreur lors de la mise à jour : #{post_to_update.errors.full_messages.join(", ")}" end end -
post.update!(attributes): Similaire àupdate, mais lève une exception en cas d'échec. -
post.update_attribute(attribute, value): Met à jour un seul attribut. Important : cette méthode ignore les validations. À utiliser avec prudence. -
post.update_columns(attributes): Met à jour plusieurs colonnes directement dans la base de données, sans passer par les callbacks ni les validations. Très performant pour les mises à jour massives ou lorsque vous êtes sûr de l'intégrité des données.
Suppression (Delete)
Permet de supprimer des entrées de la base de données.
-
post.destroy: Supprime l'objet de la base de données. Exécute les callbacksbefore_destroyetafter_destroyet respecte les optionsdependent:des associations. C'est la méthode de suppression recommandée dans la plupart des cas.post_to_delete = Post.find_by(title: "Titre mis à jour") if post_to_delete post_id = post_to_delete.id post_to_delete.destroy puts "Article avec l'ID #{post_id} supprimé avec succès." else puts "Article à supprimer non trouvé." end -
Post.destroy_all(conditions)/Post.destroy(id/ids): Méthodes de classe qui permettent de supprimer plusieurs enregistrements. Elles appellentdestroysur chaque enregistrement, déclenchant ainsi les callbacks et les dépendances. -
Post.delete/Post.delete_all(conditions): Méthodes qui suppriment les enregistrements directement de la base de données sans charger les objets en mémoire, sans exécuter les callbacks et sans gérer les dépendances. Plus rapide, mais potentiellement dangereux si vous avez des callbacks ou des dépendances à gérer. À utiliser pour des suppressions en masse où vous n'avez pas besoin de la logique métier.
Les Associations Active Record
Les applications web gèrent rarement des entités isolées. Il y a toujours des relations entre elles (un utilisateur a plusieurs articles, un article a plusieurs commentaires, etc.). Active Record rend la gestion de ces relations incroyablement simple et intuitive.
Voici les types d'associations les plus courants :
-
belongs_to: Indique qu'un modèle "appartient à" un autre. Le modèle quibelongs_tocontient la clé étrangère de l'autre modèle. Exemple : UnPostappartient à unUser.# app/models/post.rb class Post < ApplicationRecord belongs_to :user # ... validations ... end # app/models/user.rb class User < ApplicationRecord has_many :posts # L'autre côté de l'association # ... endCela vous permet de faire :
post.userpour obtenir l'utilisateur de l'article, etuser.postspour obtenir tous les articles d'un utilisateur. -
has_one: Indique qu'un modèle "a un" autre modèle. La clé étrangère réside dans le modèle associé. Exemple : UnUsera unProfile. Le modèleProfilecontiendraituser_id.# app/models/user.rb class User < ApplicationRecord has_one :profile end # app/models/profile.rb class Profile < ApplicationRecord belongs_to :user end -
has_many: Indique qu'un modèle "a plusieurs" autres modèles. La clé étrangère réside dans les modèles associés. Exemple : UnUsera plusieursPost.# app/models/user.rb class User < ApplicationRecord has_many :posts end # app/models/post.rb class Post < ApplicationRecord belongs_to :user end -
has_many :through: Utilisé pour définir une relation de type "plusieurs-à-plusieurs" via un modèle intermédiaire. Exemple : UnAuthor(Auteur) a plusieursBook(Livre) à travers lesAuthorship(Co-rédactions).# app/models/author.rb class Author < ApplicationRecord has_many :authorships has_many :books, through: :authorships end # app/models/book.rb class Book < ApplicationRecord has_many :authorships has_many :authors, through: :authorships end # app/models/authorship.rb (le modèle de jonction) class Authorship < ApplicationRecord belongs_to :author belongs_to :book end
Options courantes pour les associations :
-
dependent:: Spécifie ce qui se passe pour les enregistrements associés lorsque l'enregistrement parent est détruit.dependent: :destroy: Détruit les enregistrements associés. (Ex: détruire unUserdétruit sesPosts).dependent: :delete_all: Supprime les enregistrements associés directement de la BDD (plus rapide, pas de callbacks).dependent: :nullify: Met la clé étrangère des enregistrements associés àNULL.
# app/models/user.rb class User < ApplicationRecord has_many :posts, dependent: :destroy # Si un utilisateur est supprimé, tous ses articles sont également supprimés. end
Les Callbacks Active Record (Bref Aperçu)
Les callbacks sont des méthodes qui sont exécutées à des moments spécifiques du cycle de vie d'un objet Active Record (avant ou après qu'il soit sauvegardé, mis à jour, détruit, etc.). Ils sont parfaits pour implémenter de la logique métier qui doit se déclencher lors de ces événements.
Quelques callbacks courants :
before_validation,after_validationbefore_save,around_save,after_savebefore_create,around_create,after_createbefore_update,around_update,after_updatebefore_destroy,around_destroy,after_destroy
# app/models/post.rb
class Post < ApplicationRecord
before_save :set_default_title_if_blank
private
def set_default_title_if_blank
self.title = "Sans titre" if title.blank?
end
end
Les Scopes Active Record
Les scopes vous permettent de définir des requêtes réutilisables et lisibles au sein de vos modèles. Ils encapsulent la logique de requête complexe et la rendent facile à appliquer à des requêtes Active Record.
# app/models/post.rb
class Post < ApplicationRecord
# ... associations et validations ...
# Scope pour les articles publiés récemment (ex: dans les 7 derniers jours)
scope :recent, -> { where("created_at >= ?", 7.days.ago).order(created_at: :desc) }
# Scope pour les articles avec un certain mot-clé dans le titre
scope :with_keyword, -> (keyword) { where("title LIKE ?", "%#{keyword}%") }
end
Utilisation des scopes :
# Récupérer les articles récents
recent_posts = Post.recent
# Récupérer les articles contenant "Rails" dans le titre
rails_posts = Post.with_keyword("Rails")
# Combiner les scopes
recent_rails_posts = Post.recent.with_keyword("Rails")
Les scopes améliorent la lisibilité et la modularité de votre code de requête.
Les Validations : Assurer l'Intégrité des Données
Les validations sont des règles définies au niveau du modèle qui garantissent que les données sont valides avant qu'elles ne soient sauvegardées dans la base de données. C'est une couche de protection cruciale pour l'intégrité de vos données.
Pourquoi valider les données ?
- Cohérence et fiabilité : Empêche les données incohérentes ou invalides d'entrer dans votre base de données, assurant la fiabilité de votre application.
- Sécurité : Une bonne validation peut aider à prévenir certains types d'attaques (comme l'injection SQL si elle est combinée avec d'autres mesures de sécurité, ou la corruption de données).
- Expérience utilisateur : Fournit un feedback immédiat et compréhensible à l'utilisateur lorsqu'il tente de soumettre des données incorrectes. C'est beaucoup mieux que d'attendre une erreur de base de données obscure.
- Débogage simplifié : Les problèmes de données sont détectés plus tôt dans le cycle de vie de l'application, ce qui facilite le débogage.
Les validations s'exécutent avant la sauvegarde dans la base de données. Si une validation échoue, l'opération de sauvegarde est annulée et l'objet n'est pas persisté.
Les helpers de validation courants
Active Record fournit une riche collection de helpers de validation prédéfinis.
Pour ces exemples, nous utiliserons un modèle Product:
# app/models/product.rb
class Product < ApplicationRecord
# Ajoutez vos validations ici
end
-
presence: true: L'attribut ne doit pas être vide (nilou chaîne vide).validates :name, presence: true validates :description, presence: true -
uniqueness: true: L'attribut doit être unique dans la table. Peut être combiné avecscope:pour l'unicité dans un contexte particulier.validates :sku, uniqueness: true # Le SKU (Stock Keeping Unit) doit être unique validates :name, uniqueness: { scope: :category_id, message: "doit être unique pour cette catégorie" } -
length:: La longueur de la chaîne doit respecter certaines contraintes.validates :name, length: { minimum: 3, maximum: 50 } validates :description, length: { within: 10..500 } # Entre 10 et 500 caractères validates :password, length: { is: 8 } # Exactement 8 caractères -
format:: L'attribut doit correspondre à une expression régulière.validates :email, format: { with: URI::MailTo::EMAIL_REGEXP, message: "n'est pas un format d'email valide" } -
numericality:: L'attribut doit être un nombre et respecter certaines conditions (ex: entier, supérieur à...).validates :price, numericality: { greater_than: 0, less_than_or_equal_to: 1000 } validates :stock, numericality: { only_integer: true, greater_than_or_equal_to: 0 } -
inclusion:: La valeur de l'attribut doit être incluse dans un ensemble prédéfini de valeurs.validates :status, inclusion: { in: %w(draft published archived), message: "%{value} n'est pas un statut valide" } -
exclusion:: La valeur de l'attribut ne doit pas être incluse dans un ensemble prédéfini de valeurs.validates :slug, exclusion: { in: %w(admin new edit), message: "%{value} est un mot réservé" } -
confirmation: true: Utile pour les mots de passe. Nécessite un champ_confirmationcorrespondant.# Dans le modèle User validates :password, confirmation: true # Le formulaire devrait avoir des champs pour `password` et `password_confirmation` -
acceptance: true: Utilisé pour vérifier que l'utilisateur a accepté les termes et conditions.validates :terms_of_service, acceptance: true
Gestion des erreurs de validation
Quand une validation échoue, Active Record ajoute des messages d'erreur à l'objet. Ces messages sont accessibles via l'attribut errors.
model.valid?: Exécute toutes les validations et retournetruesi l'objet est valide,falsesinon.model.errors: Un objetActiveModel::Errorscontenant tous les messages d'erreur pour l'objet.model.errors.full_messages: Retourne un tableau de chaînes de caractères avec des messages d'erreur complets et lisibles par l'utilisateur (ex: "Nom ne peut pas être vide").model.errors[:attribute]: Retourne un tableau de chaînes de caractères avec les erreurs spécifiques à un attribut.
product = Product.new(name: "", price: -10) # Crée un produit invalide
if product.valid?
puts "Le produit est valide."
else
puts "Le produit est invalide :"
product.errors.each do |error|
puts "- #{error.attribute}: #{error.message}"
end
puts "\nMessages complets :"
product.errors.full_messages.each { |msg| puts "- #{msg}" }
end
# Sortie probable :
# Le produit est invalide :
# - name: ne peut pas être vide
# - price: doit être supérieur à 0
#
# Messages complets :
# - Name ne peut pas être vide
# - Price doit être supérieur à 0
Méthodes de sauvegarde et de validation
Nous avons déjà abordé save et create, mais il est important de noter leur interaction avec les validations :
saveetcreate: Tentent de sauvegarder l'objet et exécutent les validations. Retournenttrue/l'objet en cas de succès,false/l'objet avec erreurs en cas d'échec de validation.save!etcreate!: Tentent de sauvegarder l'objet et exécutent les validations. Lèvent uneActiveRecord::RecordInvaliden cas d'échec de validation.updateetupdate!: Fonctionnent de la même manière quesaveetsave!, mais pour la mise à jour d'un enregistrement existant.
Quand utiliser ! ?
Utilisez les méthodes avec ! (save!, create!, update!) lorsque vous voulez qu'une exception soit levée si l'opération échoue. C'est utile dans les contrôleurs où vous attendez qu'une opération réussisse (par exemple, après avoir déjà validé l'entrée utilisateur), et que vous voulez que l'erreur soit propagée si quelque chose d'inattendu se produit (comme une violation d'une contrainte de base de données).
Validations personnalisées
Lorsque les helpers de validation prédéfinis ne suffisent pas, vous pouvez créer vos propres validations.
-
Méthodes de validation personnalisées : Vous pouvez définir une méthode qui contient votre logique de validation et l'appeler avec
validate. Cette méthode est exécutée au moment de la validation. Les erreurs sont ajoutées àerrors.# app/models/post.rb class Post < ApplicationRecord # ... autres validations ... validate :title_must_contain_specific_keyword private def title_must_contain_specific_keyword unless title.nil? || title.include?('Ruby') || title.include?('Rails') errors.add(:title, "doit contenir 'Ruby' ou 'Rails'") end end end -
Validateurs personnalisés (Custom Validators) : Pour des validations plus complexes ou réutilisables à travers plusieurs modèles, vous pouvez créer des classes de valideurs séparées qui héritent de
ActiveModel::EachValidatorouActiveModel::Validator.# app/validators/email_validator.rb class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i record.errors.add attribute, (options[:message] || "n'est pas un email valide") end end end # Utilisation dans le modèle : # app/models/user.rb class User < ApplicationRecord validates :email, presence: true, email: true end
Exemples Pratiques
Mettons en pratique ce que nous avons appris avec un modèle Post complet.
Modèle Post avec associations et validations
# app/models/post.rb
class Post < ApplicationRecord
# --- Associations ---
# Un post appartient à un utilisateur.
# La colonne `user_id` est la clé étrangère.
belongs_to :user
# Un post peut avoir plusieurs commentaires.
# Si un post est supprimé, tous ses commentaires associés sont aussi détruits.
has_many :comments, dependent: :destroy
# --- Validations ---
# Le titre doit être présent et avoir une longueur minimale de 5 caractères.
validates :title, presence: { message: "ne peut pas être vide" },
length: { minimum: 5, message: "doit avoir au moins 5 caractères" }
# Le contenu doit être présent et avoir une longueur minimale de 20 caractères.
validates :content, presence: { message: "ne peut pas être vide" },
length: { minimum: 20, message: "doit avoir au moins 20 caractères" }
# La `user_id` doit être présente, car un post doit être lié à un utilisateur.
validates :user_id, presence: true
# Une validation personnalisée : Le titre doit contenir "Ruby" ou "Rails"
validate :title_must_contain_ruby_or_rails
# --- Scopes ---
# Articles les plus récents
scope :most_recent, -> { order(created_at: :desc) }
# Articles publiés aujourd'hui
scope :published_today, -> { where(created_at: Date.current.all_day) }
private
# Méthode pour la validation personnalisée
def title_must_contain_ruby_or_rails
# S'assurer que le titre n'est pas nil pour éviter une erreur sur .include?
if title.present? && !title.include?('Ruby') && !title.include?('Rails')
errors.add(:title, "doit inclure le mot 'Ruby' ou 'Rails' pour être publié")
end
end
end
# app/models/user.rb (modèle simple pour l'association)
class User < ApplicationRecord
validates :name, presence: true
validates :email, presence: true, uniqueness: true
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy # Supposons qu'un utilisateur peut aussi avoir des commentaires
end
# app/models/comment.rb (modèle simple pour l'association)
class Comment < ApplicationRecord
validates :content, presence: true, length: { minimum: 5 }
validates :user_id, presence: true
validates :post_id, presence: true
belongs_to :user
belongs_to :post
end
Explication du code :
Ce bloc de code définit le modèle Post avec ses dépendances User et Comment.
- Associations :
belongs_to :userétablit le lien avec l'utilisateur qui a créé le post.has_many :comments, dependent: :destroyindique qu'un post peut avoir de nombreux commentaires et que si le post est supprimé, ses commentaires le sont aussi (dependent: :destroy). - Validations : Des validations
presenceetlengthsont appliquées autitleetcontentpour s'assurer qu'ils ne sont pas vides et ont une taille minimale.user_idest aussipresence: truepour garantir qu'un post est toujours rattaché à un utilisateur. - Validation personnalisée : La méthode
title_must_contain_ruby_or_railsest un exemple de validation personnalisée. Elle vérifie que le titre contient l'un des mots-clés "Ruby" ou "Rails". Si ce n'est pas le cas, une erreur est ajoutée à l'attributtitleviaerrors.add. - Scopes :
most_recentetpublished_todaysont des scopes pour des requêtes courantes, rendant le code plus DRY (Don't Repeat Yourself) et lisible.
Exemples d'utilisation dans la console Rails
Ouvrez votre console Rails avec rails c dans le terminal.
# --- Préparation : Assurons-nous d'avoir au moins un utilisateur ---
user = User.find_by(email: "prof@example.com")
if user.nil?
user = User.create!(name: "Professeur de Rails", email: "prof@example.com")
puts "Utilisateur 'Professeur de Rails' créé (ID: #{user.id})"
else
puts "Utilisateur 'Professeur de Rails' déjà existant (ID: #{user.id})"
end
# --- 1. Création d'un article valide ---
puts "\n--- Tentative de création d'un article valide ---"
valid_post = Post.new(
title: "Apprendre Ruby on Rails en 2024",
content: "Ceci est un article détaillé pour enseigner les bases de Ruby on Rails aux étudiants. Nous couvrons Active Record, les validations et les associations.",
user: user # Lien direct via l'objet user
)
if valid_post.save
puts "Article créé avec succès ! ID: #{valid_post.id}, Titre: '#{valid_post.title}'"
else
puts "Échec de la création de l'article valide : #{valid_post.errors.full_messages.join(', ')}"
end
# --- 2. Tentative de création d'un article invalide (longueur, présence, validation personnalisée) ---
puts "\n--- Tentative de création d'un article invalide ---"
invalid_post = Post.new(
title: "Court", # Trop court, et ne contient pas 'Ruby' ou 'Rails'
content: "Pas assez long.", # Trop court
user: user
)
if invalid_post.save
puts "Article invalide créé par erreur !"
else
puts "Échec attendu de la création de l'article invalide :"
invalid_post.errors.full_messages.each { |msg| puts "- #{msg}" }
end
# --- 3. Création d'un commentaire pour l'article valide ---
puts "\n--- Création d'un commentaire ---"
comment = valid_post.comments.create(content: "Super article ! Très clair.", user: user)
if comment.persisted?
puts "Commentaire créé : '#{comment.content}' pour l'article '#{valid_post.title}'"
else
puts "Échec de la création du commentaire : #{comment.errors.full_messages.join(', ')}"
end
# --- 4. Lecture des données ---
puts "\n--- Lecture des données ---"
# Trouver un article par son ID
found_post = Post.find(valid_post.id)
puts "Article trouvé par ID : '#{found_post.title}' par #{found_post.user.name}"
# Utiliser un scope
recent_posts = Post.most_recent
puts "Articles les plus récents (#{recent_posts.count}) :"
recent_posts.each { |p| puts "- #{p.title}" }
# Accéder aux commentaires d'un article
puts "Commentaires pour '#{found_post.title}' :"
found_post.comments.each { |c| puts "- '#{c.content}' (par #{c.user.name})" }
# --- 5. Mise à jour d'un article ---
puts "\n--- Mise à jour d'un article ---"
if found_post.update(title: "Maîtriser Ruby et Rails en 2024 (Mis à jour)")
puts "Article mis à jour avec succès : '#{found_post.title}'"
else
puts "Échec de la mise à jour : #{found_post.errors.full_messages.join(', ')}"
end
# --- 6. Tentative de mise à jour invalide ---
puts "\n--- Tentative de mise à jour invalide ---"
found_post.title = "A" # Trop court
if found_post.save
puts "Mise à jour invalide effectuée par erreur !"
else
puts "Échec attendu de la mise à jour : #{found_post.errors.full_messages.join(', ')}"
end
puts "Le titre est resté : '#{found_post.title}'" # Le titre n'a pas été modifié en BDD
# --- 7. Suppression d'un article (et de ses commentaires grâce à `dependent: :destroy`) ---
puts "\n--- Suppression d'un article ---"
post_id_to_delete = found_post.id
found_post.destroy
if Post.find_by(id: post_id_to_delete).nil?
puts "Article ID #{post_id_to_delete} supprimé."
# Vérifier que les commentaires ont aussi été supprimés
if Comment.where(post_id: post_id_to_delete).empty?
puts "Les commentaires associés ont également été supprimés."
else
puts "Attention: Les commentaires n'ont PAS été supprimés !"
end
else
puts "Échec de la suppression de l'article."
end
Explication du code : Ce bloc de code simule des interactions courantes avec les modèles dans une console Rails.
- Préparation : Assure qu'un utilisateur existe pour lier les posts.
- Création valide : Montre comment créer un
Postavec des données valides qui passent toutes les validations, y compris la validation personnalisée. Le succès est vérifié parif valid_post.save. - Création invalide : Démontre ce qui se passe lorsque les données ne respectent pas les validations (
length,presence, et la validation personnaliséetitle_must_contain_ruby_or_rails). Les messages d'erreur sont affichés viainvalid_post.errors.full_messages. - Création de commentaire : Illustre comment créer un objet lié (
comment) via l'associationvalid_post.comments.create. - Lecture : Montre diverses façons de récupérer des données : par ID, et en utilisant les scopes (
most_recent). Il montre également comment accéder aux données associées (found_post.user.name,found_post.comments). - Mise à jour : Explique comment modifier un enregistrement existant avec
updateet comment vérifier son succès. - Mise à jour invalide : Teste une tentative de mise à jour qui échoue aux validations, et montre que les changements ne sont pas persistés en base de données.
- Suppression : Illustre la suppression d'un enregistrement avec
destroy. Il met en évidence l'effet dedependent: :destroyen vérifiant que les commentaires associés sont également supprimés.
Conclusion
Dans cette leçon, nous avons exploré en profondeur le rôle central des Modèles Active Record et des Validations dans le développement d'applications Ruby on Rails.
Nous avons vu que :
- Active Record agit comme un puissant ORM, permettant de manipuler la base de données via des objets Ruby, en tirant parti de la "Convention over Configuration".
- Les opérations CRUD (Création, Lecture, Mise à jour, Suppression) sont gérées de manière intuitive et flexible par Active Record, avec des méthodes adaptées à différents scénarios (avec/sans validation, avec/sans callbacks).
- Les Associations (comme
belongs_to,has_many) simplifient la modélisation et l'interaction entre les différentes entités de votre application, reflétant les relations du monde réel. - Les Validations sont une couche de sécurité essentielle qui garantit l'intégrité et la cohérence des données avant leur persistance. Rails offre une multitude de helpers de validation intégrés, ainsi que la possibilité de créer des validations personnalisées pour des règles métier spécifiques.
La maîtrise d'Active Record et des validations est fondamentale pour tout développeur Rails. Elle vous permet de construire des applications robustes, faciles à maintenir et à faire évoluer, tout en offrant une expérience utilisateur fluide grâce à un feedback immédiat sur les données soumises.
Continuez à explorer les fonctionnalités avancées d'Active Record, telles que les requêtes complexes, la gestion des transactions, et l'optimisation des performances des requêtes. C'est un domaine riche qui ne cesse d'évoluer.