Cosa impari
- Cos'e un database contro un foglio di calcolo e perche un database reattivo cambia il tuo modo di costruire
- Schema, query, mutation e action, con type safety end-to-end dal database all'UI
- Gli index per letture veloci e soft delete contro hard delete con un vero pattern di ripristino
Panoramica
I tuoi utenti possono connettersi. Ora hanno bisogno di dati che persistono: il loro profilo, i loro progetti, il loro lavoro salvato. Questo e lo strato database, e Convex si adatta particolarmente bene ai builder a cui si rivolge questo corso, perche fa qualcosa che la maggior parte dei database non fa - e reattivo. Quando i dati cambiano, ogni parte della tua UI che dipende da quei dati si aggiorna da sola, senza codice di aggiornamento manuale. In piu, i tuoi tipi circolano end-to-end, dal database ai tuoi componenti React, cosicche un'intera categoria di bug semplicemente non puo prodursi. Questa lezione insegna il modello: cos'e un database, i quattro tipi di funzione che scrivi, gli index e i pattern di cancellazione che separano un giocattolo da un vero prodotto.
Cosa imparerai
Imparerai cos'e davvero un database e perche batte un foglio di calcolo per un'app, cosa ti da la reattivita, come definire uno schema e scrivere query, mutation e action, come la type safety end-to-end impedisce i bug prima che arrivino, quando e perche aggiungere un index e la differenza tra hard e soft delete con un pattern di ripristino che puoi copiare.
Prerequisiti
Un'app funzionante con l'auth Clerk della lezione precedente, perche vuoi legare dati agli utenti connessi. La lezione d'architettura, per ricordarti che il database e il terzo strato. E la pagina dei fondamenti su cos'e un database, se l'idea di uno schema o di una tabella e tutta nuova. Una familiarita con del TypeScript semplice aiuta, perche in Convex la tua logica di database e solo funzioni TypeScript.
Il problema
I principianti afferrano cio che conoscono: un foglio di calcolo, un file CSV o un mucchio di JSON salvato su disco. Cio funziona, finche due utenti toccano gli stessi dati, o devi trovare un record tra diecimila in fretta, o cambi la forma dei tuoi dati e tutto si rompe in silenzio. Allora collegano un database tradizionale a lato e passano giorni in glue code: recuperare al caricamento, ri-recuperare dopo ogni cambiamento, gestire gli stati di caricamento, tenere l'UI sincronizzata e rintracciare i bug di dati obsoleti dove lo schermo mostra vecchi numeri. L'essenziale di questa colla e puro spreco. Un database reattivo e tipizzato cancella la colla e la classe di bug con essa.
Un database non e un foglio di calcolo
Un foglio Excel o un CSV va bene per una lista che leggi a occhio. Un database e costruito per un altro job: numerosi utenti che leggono e scrivono simultaneamente, trovare record precisi in fretta, anche tra milioni, imporre la forma dei tuoi dati perche nessuna riga sbagliata si infili, e non perdere ne corrompere mai dati quando due cose arrivano nello stesso momento. Un foglio di calcolo non ha idea di chi altro lo edita, e nessun modo di garantire che una colonna "prezzo" sia sempre un numero. Un database puo fare entrambe le cose. L'upgrade mentale e questo: un foglio di calcolo e un documento che guardi; un database e un servizio a cui la tua app parla e che garantisce che i dati restino corretti e coerenti, non importa il numero di utenti o il carico. Dall'istante in cui i tuoi dati sono condivisi tra utenti o devono essere interrogati in fretta, hai superato il foglio di calcolo.
- Foglio di calcolo/CSV: un editor alla volta, nessuna garanzia sulla forma dei dati, lento da frugare su larga scala, facile da corrompere.
- Database: numerosi utenti simultanei, forma dei dati imposta (lo schema), lookup rapidi via index, sicuro sotto carico.
- Convex aggiunge la reattivita sopra: i cambiamenti sono spinti automaticamente a ogni client connesso.
Perche la reattivita conta
In un setup normale, scrivi molto codice per tenere lo schermo sincronizzato con il database: recuperare al caricamento della pagina, ri-recuperare dopo aver cambiato qualcosa, fare polling per gli aggiornamenti di altri utenti, gestire a mano gli stati di caricamento e di errore. Ognuno e un'occasione di bug dove l'UI mostra dati obsoleti. Convex inverte cio. Una query e un abbonamento dal vivo: la scrivi una volta, e ogni volta che i dati che legge cambiano - che li abbia cambiati tu o un altro utente - Convex spinge il nuovo risultato al tuo componente e lui si ri-rende. Cancelli la logica di re-fetch, il polling e tutta la classe di bug di dati obsoleti. Per un prodotto collaborativo o real-time (un dashboard, una chat, una lista condivisa), e trasformativo, e persino per un'app semplice, toglie un mucchio di cablaggio noioso e soggetto a errori.
Schema, query, mutation e action
Convex ti da quattro blocchi di costruzione. Lo schema dichiara la forma dei tuoi dati - le tue tabelle e i loro campi e tipi - perche il database possa imporla. Le query leggono i dati e sono reattive. Le mutation cambiano i dati (insert, update, delete) e girano in una transazione, quindi riescono interamente o falliscono interamente. Le action servono per parlare al mondo esterno (chiamare una API esterna, inviare una e-mail), dove devi fare qualcosa che non e una pura lettura o scrittura di database. Ecco un vero schema piu una query e una mutation, i gesti quotidiani che scriverai di continuo.
// convex/schema.ts - la forma dei tuoi dati, imposta dal database
import { defineSchema, defineTable } from 'convex/server'
import { v } from 'convex/values'
export default defineSchema({
projects: defineTable({
ownerId: v.string(), // l'ID utente Clerk proprietario di questo progetto
name: v.string(),
archivedAt: v.optional(v.number()), // null/assente = attivo ; un timestamp = soft-deleted
})
// Index perche 'trovare i progetti di questo utente' sia un lookup rapido, non un full scan
.index('by_owner', ['ownerId']),
})// convex/projects.ts - una query (lettura, reattiva) e una mutation (scrittura, transazionale)
import { query, mutation } from './_generated/server'
import { v } from 'convex/values'
// Reattivo : ogni client abbonato si ri-rende quando i dati cambiano.
export const listActive = query({
args: { ownerId: v.string() },
handler: async (ctx, { ownerId }) => {
const rows = await ctx.db
.query('projects')
.withIndex('by_owner', (q) => q.eq('ownerId', ownerId))
.collect()
// Le righe soft-deleted hanno archivedAt posto - nascondile dalla lista attiva.
return rows.filter((row) => row.archivedAt === undefined)
},
})
export const create = mutation({
args: { ownerId: v.string(), name: v.string() },
handler: async (ctx, { ownerId, name }) => {
return await ctx.db.insert('projects', { ownerId, name })
},
})Type safety end-to-end
E il super-potere silenzioso. Poiche il tuo schema e TypeScript e Convex genera tipi a partire da esso, il tipo dei tuoi dati circola per tutto il percorso, dal database ai tuoi componenti React. Se rinomini un campo nello schema, ogni query, mutation e componente che usava il vecchio nome diventa rosso nel tuo editor, prima ancora che tu lanci l'app. Non puoi leggere per sbaglio un campo che non esiste, passare l'argomento sbagliato a una mutation o supporre che una string sia un numero. Intere classi di bug "e crashato in produzione perche i dati non avevano la forma che supponevo" diventano impossibili, perche il tuo editor e il typechecker li colgono mentre scrivi. Per qualcuno che costruisce con un agente, e enorme: l'agente riceve gli stessi segnali di tipo e scrive codice corretto molto piu spesso, e il tuo quality gate di typecheck coglie il resto prima che pubblichi.
Index: rendere veloci le letture
Quando chiedi al database "dammi tutti i progetti che appartengono a questo utente", ha due modi di rispondere. Senza index, scansiona ogni riga e verifica ciascuna - bene per dieci righe, dolorosamente lento per un milione. Con un index su ownerId, salta direttamente alle righe corrispondenti, come usare l'indice alla fine di un libro invece di leggere ogni pagina. La regola e semplice: ogni campo su cui filtri o ordini regolarmente merita un index. L'hai visto nello schema qui sopra - l'index by_owner e cio che rende veloce listActive, non importa quanti progetti esistano in totale. Aggiungere index presto non costa quasi nulla; scoprire che ne avevi bisogno quando la tua app si mette a strisciare sotto veri dati costa una sessione di debugging stressante.
- Nessun index: il database verifica ogni riga (un full scan). Bene per tabelle molto piccole, lento su larga scala.
- Con un index: salta direttamente alle righe corrispondenti. Veloce anche con milioni di righe.
- Indicizza i campi su cui filtri o ordini spesso - ownerId, status, createdAt sono comuni.
- Definisci l'index nello schema; usalo nelle query con .withIndex(...).
Soft delete contro hard delete, con ripristino
Un hard delete rimuove una riga definitivamente - e sparita, e ogni possibilita di recuperarla anche. Un soft delete tiene la riga ma la marca come cancellata, spesso con un timestamp come il campo archivedAt dello schema qui sopra. I veri prodotti preferiscono quasi sempre i soft delete per i dati utente, perche gli utenti cancellano di continuo cose per sbaglio, e "spiacenti, e sparito per sempre" e un'esperienza orribile e a volte un problema di conformita. Con un soft delete, puoi offrire un cestino o un undo, ripristinare i dati e tenere intatti la cronologia e gli audit trail. Fai un hard delete solo piu tardi, secondo un calendario, per dati soft-deleted da abbastanza tempo e che hai il diritto legale di cancellare. Ecco il pattern: cancellare pone il timestamp, ripristinare lo svuota, e un job di sfondo puo davvero hard-deletare le righe molto vecchie.
// Soft delete : marcalo, non distruggerlo. Ripristinare diventa allora banale.
export const softDelete = mutation({
args: { id: v.id('projects') },
handler: async (ctx, { id }) => {
await ctx.db.patch(id, { archivedAt: Date.now() })
},
})
export const restore = mutation({
args: { id: v.id('projects') },
handler: async (ctx, { id }) => {
await ctx.db.patch(id, { archivedAt: undefined })
},
})
// Hard delete : solo per dati soft-deleted da tempo, spesso eseguito da un job pianificato.
export const purge = mutation({
args: { id: v.id('projects') },
handler: async (ctx, { id }) => {
await ctx.db.delete(id)
},
})Errori frequenti
I comuni: scrivere codice di re-fetch e polling manuale quando le query Convex sono gia reattive, cosicche lotti contro lo strumento; dimenticare gli index e guardare l'app strisciare appena veri dati arrivano; hard-deletare dati utente senza ripristino poi ricevere il ticket di supporto che non puoi risolvere; provare a chiamare una API esterna in una query o mutation invece di una action; e allentare il tuo schema (tutto opzionale, tutto una string) finche la type safety che ti proteggeva svapora. Appoggiati allo schema e alla reattivita, invece di lavorare intorno a essi.
ROI business
Un database reattivo e tipizzato e al tempo stesso moltiplicatore di produttivita e moltiplicatore di qualita. Scrivi radicalmente meno cablaggio, quindi le feature pubblicano piu in fretta. I tipi end-to-end colgono i bug prima che raggiungano un cliente, quindi pubblichi con meno incendi. Gli index tengono l'app veloce man mano che cresci, il che protegge sia la conversione sia la tua reputazione. E il pattern soft delete trasforma una categoria di incidenti di supporto catastrofici - "ho cancellato tutto per sbaglio" - in un ripristino con un click. Per un fondatore, il tempo guadagnato sul cablaggio e i bug che non arrivano mai valgono molto piu del costo moderato del servizio.
Checklist
Sei pronto ad andare avanti quando ognuno di questi punti e vero in una vera app che hai costruito, non solo capito in teoria.
- Sai spiegare perche un database batte un foglio di calcolo appena i dati sono condivisi o voluminosi.
- Hai uno schema con almeno una tabella, un index e funzioni query e mutation funzionanti.
- Sai vedere i tipi circolare dallo schema ai tuoi componenti e cogliere un errore di rinomina nell'editor.
- Hai implementato un soft delete con un ripristino funzionante e sai quando un hard delete e appropriato.
Risorse
La documentazione Convex e eccellente e aggiornata - tieni nei preferiti le pagine su schema, query, mutation e index. La pagina dei fondamenti su cos'e un database ancora i concetti se uno era traballante. Ora hai l'auth e i dati; la prossima lezione mette in sicurezza i segreti che collegano ogni servizio che hai aggiunto, incluso come cifrare le chiavi API che i tuoi utenti ti affidano.
La tua missione
Aggiungi una tabella Convex per qualcosa di cui la tua app ha bisogno (una lista di item, di progetti o di note, legata all'utente connesso), con un index sul proprietario. Scrivi una query reattiva e una mutation di creazione, poi implementa soft delete e restore. Cancella un item, conferma che sparisce dalla lista, poi ripristinalo e guardalo riapparire senza aggiornamento di pagina. Questo solo esercizio prova che capisci la reattivita, gli index e le cancellazioni ripristinabili tutti in una volta.
Prossima lezione
La tua app ora ha utenti e dati, il che vuol dire che ha un mucchio crescente di chiavi segrete che li collegano. La prossima lezione e la versione disciplinata della gestione dei segreti: file env, .gitignore, cosa fare se hai gia pubblicato un segreto, deploy key Convex e cifrare le chiavi API degli utenti invece di memorizzarle in chiaro.

Commenti
Caricamento dei commenti.
Pubblica un commento