La lliçó anterior va deixar clar el principal maldecap de l'ADO.NET clàssic: cada consulta
exigeix escriure SQL a mà, gestionar paràmetres un a un, i mapejar manualment cada columna cap
a cada propietat d'un objecte. Un ORM (Object-Relational Mapper, mapador
objecte-relacional) automatitza precisament aquesta feina repetitiva: tradueix operacions
sobre objectes i col·leccions de C# al SQL equivalent, i reconstrueix objectes a partir dels
resultats sense codi manual de mapeig. Aquesta lliçó presenta Entity Framework Core (EF
Core), l'ORM oficial de .NET, i l'aplica al model de domini de BiblioTech: Llibre, Soci i
Prestec passen a viure en una base de dades real, consultables amb la mateixa sintaxi LINQ
que ja coneixes des del Mòdul 4, i desats amb el mateix async/await de la lliçó de
Programació Asíncrona.
Contingut
- Què és un ORM i quin problema resol davant l'ADO.NET manual
- Instal·lar Entity Framework Core i el proveïdor de SQLite
DbContextiDbSet<T>: mapejant el domini de BiblioTech- Configurar el model sense tocar les classes de domini: Fluent API a
OnModelCreating - Migracions:
dotnet ef migrations addidotnet ef database update - Consultes LINQ contra un
DbSet<T> - Desar canvis:
SaveChanges()iSaveChangesAsync()
- Què és un ORM i quin problema resol davant l'ADO.NET manual
Un ORM resol l'anomenat "desajust d'impedància" (impedance mismatch) entre dos mons que s'organitzen de forma diferent: els objectes de C# (amb propietats, herència, col·leccions imbricades) i les taules d'una base de dades relacional (files, columnes, claus foranes). A la lliçó anterior, aquest desajust es resolia a mà, línia a línia:
| ADO.NET manual (lliçó anterior) | Entity Framework Core (aquesta lliçó) | |
|---|---|---|
| Escriure el SQL de cada consulta | A mà, com a text | Generat automàticament a partir de LINQ |
| Mapejar files a objectes | A mà, columna a columna | Automàtic, segons el model configurat |
| Rastrejar què ha canviat per desar-ho | El programador decideix quin UPDATE/INSERT executar |
EF Core detecta els canvis i genera el SQL necessari |
| Control sobre el SQL exacte | Total | Alt nivell, amb la possibilitat de baixar a SQL manual si cal |
Un ORM no substitueix del tot l'ADO.NET: per sota, EF Core continua fent servir ADO.NET per comunicar-se amb la base de dades; el que aporta és una capa d'abstracció que estalvia escriure aquest SQL repetitiu a mà en el dia a dia.
- Instal·lar Entity Framework Core i el proveïdor de SQLite
EF Core s'instal·la com un conjunt de paquets NuGet, més una eina de línia d'ordres per gestionar migracions (apartat 5):
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-efMicrosoft.EntityFrameworkCore.Sqlite inclou el motor d'EF Core més el proveïdor específic de
SQLite (cada motor de base de dades té el seu propi paquet de proveïdor, seguint la mateixa
idea de "proveïdor" ja vista amb ADO.NET). Microsoft.EntityFrameworkCore.Design i l'eina
global dotnet-ef són necessàries per generar i aplicar migracions des del terminal.
DbContext i DbSet<T>: mapejant el domini de BiblioTech
DbContext i DbSet<T>: mapejant el domini de BiblioTechEl punt d'entrada d'EF Core és una classe que hereta de DbContext, amb una propietat
DbSet<T> per cada tipus que es vol persistir. Cada DbSet<T> representa, conceptualment,
"la taula de T a la base de dades":
using Microsoft.EntityFrameworkCore;
class BibliotecaDbContext : DbContext
{
public DbSet<Llibre> Llibres { get; set; } = null!;
public DbSet<Soci> Socis { get; set; } = null!;
public DbSet<Prestec> Prestecs { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder opcions)
{
opcions.UseSqlite("Data Source=bibliotech_ef.db");
}
}OnConfiguring indica a quina base de dades connectar-se (aquí, SQLite, amb la mateixa cadena
de connexió que a la lliçó anterior); en aplicacions més grans aquesta configuració se sol
injectar des de fora mitjançant DbContextOptions, però per a un exemple autocontingut n'hi
ha prou amb OnConfiguring. L'operador = null! (el ! és l'operador null-forgiving de
C#, recordant la lliçó de Pattern Matching i Característiques Modernes del Mòdul 4) li diu al
compilador "confia que aquesta propietat no serà null en temps d'execució", ja que EF Core
l'assigna automàticament en construir el DbContext, encara que el compilador no ho pugui
veure per si sol.
Llibre i Soci ja són perfectament vàlids per a EF Core tal com estan definits des de
Mòduls anteriors: no cal modificar-los. Prestec, amb les seves propietats de només lectura
(Llibre, Soci, DataPrestec) i el seu constructor Prestec(Llibre llibre, Soci soci), és
una mica més particular; l'apartat següent explica com EF Core el mapeja sense tocar aquesta
classe.
- Configurar el model sense tocar les classes de domini: Fluent API a
OnModelCreating
OnModelCreatingEF Core ofereix dues maneres d'indicar detalls del mapeig que no pot deduir per convenció (com
quina és la clau primària de cada taula): Data Annotations (atributs com [Key]
directament sobre les propietats del model) o Fluent API (codi de configuració
centralitzat, dins del mateix DbContext, sense tocar les classes de domini). Aquesta lliçó
fa servir Fluent API precisament per no haver d'afegir atributs de persistència a Llibre,
Soci i Prestec —classes que, fins ara, no sabien res de bases de dades ni d'EF Core, i que
continuen sense saber-ho:
class BibliotecaDbContext : DbContext
{
public DbSet<Llibre> Llibres { get; set; } = null!;
public DbSet<Soci> Socis { get; set; } = null!;
public DbSet<Prestec> Prestecs { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder opcions)
{
opcions.UseSqlite("Data Source=bibliotech_ef.db");
}
protected override void OnModelCreating(ModelBuilder generadorModel)
{
generadorModel.Entity<Llibre>().HasKey(llibre => llibre.Isbn);
generadorModel.Entity<Soci>().HasKey(soci => soci.Id);
// Prestec no te cap propietat pensada com a identificador propi;
// se li afegeix una clau "ombra" (shadow property), que existeix a la base
// de dades pero no com a propietat visible a la classe C#.
generadorModel.Entity<Prestec>().Property<int>("PrestecId");
generadorModel.Entity<Prestec>().HasKey("PrestecId");
}
}HasKey(llibre => llibre.Isbn) indica que Isbn és la clau primària de la taula Llibres
(igual que a la taula SQLite de la lliçó anterior); HasKey(soci => soci.Id) fa el mateix amb
Soci.Id, que a més ja era de només lectura per disseny (lliçó d'Encapsulació) —un bon
candidat natural a clau primària. Per a Prestec, que no té cap propietat pensada com a
identificador únic, es defineix una propietat ombra ("PrestecId"): existeix com a
columna a la base de dades i com a clau primària interna d'EF Core, però no apareix com a
propietat a la classe Prestec de C#. Aquest és un dels avantatges pràctics d'un ORM madur
com EF Core: el model de domini pot romandre net, centrat en les regles de negoci, sense
barrejar-se amb detalls de persistència.
EF Core també necessita saber com es relaciona Prestec amb Llibre i Soci; per
convenció, en detectar les propietats Llibre i Soci dins de Prestec, genera
automàticament columnes de clau forana (dues propietats ombra més, típicament LlibreIsbn i
SociId) sense necessitar configuració explícita addicional per a aquest exemple.
- Migracions:
dotnet ef migrations add i dotnet ef database update
dotnet ef migrations add i dotnet ef database updateUna migració és un fitxer de codi C#, generat automàticament, que descriu com transformar
l'esquema de la base de dades perquè coincideixi amb el model actual (quines taules crear,
quines columnes afegir...). Es generen i s'apliquen amb l'eina dotnet ef des del terminal,
a la carpeta del projecte:
| Ordre | Què fa |
|---|---|
dotnet ef migrations add <Nom> |
Compara el model actual (DbSet<T> + configuració d'OnModelCreating) amb l'historial de migracions, i genera una nova migració amb els canvis detectats |
dotnet ef database update |
Aplica contra la base de dades real totes les migracions pendents, creant o modificant taules segons calgui |
dotnet ef migrations add InicialBiblioTech crea una carpeta Migrations/ amb dos fitxers
per migració: un amb el codi que aplica el canvi (Up()) i un altre amb el que el desfà
(Down()), més un fitxer d'"instantània" del model complet tal com va quedar després
d'aquella migració. Cada vegada que el model canviï (una propietat nova a Llibre, per
exemple), es genera una nova migració amb migrations add i s'aplica amb database update;
EF Core calcula automàticament només la diferència respecte a la migració anterior, sense
necessitat d'escriure l'ALTER TABLE a mà.
- Consultes LINQ contra un
DbSet<T>
DbSet<T>Aquí és on EF Core connecta directament amb una cosa que ja domines: DbSet<T> implementa
IQueryable<T> (una extensió d'IEnumerable<T>, la interfície que tanca la lliçó de
Col·leccions), així que es consulta amb exactament la mateixa sintaxi LINQ de la lliçó de LINQ
—només que, per sota, EF Core tradueix aquestes operacions a SQL i l'executa contra la base de
dades, en lloc de recórrer una col·lecció ja carregada a memòria:
using BibliotecaDbContext context = new BibliotecaDbContext();
List<Llibre> llibresDisponibles = context.Llibres
.Where(llibre => llibre.Disponible)
.OrderBy(llibre => llibre.Titol)
.ToList();
foreach (Llibre llibre in llibresDisponibles)
{
Console.WriteLine(llibre.Titol);
}
Llibre? rayuela = context.Llibres.FirstOrDefault(llibre => llibre.Isbn == "978-84-376-0495-4");Where, OrderBy, FirstOrDefault... són els mateixos operadors de la lliçó de LINQ,
escrits exactament igual; la diferència és que aquí, context.Llibres.Where(...) es tradueix
internament a una sentència SELECT ... WHERE ... a SQLite, en lloc de filtrar una llista ja
existent a memòria. BibliotecaDbContext, igual que SqliteConnection a la lliçó anterior,
implementa IDisposable: es declara amb using pel mateix motiu.
- Desar canvis:
SaveChanges() i SaveChangesAsync()
SaveChanges() i SaveChangesAsync()Afegir, modificar o eliminar entitats a través d'un DbContext no impacta immediatament la
base de dades: EF Core rastreja els canvis a memòria, i només els tradueix a SQL
(INSERT, UPDATE, DELETE) quan es crida explícitament SaveChanges() (o el seu
equivalent asíncron SaveChangesAsync(), recordant async/await de la lliçó de Programació
Asíncrona):
using BibliotecaDbContext context = new BibliotecaDbContext();
Llibre nouLlibre = new Llibre("El Aleph", "Jorge Luis Borges", "978-84-376-0497-8");
context.Llibres.Add(nouLlibre);
await context.SaveChangesAsync(); // aqui, i nomes aqui, s'executa l'INSERT real contra SQLite
Console.WriteLine("Llibre desat correctament.");using BibliotecaDbContext context = new BibliotecaDbContext();
Llibre? llibre = context.Llibres.FirstOrDefault(l => l.Isbn == "978-84-376-0497-8");
if (llibre is not null)
{
llibre.Prestar(); // canvia Disponible a false; EF Core detecta aquest canvi automaticament
await context.SaveChangesAsync(); // genera i executa l'UPDATE necessari
}En el segon exemple no cal cap context.Llibres.Update(...) explícit: com que llibre es va
obtenir a través del propi context, EF Core ja l'està rastrejant (change tracking) i
detecta, en cridar SaveChangesAsync(), que la seva propietat Disponible ha canviat, generant
l'UPDATE corresponent. Aquesta és una de les diferències més visibles davant l'ADO.NET
manual, on cada UPDATE calia escriure'l i executar-lo explícitament.
classDiagram
class BibliotecaDbContext {
+DbSet~Llibre~ Llibres
+DbSet~Soci~ Socis
+DbSet~Prestec~ Prestecs
#OnConfiguring(DbContextOptionsBuilder)
#OnModelCreating(ModelBuilder)
}
BibliotecaDbContext --|> DbContext
BibliotecaDbContext --> "*" Llibre
BibliotecaDbContext --> "*" Soci
BibliotecaDbContext --> "*" Prestec
Errors Comuns i Consells
- Oblidar
SaveChanges()/SaveChangesAsync(): afegir una entitat ambcontext.Llibres.Add(...)no la persisteix per si sola; sense cridarSaveChanges(), el canvi només existeix a memòria i es perd en tancar elDbContext. - No generar ni aplicar una migració després de canviar el model: si s'afegeix una
propietat nova a
Llibresense executardotnet ef migrations addidotnet ef database update, la base de dades real queda desincronitzada respecte al model, i les consultes fallaran en temps d'execució en no trobar la columna esperada. - Oblidar
HasKeyper a un tipus sense una propietat d'identificador natural: EF Core exigeix que tota entitat tingui una clau primària; si cap propietat serveix com a tal (el cas dePrestec), cal definir una propietat ombra explícitament ambProperty<T>()+HasKey()aOnModelCreating. - Crear un
DbContextnou per cada operació petita sense necessitat, o reutilitzar-ne un de sol durant massa temps: la pràctica habitual a EF Core és una vida curta perDbContext(típicament, un per operació o per petició HTTP en una API web, tema del Mòdul 7), ni un de compartit per a tota l'aplicació ni un de diferent per cada línia de codi. - Consell: fes servir sempre les consultes LINQ per llegir dades, i dosifica el SQL manual
(possible també des d'EF Core, amb
FromSqlRaw, fora de l'abast d'aquesta lliçó) per als casos puntuals on LINQ no expressi bé el que necessites; per a la immensa majoria d'operacions del dia a dia, LINQ contra unDbSet<T>és més llegible i menys propens a errors que el SQL escrit a mà.
Exercicis
-
Defineix
BibliotecaDbContextambDbSet<Llibre> LlibresiDbSet<Soci> Socis, configurantHasKeyper a tots dos aOnModelCreatingtal com s'ha mostrat en aquesta lliçó. Genera la migració inicial ambdotnet ef migrations addi aplica-la ambdotnet ef database update. -
Fent servir el
BibliotecaDbContextde l'exercici anterior, afegeix dosLlibrenous ambcontext.Llibres.Add(...)i desa els canvis ambSaveChangesAsync(). Després, en una nova instància deBibliotecaDbContext, consulta amb LINQ (Where+OrderBy) els llibres disponibles ordenats per títol. -
Recupera un
Llibreexistent ambFirstOrDefaultpel seuIsbn, cridaPrestar()sobre ell, i desa els canvis ambSaveChangesAsync()sense cridar cap mètodeUpdateexplícit. Torna a consultar-lo en una nova instància del context i comprova queDisponibleara ésfalse.
Solucions
class BibliotecaDbContext : DbContext
{
public DbSet<Llibre> Llibres { get; set; } = null!;
public DbSet<Soci> Socis { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder opcions)
{
opcions.UseSqlite("Data Source=bibliotech_ef.db");
}
protected override void OnModelCreating(ModelBuilder generadorModel)
{
generadorModel.Entity<Llibre>().HasKey(llibre => llibre.Isbn);
generadorModel.Entity<Soci>().HasKey(soci => soci.Id);
}
}
dotnet ef migrations add InicialBiblioTech
dotnet ef database update
using (BibliotecaDbContext context = new BibliotecaDbContext())
{
context.Llibres.Add(new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
context.Llibres.Add(new Llibre("Ficcions", "Jorge Luis Borges", "978-84-376-0496-1"));
await context.SaveChangesAsync();
}
using (BibliotecaDbContext altreContext = new BibliotecaDbContext())
{
List<Llibre> disponibles = altreContext.Llibres
.Where(l => l.Disponible)
.OrderBy(l => l.Titol)
.ToList();
foreach (Llibre llibre in disponibles)
{
Console.WriteLine(llibre.Titol);
}
}
using (BibliotecaDbContext context = new BibliotecaDbContext())
{
Llibre? llibre = context.Llibres.FirstOrDefault(l => l.Isbn == "978-84-376-0495-4");
if (llibre is not null)
{
llibre.Prestar();
await context.SaveChangesAsync();
}
}
using (BibliotecaDbContext altreContext = new BibliotecaDbContext())
{
Llibre? llibreActualitzat = altreContext.Llibres.FirstOrDefault(l => l.Isbn == "978-84-376-0495-4");
Console.WriteLine(llibreActualitzat?.Disponible); // False
}
Conclusió
En aquesta lliçó has conegut Entity Framework Core: quin problema resol un ORM davant
l'ADO.NET manual, com definir un DbContext amb DbSet<T> per a cada tipus del domini de
BiblioTech, com configurar el model amb Fluent API sense embrutar les classes de domini amb
atributs de persistència, com generar i aplicar migracions, i com consultar i desar dades
reutilitzant LINQ i async/await, dues eines que ja dominaves des del Mòdul 4. Llibre,
Soci i Prestec ja viuen en una base de dades relacional real, gestionada per un ORM en
lloc de SQL escrit a mà.
Tot el que s'ha vist fins ara al Mòdul 5 assumeix que BiblioTech és l'única aplicació que
llegeix i escriu aquestes dades. L'última lliçó del mòdul, Treball amb JSON i Consum d'APIs
REST, fa un pas més: aprofundeix en System.Text.Json per a escenaris més complexos, i fa
servir HttpClient perquè BiblioTech consulti informació addicional des d'un servei extern
—l'escenari habitual de qualsevol aplicació moderna que no viu aïllada, sinó que es comunica
amb altres sistemes a través d'una API REST.
Curs de Programació en C#
Mòdul 1: Introducció al C#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
