ORM & Base de Données

Requêtes CRUD

SELECT — Récupérer

// Tous les enregistrements
let users: Vec<users::Model> = users::Entity::objects
    .all()
    .all(&*db)
    .await?;

// Avec limite et offset
let users = users::Entity::objects
    .all()
    .limit(10)
    .offset(0)
    .all(&*db)
    .await?;

// Avec tri
let users = users::Entity::objects
    .all()
    .order_by_asc(users::Column::Name)
    .all(&*db)
    .await?;

COUNT — Compter

let count = users::Entity::objects
    .filter(users::Column::Active.eq(true))
    .count(&*db)
    .await?;

WHERE — Filtrage (SeaORM natif)

use sea_orm::ColumnTrait;

// Égalité
let user = users::Entity::objects
    .filter(users::Column::Email.eq("test@example.com"))
    .first(&*db)
    .await?;

// Comparaisons
let users = users::Entity::objects
    .filter(users::Column::Age.gt(18))
    .all(&*db)
    .await?;

// Multiples conditions (AND)
let users = users::Entity::objects
    .filter(users::Column::Active.eq(true))
    .filter(users::Column::Age.gte(18))
    .all(&*db)
    .await?;

// OU
use sea_orm::Condition;
let users = users::Entity::objects
    .filter(
        Condition::any()
            .add(users::Column::Email.eq("a@test.com"))
            .add(users::Column::Email.eq("b@test.com"))
    )
    .all(&*db)
    .await?;

Macro `search!` — DSL de filtrage

La macro search! offre une syntaxe inspirée de Django pour construire des filtres SeaORM. Elle retourne un RuniqueQueryBuilder chainable (.limit(), .order_by_asc(), .all(), etc.).

Tableau de référence complet

SyntaxeSQL généréÉquivalent Django
search!(Entity)(aucun filtre).objects.all()
Col eq valWHERE col = valfilter(col=val)
Col exact valWHERE col = valfilter(col__exact=val)
Col ne valWHERE col != val
Col gt valWHERE col > valfilter(col__gt=val)
Col lt valWHERE col < valfilter(col__lt=val)
Col gte valWHERE col >= valfilter(col__gte=val)
Col lte valWHERE col <= valfilter(col__lte=val)
Col like valWHERE col LIKE val
Col ilike valWHERE col ILIKE val
Col not_like valWHERE col NOT LIKE val
Col not_ilike valWHERE col NOT ILIKE val
Col contains valWHERE col LIKE '%val%'filter(col__contains=val)
Col icontains valWHERE col ILIKE '%val%'filter(col__icontains=val)
Col startswith valWHERE col LIKE 'val%'filter(col__startswith=val)
Col endswith valWHERE col LIKE '%val'filter(col__endswith=val)
Col iexact valWHERE col ILIKE valfilter(col__iexact=val)
Col isnullWHERE col IS NULLfilter(col__isnull=True)
Col not_nullWHERE col IS NOT NULLfilter(col__isnull=False)
Col in [v1, v2]WHERE col IN (v1, v2) (littéral)filter(col__in=[v1, v2])
Col not_in [v1, v2]WHERE col NOT IN (v1, v2)exclude(col__in=[v1, v2])
Col in (expr)WHERE col IN (...) (Vec/itérateur)filter(col__in=qs)
Col not_in (expr)WHERE col NOT IN (...)exclude(col__in=qs)
?Col in (expr)WHERE col IN (...) (sauté si vide)
?Col not_in (expr)WHERE col NOT IN (...) (sauté si vide)
Col range (a, b)WHERE col BETWEEN a AND bfilter(col__range=(a, b))
Col not_range (a, b)WHERE col NOT BETWEEN a AND b
! Col op valexclusion (NOT).exclude(col__op=val)
or(C1 op v, C2 op v)WHERE c1 op v OR c2 op vQ(c1__op=v) | Q(c2__op=v)

Fetch all

let all = search!(users::Entity)
    .order_by_asc(users::Column::Name)
    .all(&*db).await?;

Opérateurs de base

use runique::search;

let actifs     = search!(users::Entity => Active eq true).all(&*db).await?;
let adultes    = search!(users::Entity => Age gte 18).all(&*db).await?;
let non_admins = search!(users::Entity => ! Level eq 99).all(&*db).await?;

// LIKE / ILIKE / NOT LIKE / NOT ILIKE
let rust          = search!(users::Entity => Bio like "%rust%").all(&*db).await?;
let rust_ci       = search!(users::Entity => Bio ilike "%rust%").all(&*db).await?;
let pas_rust      = search!(users::Entity => Bio not_like "%rust%").all(&*db).await?;
let pas_rust_ci   = search!(users::Entity => Bio not_ilike "%rust%").all(&*db).await?;

// NULL
let sans_bio = search!(users::Entity => Bio isnull).all(&*db).await?;
let avec_bio = search!(users::Entity => Bio not_null).all(&*db).await?;

contains / icontains / startswith / endswith / iexact

Ces opérateurs gèrent les % automatiquement.

let rust  = search!(posts::Entity => Title contains "rust").all(&*db).await?;
let rust_ci = search!(posts::Entity => Title icontains "rust").all(&*db).await?;
let hello = search!(posts::Entity => Title startswith "Hello").all(&*db).await?;
let rs    = search!(posts::Entity => Filename endswith ".rs").all(&*db).await?;
let user  = search!(users::Entity => Username iexact "Alice").first(&*db).await?;

IN / NOT IN

// IN littéral
let selected = search!(users::Entity => Id in [1, 2, 3]).all(&*db).await?;

// NOT IN littéral
let others = search!(users::Entity => Status not_in ["banned", "deleted"]).all(&*db).await?;

// IN dynamique (Vec, itérateur)
let ids: Vec<i32> = get_allowed_ids();
let users = search!(users::Entity => Id in (ids)).all(&*db).await?;

// NOT IN dynamique
let blocked: Vec<i32> = get_blocked_ids();
let clean = search!(users::Entity => Id not_in (blocked)).all(&*db).await?;

Note — inférence de type avec in (expr)

La macro convertit $val en Vec<_> en interne avant de l'appeler avec is_in. Cela évite les erreurs d'inférence dans les contextes où le type de retour global est complexe (ex: HashMap<i32, String>). Si le compilateur ne parvient pas à inférer le type des éléments, annoter la variable :

let ids: Vec<i32> = lignes.iter().filter_map(|l| l.plat_id).collect();
let plats = search!(plat::Entity => Id in (ids)).all(db).await?;

BETWEEN / NOT BETWEEN

let mid  = search!(users::Entity => Age range (18, 30)).all(&*db).await?;
let hors = search!(users::Entity => Age not_range (18, 30)).all(&*db).await?;

OR multi-colonnes — or(...)

// Recherche sur plusieurs colonnes
let results = search!(posts::Entity => or(Title icontains "rust", Content icontains "rust"))
    .all(&*db).await?;

// Avec variable runtime
let results = search!(posts::Entity => or(Title icontains term, Summary icontains term))
    .all(&*db).await?;

Multi-conditions (AND chainé)

Séparer les conditions par des virgules — chaque condition est un AND.

let results = search!(users::Entity =>
    Active eq true,
    Age gte 18,
    Role in ["admin", "moderator"],
)
.order_by_desc(users::Column::CreatedAt)
.limit(20)
.all(&*db)
.await?;

Tous les opérateurs sont disponibles en multi-conditions :

search!(users::Entity =>
    Role in ["admin", "moderator"],        // IN littéral
    Id in (ids_dynamiques),                // IN dynamique
    CreatedAt range (date_a, date_b),      // BETWEEN
    Bio not_null,                           // IS NOT NULL
    Username icontains "alice",            // ILIKE %alice%
    or(Title icontains q, Bio icontains q), // OR multi-colonnes
)

order_by_random() — ordre aléatoire

let selection = search!(produit::Entity => Disponible eq true)
    .order_by_random()
    .limit(5)
    .all(&*db).await?;

order_by_expr(expr, order) — expression custom

Accepte toute expression SeaORM IntoSimpleExpr — utile pour colonnes calculées, COALESCE, CASE, etc.

use sea_orm::Order;
use sea_orm::sea_query::Expr;

let results = search!(produit::Entity)
    .order_by_expr(Expr::col(produit::Column::Prix), Order::Desc)
    .all(&*db).await?;

.one() — exactement un résultat attendu

Retourne Ok(None) si aucune ligne ne correspond, Ok(Some(model)) si exactement une, et Err si plus d'une ligne correspond. Analogue au .get() de Django.

Charge au plus 2 lignes en interne pour détecter le cas ambigu sans scan complet.

// Retourne Err si plusieurs admins actifs correspondent
let admin = search!(users::Entity => IsStaff eq true, Username eq "alice")
    .one(&*db)
    .await?; // Result<Option<users::Model>, DbErr>

Utilisez .first() à la place si vous voulez seulement la première ligne d'un résultat potentiellement multiple, sans erreur.

.into_select() — projection partielle

Pour les cas où select_only(), column(), distinct() ou into_tuple() sont nécessaires, .into_select() expose le Select<E> SeaORM interne.

// Récupérer une seule colonne distincte
let difficultes: Vec<String> = search!(cour::Entity => Lang eq "fr")
    .into_select()
    .select_only()
    .column(cour::Column::Difficulte)
    .distinct()
    .into_tuple::<String>()
    .all(db.as_ref())  // ← .as_ref() obligatoire ici
    .await?;

Note — .as_ref() requis : une fois sorti du RuniqueQueryBuilder, les méthodes SeaORM utilisent un bound générique C: ConnectionTrait. Arc<DatabaseConnection> ne satisfait pas ce bound directement — db.as_ref() convertit explicitement en &DatabaseConnection. Les méthodes du RuniqueQueryBuilder (.all(), .first(), etc.) n'ont pas ce problème car elles prennent &DatabaseConnection en paramètre concret.

Via @Form — FormEntity

Si le form est lié à une entité via #[form(model = ...)], utiliser @Form comme raccourci :

#[form(schema = user_schema, model = users::Entity)]
pub struct UserForm;

// Identique à search!(users::Entity => ...)
let actifs = search!(@UserForm => Active eq true)
    .all(&*db).await?;

// Toutes les syntaxes sont supportées
search!(@UserForm =>
    Role in ["admin", "moderator"],
    Age gte 18,
)
.limit(10)
.all(&*db)
.await?;

INSERT — Créer

use sea_orm::Set;

let new_user = users::ActiveModel {
    email: Set("john@example.com".to_string()),
    username: Set("john".to_string()),
    password: Set(hash_password("password123")),
    ..Default::default()
};

let user = new_user.insert(&*db).await?;

UPDATE — Modifier

use sea_orm::{Set, Unchanged};

let mut user = users::Entity::find_by_id(1)
    .one(&*db)
    .await?
    .ok_or("User not found")?;

let mut user = user.into_active_model();
user.email = Set("newemail@example.com".to_string());

let updated = user.update(&*db).await?;

DELETE — Supprimer

// Supprimer un seul
let result = users::Entity::delete_by_id(1)
    .exec(&*db)
    .await?;

// Supprimer multiples
let result = users::Entity::delete_many()
    .filter(users::Column::Active.eq(false))
    .exec(&*db)
    .await?;

`search_cond!` — Condition brute

Retourne une sea_orm::Condition à injecter dans un query builder existant, au lieu de créer un nouveau builder.

SyntaxeRésultat
all_columns icontains valOR ILIKE sur toutes les colonnes
or("col1" icontains val, "col2" icontains val)OR ILIKE sur les colonnes nommées
?Col in (expr)Condition::all().add(col IN (...)) — no-op si vide
?Col not_in (expr)Condition::all().add(col NOT IN (...)) — no-op si vide
// Utilisé typiquement pour injecter une condition dans une requête déjà construite
let cond = search_cond!(commande::Entity => or("numero" icontains q, "statut" icontains q));
let results = commande::Entity::find()
    .filter(cond)
    .all(db).await?;

// Filtre conditionnel sur une Vec (no-op si vide)
let statuts: Vec<StatutCommande> = get_statuts_actifs();
let cond = search_cond!(commande::Entity => ?Statut in (statuts));
let results = commande::Entity::find()
    .filter(cond)
    .all(db).await?;

in (expr) avec un vec vide

Col in (vec) génère WHERE col IN (...). Si le vec est vide, le SQL produit est invalide ou retourne toujours faux selon le moteur. Garder le guard manuel :

let ids: Vec<i32> = compute_ids();
if !ids.is_empty() {
    query = query.filter(col.is_in(ids));
}

Workaround intégré — ?Col in (expr) : si la vec peut être vide, utiliser la syntaxe conditionnelle qui saute le filtre automatiquement :

let ids: Vec<i32> = compute_ids();  // peut être vide
let results = search!(item::Entity => ?Id in (ids)).all(db).await?;

De même ?Col not_in (expr) pour l'exclusion conditionnelle.

Pas de filtre conditionnel inline (condition booléenne)

Il n'est pas possible de sauter un filtre à l'intérieur d'un appel search! selon une condition booléenne arbitraire. Si certains filtres sont optionnels sur un prédicat autre qu'une vec vide, appliquer .filter() manuellement après le macro.

OR sur le même champ — plusieurs variantes enum

or(...) opère sur plusieurs colonnes différentes. Pour filtrer une même colonne sur plusieurs valeurs enum, utiliser Col in [V1, V2] (littéral) ou .filter(Col.is_in(vec)) :

// OK — IN littéral
search!(commande::Entity => Statut in ["en_attente", "accepte"])

// OK — Vec d'enum
let statuts = vec![StatutCommande::EnAttente, StatutCommande::Accepte];
query.filter(commande::Column::Statut.is_in(statuts))

// Pas disponible — syntaxe future prévue :
// search!(commande::Entity => Statut any [EnAttente, Accepte])

Pas d'agrégats

.avg(), .sum(), .count_by() ne sont pas disponibles sur RuniqueQueryBuilder. Pour les agrégats, utiliser SeaORM directement ou .into_select().