Maîtriser Ruby on Rails : Développement Web Rapide et Efficace
Maîtriser Ruby on Rails : Développement Web Rapide et Efficace

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 User sera automatiquement mappée à une table de base de données nommée users.
    • Une colonne nommée first_name dans la table users sera automatiquement disponible comme un attribut user.first_name sur l'objet User.
    • Une colonne nommée user_id dans une table posts indique par convention une relation belongs_to avec le modèle User. Cette approche réduit la quantité de code que vous devez écrire et rend les applications Rails plus cohérentes.
  • Héritage : Tous les modèles Active Record héritent de ApplicationRecord (qui hérite lui-même de ActiveRecord::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.new et post.save : La méthode new crée une instance du modèle en mémoire, mais ne la sauvegarde pas dans la base de données tant que la méthode save n'est pas appelée. save retourne true en cas de succès et false en 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 combine new et save. Elle crée l'instance et tente de la sauvegarder immédiatement. Elle retourne l'objet créé (avec son id s'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 table posts.

    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. Retourne nil si 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 par id croissant).

    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. Retourne true ou false comme save.

    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 callbacks before_destroy et after_destroy et respecte les options dependent: 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 appellent destroy sur 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 qui belongs_to contient la clé étrangère de l'autre modèle. Exemple : Un Post appartient à un User.

    # 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
      # ...
    end
    

    Cela vous permet de faire : post.user pour obtenir l'utilisateur de l'article, et user.posts pour 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 : Un User a un Profile. Le modèle Profile contiendrait user_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 : Un User a plusieurs Post.

    # 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 : Un Author (Auteur) a plusieurs Book (Livre) à travers les Authorship (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 un User détruit ses Posts).
    • 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_validation
  • before_save, around_save, after_save
  • before_create, around_create, after_create
  • before_update, around_update, after_update
  • before_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 ?

  1. 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.
  2. 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).
  3. 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.
  4. 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 (nil ou chaîne vide).

    validates :name, presence: true
    validates :description, presence: true
    
  • uniqueness: true : L'attribut doit être unique dans la table. Peut être combiné avec scope: 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 _confirmation correspondant.

    # 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 retourne true si l'objet est valide, false sinon.
  • model.errors : Un objet ActiveModel::Errors contenant 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 :

  • save et create : Tentent de sauvegarder l'objet et exécutent les validations. Retournent true/l'objet en cas de succès, false/l'objet avec erreurs en cas d'échec de validation.
  • save! et create! : Tentent de sauvegarder l'objet et exécutent les validations. Lèvent une ActiveRecord::RecordInvalid en cas d'échec de validation.
  • update et update! : Fonctionnent de la même manière que save et save!, 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::EachValidator ou ActiveModel::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: :destroy indique 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 presence et length sont appliquées au title et content pour s'assurer qu'ils ne sont pas vides et ont une taille minimale. user_id est aussi presence: true pour garantir qu'un post est toujours rattaché à un utilisateur.
  • Validation personnalisée : La méthode title_must_contain_ruby_or_rails est 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'attribut title via errors.add.
  • Scopes : most_recent et published_today sont 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.

  1. Préparation : Assure qu'un utilisateur existe pour lier les posts.
  2. Création valide : Montre comment créer un Post avec des données valides qui passent toutes les validations, y compris la validation personnalisée. Le succès est vérifié par if valid_post.save.
  3. Création invalide : Démontre ce qui se passe lorsque les données ne respectent pas les validations (length, presence, et la validation personnalisée title_must_contain_ruby_or_rails). Les messages d'erreur sont affichés via invalid_post.errors.full_messages.
  4. Création de commentaire : Illustre comment créer un objet lié (comment) via l'association valid_post.comments.create.
  5. 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).
  6. Mise à jour : Explique comment modifier un enregistrement existant avec update et comment vérifier son succès.
  7. 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.
  8. Suppression : Illustre la suppression d'un enregistrement avec destroy. Il met en évidence l'effet de dependent: :destroy en 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.