Models

L'ORM di Flux: potente come Eloquent, pulito come Python

Active Record Pattern: Ogni model rappresenta una tabella del database. Le istanze del model rappresentano righe, con metodi intuitivi per query e relazioni.

Definire un Model

I model si creano nella cartella app/models/:

# app/models/User.flux
model User {
    # Tabella del database (opzionale, default: users)
    table = "users"
    
    # Campi fillable (mass assignment)
    fillable = ["name", "email", "password"]
    
    # Campi nascosti nella serializzazione
    hidden = ["password", "remember_token"]
    
    # Cast automatici
    casts = {
        "email_verified_at": "datetime",
        "is_admin": "boolean",
        "metadata": "json"
    }
    
    # Timestamps automatici (created_at, updated_at)
    timestamps = True
}

CRUD Operations

Operazioni base per creare, leggere, aggiornare ed eliminare:

Create

# Metodo 1: create()
user = User.create({
    "name": "Mario Rossi",
    "email": "mario@example.com",
    "password": hash("secret123")
})

# Metodo 2: new() + save()
user = User()
user.name = "Luigi Verdi"
user.email = "luigi@example.com"
user.save()

# Bulk insert
users = User.insert([
    {"name": "Anna", "email": "anna@example.com"},
    {"name": "Paolo", "email": "paolo@example.com"}
])

# FirstOrCreate
user = User.first_or_create(
    {"email": "test@example.com"},  # Cerca per
    {"name": "Test User"}           # Crea con
)

Read

# Trova per ID
user = User.find(1)
user = User.find_or_404(1)  # Lancia 404 se non trovato

# Prima/ultima riga
first_user = User.first()
last_user = User.latest().first()

# Query con condizioni
users = User.where("age", ">", 18).get()
user = User.where("email", "mario@example.com").first()

# Query multiple condizioni
users = User.where("active", True)
    .where("role", "admin")
    .order_by("name")
    .get()

# Tutti i record
all_users = User.all()

# Count
total = User.count()
adults = User.where("age", ">=", 18).count()

Update

# Update singolo record
user = User.find(1)
user.name = "Nuovo Nome"
user.save()

# Update con dizionario
user.update({
    "name": "Mario Bianchi",
    "email": "mario.bianchi@example.com"
})

# Mass update
User.where("status", "pending")
    .update({"status": "active"})

# Increment/Decrement
post = Post.find(1)
post.increment("views")  # views + 1
post.decrement("stock", 5)  # stock - 5

Delete

# Delete singolo
user = User.find(1)
user.delete()

# Mass delete
User.where("created_at", "<", "2020-01-01").delete()

# Soft Delete (se abilitato)
user.soft_delete()  # Setta deleted_at
user.restore()      # Ripristina

# Force delete (anche con soft delete)
user.force_delete()

Query Builder

Costruisci query complesse con metodi concatenabili:

# Where conditions
users = User.where("age", ">", 18).get()
users = User.where("name", "LIKE", "%mario%").get()
users = User.where_in("role", ["admin", "editor"]).get()
users = User.where_between("age", [18, 65]).get()
users = User.where_null("deleted_at").get()

# Or conditions
users = User.where("role", "admin")
    .or_where("role", "superadmin")
    .get()

# Complex where
users = User.where(lambda query:
    query.where("age", ">", 18)
         .where("city", "Roma")
).or_where("vip", True).get()

# Order e Limit
recent_posts = Post.order_by("created_at", "desc")
    .limit(10)
    .get()

# Offset e Pagination
users = User.order_by("id")
    .offset(20)
    .limit(10)
    .get()

# Distinct
cities = User.select("city").distinct().get()

# Group By e Having
stats = Order.select("status", db.raw("COUNT(*) as total"))
    .group_by("status")
    .having("total", ">", 10)
    .get()

Relazioni

Definisci relazioni tra model in modo elegante:

One to Many

# app/models/User.flux
model User {
    def posts(self):
        return self.has_many(Post)
}

# app/models/Post.flux
model Post {
    def author(self):
        return self.belongs_to(User, "user_id")
}

# Utilizzo
user = User.find(1)
posts = user.posts().get()
recent_posts = user.posts()
    .where("published", True)
    .latest()
    .limit(5)
    .get()

# Accesso inverso
post = Post.find(1)
author = post.author().first()

Many to Many

# app/models/User.flux
model User {
    def roles(self):
        return self.belongs_to_many(Role, "user_roles")
}

# app/models/Role.flux
model Role {
    def users(self):
        return self.belongs_to_many(User, "user_roles")
}

# Attach/Detach
user = User.find(1)
user.roles().attach(1)  # Aggiungi ruolo con ID 1
user.roles().attach([1, 2, 3])  # Multipli
user.roles().detach(1)  # Rimuovi
user.roles().sync([1, 3])  # Sincronizza (solo 1 e 3)

# Con pivot data
user.roles().attach(1, {
    "assigned_at": datetime.now(),
    "assigned_by": current_user.id
})

Has One

# app/models/User.flux
model User {
    def profile(self):
        return self.has_one(Profile)
}

# app/models/Profile.flux  
model Profile {
    def user(self):
        return self.belongs_to(User)
}

# Utilizzo
user = User.find(1)
profile = user.profile().first()

# Create related
user.profile().create({
    "bio": "Developer",
    "website": "example.com"
})

Polymorphic Relations

# app/models/Comment.flux
model Comment {
    def commentable(self):
        return self.morph_to()
}

# app/models/Post.flux
model Post {
    def comments(self):
        return self.morph_many(Comment, "commentable")
}

# app/models/Video.flux
model Video {
    def comments(self):
        return self.morph_many(Comment, "commentable")
}

# Utilizzo
post = Post.find(1)
comments = post.comments().get()

# Create polymorphic
post.comments().create({
    "content": "Great post!"
})

Eager Loading

Ottimizza le query caricando le relazioni in anticipo:

# N+1 problem (evita!)
users = User.all()
for user in users:
    print(user.posts().count())  # Query per ogni user!

# Eager loading (ottimizzato)
users = User.with("posts").get()
for user in users:
    print(len(user.posts))  # Nessuna query aggiuntiva

# Multiple relations
posts = Post.with("author", "comments", "tags").get()

# Nested relations
posts = Post.with("comments.user").get()

# Conditional loading
users = User.with({
    "posts": lambda query: query.where("published", True)
}).get()

# Lazy eager loading
users = User.all()
users.load("posts")  # Carica dopo

Scopes

Definisci query riutilizzabili:

# app/models/Post.flux
model Post {
    # Local scope
    def scope_published(self, query):
        return query.where("published", True)
            .where("published_at", "<=", datetime.now())
    
    def scope_by_author(self, query, author_id):
        return query.where("author_id", author_id)
    
    # Global scope (sempre applicato)
    def boot(self):
        self.add_global_scope("active", 
            lambda query: query.where("deleted_at", None)
        )
}

# Utilizzo
published = Post.published().get()
user_posts = Post.published().by_author(1).get()

# Disabilita global scope
all_posts = Post.without_global_scope("active").get()

Mutators & Accessors

Modifica automaticamente i valori in get/set:

model User {
    # Accessor (get)
    def get_full_name(self):
        return f"{self.first_name} {self.last_name}"
    
    # Mutator (set)
    def set_password(self, value):
        self.attributes["password"] = hash(value)
    
    # Accessor per attributo esistente
    def get_email(self):
        return self.attributes["email"].lower()
}

# Utilizzo
user = User.find(1)
print(user.full_name)  # Chiama get_full_name()
user.password = "newpass"  # Chiama set_password()

Events

Reagisci agli eventi del model:

model User {
    # Eventi disponibili:
    # creating, created, updating, updated,
    # saving, saved, deleting, deleted
    
    def boot(self):
        # Prima di creare
        self.creating(lambda user:
            user.uuid = generate_uuid()
        )
        
        # Dopo aver salvato
        self.saved(lambda user:
            Cache.forget(f"user_{user.id}")
        )
        
        # Prima di eliminare
        self.deleting(lambda user:
            user.posts().delete()  # Elimina posts correlati
        )
}

# Observer pattern
class UserObserver:
    def created(self, user):
        send_welcome_email(user)
        
    def updated(self, user):
        if user.is_dirty("email"):
            send_verification_email(user)

# Registra observer
User.observe(UserObserver)

Advanced Features

Soft Deletes

model Post {
    use_soft_deletes = True  # Abilita soft delete
    
    deleted_at = "deleted_at"  # Colonna timestamp
}

# Utilizzo
post = Post.find(1)
post.delete()  # Soft delete (setta deleted_at)

# Query
posts = Post.all()  # Solo non eliminati
all_posts = Post.with_trashed().get()  # Tutti
deleted = Post.only_trashed().get()  # Solo eliminati

# Restore
post.restore()  # Rimuove deleted_at

# Force delete
post.force_delete()  # Elimina davvero dal DB

Query Caching

# Cache query results
users = User.where("active", True)
    .cache(minutes=10)
    .get()

# Cache con tag
posts = Post.with("author")
    .cache_tags(["posts", f"user_{user_id}"])
    .cache(minutes=30)
    .get()

# Invalida cache
Cache.tags(["posts"]).flush()

Database Transactions

# Transaction automatica
with db.transaction():
    user = User.create(user_data)
    profile = user.profile().create(profile_data)
    # Commit automatico se tutto ok
    # Rollback automatico in caso di errore

# Transaction manuale
db.begin_transaction()
try:
    # Operazioni
    db.commit()
except Exception as e:
    db.rollback()
    raise e

Model Factory

Genera dati fake per testing:

# database/factories/UserFactory.flux
factory User {
    "name": fake.name(),
    "email": fake.unique().email(),
    "password": hash("password"),
    "created_at": fake.date_between("-1 year", "now")
}

# Utilizzo
user = User.factory().create()
users = User.factory().count(10).create()

# Override attributi
admin = User.factory().create({
    "role": "admin",
    "email": "admin@example.com"
})

# Stati
factory User {
    def admin(self):
        return self.state({
            "role": "admin",
            "permissions": ["all"]
        })
}

admin = User.factory().admin().create()

Best Practices

📝 Naming Conventions

  • Model: singolare, PascalCase (User, Post)
  • Tabelle: plurale, snake_case (users, posts)
  • Foreign key: model_id (user_id)
  • Pivot table: singolari ordinati (role_user)

🚀 Performance

  • Usa eager loading per evitare N+1
  • Seleziona solo campi necessari
  • Indicizza foreign keys
  • Usa cache per query costose

🔒 Sicurezza

  • Definisci sempre $fillable
  • Nascondi campi sensibili con $hidden
  • Valida input prima di salvare
  • Usa scope per filtri di sicurezza
✨ Prossimo passo: Scopri come creare interfacce dinamiche con il sistema di Templates di Flux.