Operaciones CRUD con bases de datos
Las operaciones CRUD (Create, Read, Update, Delete) son los cuatro pilares fundamentales para trabajar con cualquier sistema de almacenamiento de datos. Representan las acciones básicas que podemos realizar sobre nuestros datos: crear nuevos registros, leer información existente, actualizar registros y eliminar datos que ya no necesitamos. En el contexto de Entity Framework Core, estas operaciones se realizan de manera intuitiva utilizando objetos de C# y métodos que abstraen la complejidad del SQL subyacente.
La comprensión y dominio de las operaciones CRUD es esencial para cualquier desarrollador, ya que prácticamente todas las aplicaciones empresariales requieren manipular datos de alguna forma. Entity Framework Core nos proporciona herramientas poderosas para realizar estas operaciones de manera eficiente, con seguimiento automático de cambios, validaciones integradas y optimizaciones de rendimiento.
En este artículo aprenderemos a implementar cada una de las operaciones CRUD utilizando Entity Framework Core, desde las técnicas más básicas hasta patrones más avanzados, incluyendo operaciones asíncronas, validaciones y manejo de errores.
Conceptos fundamentales de CRUD
Antes de implementar las operaciones, es importante comprender qué representa cada una y cómo se mapea en el contexto de bases de datos relacionales.
Definición de operaciones CRUD
Operación | Significado | Acción en BD | Método EF Core |
---|---|---|---|
Create | Crear | INSERT | Add() , AddRange() |
Read | Leer | SELECT | Find() , Where() , ToList() |
Update | Actualizar | UPDATE | Modificar propiedades + SaveChanges() |
Delete | Eliminar | DELETE | Remove() , RemoveRange() |
Estados de entidades en EF Core
Entity Framework Core rastrea el estado de cada entidad para saber qué operaciones debe ejecutar:
Estado | Descripción | Acción en SaveChanges |
---|---|---|
Unchanged | La entidad no ha sido modificada | Ninguna |
Added | Entidad nueva, no existe en la BD | INSERT |
Modified | Entidad existente con cambios | UPDATE |
Deleted | Entidad marcada para eliminación | DELETE |
Detached | No está siendo rastreada | Ninguna |
Operación Create: Creando nuevos registros
La operación Create nos permite agregar nuevos registros a nuestra base de datos. EF Core proporciona varios métodos para esta tarea.
Crear un solo registro
public class LibroService
{
private readonly BibliotecaContext _context;
public LibroService(BibliotecaContext context)
{
_context = context;
}
// Crear un libro individual
public async Task<Libro> CrearLibroAsync(Libro libro)
{
try
{
// Validar que el libro no sea nulo
if (libro == null)
throw new ArgumentNullException(nameof(libro));
// Agregar el libro al contexto
_context.Libros.Add(libro);
// Guardar cambios en la base de datos
await _context.SaveChangesAsync();
Console.WriteLine($"Libro creado con ID: {libro.Id}");
return libro;
}
catch (Exception ex)
{
Console.WriteLine($"Error al crear libro: {ex.Message}");
throw;
}
}
// Método de conveniencia para crear libros rápidamente
public async Task<Libro> CrearLibroRapidoAsync(string titulo, string autor, int paginas, decimal precio)
{
var libro = new Libro
{
Titulo = titulo,
Autor = autor,
Paginas = paginas,
FechaPublicacion = DateTime.Now,
Precio = precio,
Disponible = true
};
return await CrearLibroAsync(libro);
}
}
Crear múltiples registros
// Crear múltiples libros de una vez
public async Task<List<Libro>> CrearLibrosAsync(List<Libro> libros)
{
try
{
if (libros == null || !libros.Any())
throw new ArgumentException("La lista de libros no puede estar vacía");
// Agregar todos los libros al contexto
_context.Libros.AddRange(libros);
// Guardar todos los cambios de una vez
await _context.SaveChangesAsync();
Console.WriteLine($"Se crearon {libros.Count} libros exitosamente");
// Mostrar los IDs generados
foreach (var libro in libros)
{
Console.WriteLine($"- {libro.Titulo}: ID {libro.Id}");
}
return libros;
}
catch (Exception ex)
{
Console.WriteLine($"Error al crear libros: {ex.Message}");
throw;
}
}
// Ejemplo de uso para crear varios libros
public async Task EjemploCrearVariosLibros()
{
var librosNuevos = new List<Libro>
{
new Libro
{
Titulo = "El nombre del viento",
Autor = "Patrick Rothfuss",
Paginas = 662,
FechaPublicacion = new DateTime(2007, 3, 27),
Precio = 24.95m
},
new Libro
{
Titulo = "Crónica del asesino de reyes",
Autor = "Patrick Rothfuss",
Paginas = 994,
FechaPublicacion = new DateTime(2011, 3, 1),
Precio = 26.95m
},
new Libro
{
Titulo = "El hobbit",
Autor = "J.R.R. Tolkien",
Paginas = 310,
FechaPublicacion = new DateTime(1937, 9, 21),
Precio = 19.99m
}
};
await CrearLibrosAsync(librosNuevos);
}
Operación Read: Consultando datos
La operación Read es probablemente la más utilizada y versátil. EF Core ofrece múltiples formas de consultar datos.
Consultas básicas
// Obtener todos los libros
public async Task<List<Libro>> ObtenerTodosLosLibrosAsync()
{
return await _context.Libros.ToListAsync();
}
// Buscar libro por ID
public async Task<Libro> BuscarLibroPorIdAsync(int id)
{
// Find es optimizado para búsquedas por clave primaria
var libro = await _context.Libros.FindAsync(id);
if (libro == null)
{
Console.WriteLine($"No se encontró libro con ID: {id}");
}
return libro;
}
// Buscar libro por título
public async Task<Libro> BuscarLibroPorTituloAsync(string titulo)
{
return await _context.Libros
.FirstOrDefaultAsync(l => l.Titulo.ToLower() == titulo.ToLower());
}
// Obtener libros con filtros
public async Task<List<Libro>> BuscarLibrosDisponiblesAsync()
{
return await _context.Libros
.Where(l => l.Disponible)
.OrderBy(l => l.Titulo)
.ToListAsync();
}
Consultas avanzadas con LINQ
// Búsqueda con múltiples criterios
public async Task<List<Libro>> BuscarLibrosAvanzadoAsync(
string autor = null,
int? paginasMinimas = null,
decimal? precioMaximo = null)
{
var query = _context.Libros.AsQueryable();
// Aplicar filtros dinámicamente
if (!string.IsNullOrWhiteSpace(autor))
{
query = query.Where(l => l.Autor.Contains(autor));
}
if (paginasMinimas.HasValue)
{
query = query.Where(l => l.Paginas >= paginasMinimas.Value);
}
if (precioMaximo.HasValue)
{
query = query.Where(l => l.Precio <= precioMaximo.Value);
}
return await query
.OrderBy(l => l.Autor)
.ThenBy(l => l.Titulo)
.ToListAsync();
}
// Consultas estadísticas
public async Task<dynamic> ObtenerEstadisticasLibrosAsync()
{
var estadisticas = await _context.Libros
.GroupBy(l => 1) // Agrupar todos los registros
.Select(g => new
{
TotalLibros = g.Count(),
LibrosDisponibles = g.Count(l => l.Disponible),
PrecioPromedio = g.Average(l => l.Precio),
PrecioMinimo = g.Min(l => l.Precio),
PrecioMaximo = g.Max(l => l.Precio),
PaginasTotal = g.Sum(l => l.Paginas)
})
.FirstOrDefaultAsync();
return estadisticas;
}
// Paginación de resultados
public async Task<(List<Libro> libros, int totalPaginas)> ObtenerLibrosPaginadosAsync(
int pagina = 1,
int tamanoPagina = 10)
{
var totalLibros = await _context.Libros.CountAsync();
var totalPaginas = (int)Math.Ceiling((double)totalLibros / tamanoPagina);
var libros = await _context.Libros
.OrderBy(l => l.Titulo)
.Skip((pagina - 1) * tamanoPagina)
.Take(tamanoPagina)
.ToListAsync();
return (libros, totalPaginas);
}
Operación Update: Actualizando registros
La operación Update nos permite modificar registros existentes. EF Core detecta automáticamente los cambios en las entidades rastreadas.
Actualización básica
// Actualizar un libro existente
public async Task<bool> ActualizarLibroAsync(int id, Libro libroActualizado)
{
try
{
// Buscar el libro existente
var libroExistente = await _context.Libros.FindAsync(id);
if (libroExistente == null)
{
Console.WriteLine($"No se encontró libro con ID: {id}");
return false;
}
// Actualizar propiedades
libroExistente.Titulo = libroActualizado.Titulo;
libroExistente.Autor = libroActualizado.Autor;
libroExistente.Paginas = libroActualizado.Paginas;
libroExistente.FechaPublicacion = libroActualizado.FechaPublicacion;
libroExistente.Precio = libroActualizado.Precio;
libroExistente.Disponible = libroActualizado.Disponible;
// Guardar cambios
await _context.SaveChangesAsync();
Console.WriteLine($"Libro con ID {id} actualizado exitosamente");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Error al actualizar libro: {ex.Message}");
return false;
}
}
// Actualización parcial de propiedades específicas
public async Task<bool> ActualizarPrecioLibroAsync(int id, decimal nuevoPrecio)
{
var libro = await _context.Libros.FindAsync(id);
if (libro == null)
return false;
libro.Precio = nuevoPrecio;
await _context.SaveChangesAsync();
Console.WriteLine($"Precio del libro '{libro.Titulo}' actualizado a ${nuevoPrecio:F2}");
return true;
}
// Cambiar disponibilidad de un libro
public async Task<bool> CambiarDisponibilidadLibroAsync(int id, bool disponible)
{
var libro = await _context.Libros.FindAsync(id);
if (libro == null)
return false;
libro.Disponible = disponible;
await _context.SaveChangesAsync();
string estado = disponible ? "disponible" : "no disponible";
Console.WriteLine($"Libro '{libro.Titulo}' marcado como {estado}");
return true;
}
Actualizaciones masivas
// Actualizar múltiples libros con un criterio
public async Task<int> ActualizarPreciosAutorAsync(string autor, decimal factorAumento)
{
// Obtener libros del autor especificado
var librosAutor = await _context.Libros
.Where(l => l.Autor.ToLower() == autor.ToLower())
.ToListAsync();
if (!librosAutor.Any())
{
Console.WriteLine($"No se encontraron libros del autor: {autor}");
return 0;
}
// Aplicar el aumento de precio
foreach (var libro in librosAutor)
{
libro.Precio = libro.Precio * factorAumento;
}
await _context.SaveChangesAsync();
Console.WriteLine($"Precios actualizados para {librosAutor.Count} libros de {autor}");
return librosAutor.Count;
}
// Marcar libros como no disponibles basado en fecha
public async Task<int> MarcarLibrosAntiguosComoNoDisponiblesAsync(DateTime fechaLimite)
{
var librosAntiguos = await _context.Libros
.Where(l => l.FechaPublicacion < fechaLimite && l.Disponible)
.ToListAsync();
foreach (var libro in librosAntiguos)
{
libro.Disponible = false;
}
await _context.SaveChangesAsync();
Console.WriteLine($"Se marcaron {librosAntiguos.Count} libros como no disponibles");
return librosAntiguos.Count;
}
Operación Delete: Eliminando registros
La operación Delete nos permite remover registros de la base de datos. Debemos ser cuidadosos con esta operación ya que es irreversible.
Eliminación básica
// Eliminar un libro por ID
public async Task<bool> EliminarLibroAsync(int id)
{
try
{
var libro = await _context.Libros.FindAsync(id);
if (libro == null)
{
Console.WriteLine($"No se encontró libro con ID: {id}");
return false;
}
// Confirmar eliminación
Console.WriteLine($"¿Está seguro de eliminar '{libro.Titulo}' de {libro.Autor}?");
Console.Write("Escriba 'SI' para confirmar: ");
string confirmacion = Console.ReadLine();
if (confirmacion?.ToUpper() != "SI")
{
Console.WriteLine("Eliminación cancelada");
return false;
}
// Eliminar el libro
_context.Libros.Remove(libro);
await _context.SaveChangesAsync();
Console.WriteLine($"Libro '{libro.Titulo}' eliminado exitosamente");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Error al eliminar libro: {ex.Message}");
return false;
}
}
// Eliminación suave (marcar como eliminado sin borrar)
public async Task<bool> EliminarSuaveLibroAsync(int id)
{
var libro = await _context.Libros.FindAsync(id);
if (libro == null)
return false;
// En lugar de eliminar, marcamos como no disponible
libro.Disponible = false;
await _context.SaveChangesAsync();
Console.WriteLine($"Libro '{libro.Titulo}' marcado como eliminado (eliminación suave)");
return true;
}
Eliminación masiva
// Eliminar libros por criterio
public async Task<int> EliminarLibrosPorAutorAsync(string autor)
{
var librosAutor = await _context.Libros
.Where(l => l.Autor.ToLower() == autor.ToLower())
.ToListAsync();
if (!librosAutor.Any())
{
Console.WriteLine($"No se encontraron libros del autor: {autor}");
return 0;
}
Console.WriteLine($"Se eliminarán {librosAutor.Count} libros de {autor}:");
foreach (var libro in librosAutor)
{
Console.WriteLine($"- {libro.Titulo} ({libro.FechaPublicacion.Year})");
}
Console.Write("¿Continuar con la eliminación? (SI/NO): ");
string confirmacion = Console.ReadLine();
if (confirmacion?.ToUpper() != "SI")
{
Console.WriteLine("Eliminación cancelada");
return 0;
}
_context.Libros.RemoveRange(librosAutor);
await _context.SaveChangesAsync();
Console.WriteLine($"Se eliminaron {librosAutor.Count} libros exitosamente");
return librosAutor.Count;
}
// Limpiar libros no disponibles
public async Task<int> LimpiarLibrosNoDisponiblesAsync()
{
var librosNoDisponibles = await _context.Libros
.Where(l => !l.Disponible)
.ToListAsync();
if (!librosNoDisponibles.Any())
{
Console.WriteLine("No hay libros no disponibles para eliminar");
return 0;
}
_context.Libros.RemoveRange(librosNoDisponibles);
await _context.SaveChangesAsync();
Console.WriteLine($"Se eliminaron {librosNoDisponibles.Count} libros no disponibles");
return librosNoDisponibles.Count;
}
Ejemplo práctico completo
Vamos a crear una aplicación de consola que demuestre todas las operaciones CRUD:
using Microsoft.EntityFrameworkCore;
using BibliotecaApp.Data;
using BibliotecaApp.Models;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("=== Operaciones CRUD con Entity Framework Core ===\n");
// Configurar contexto
var optionsBuilder = new DbContextOptionsBuilder<BibliotecaContext>();
optionsBuilder.UseSqlite("Data Source=biblioteca_crud.db");
using var context = new BibliotecaContext(optionsBuilder.Options);
await context.Database.EnsureCreatedAsync();
var servicio = new LibroService(context);
try
{
// Demostración de operaciones CRUD
await DemostrarOperacionesCrud(servicio);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
Console.WriteLine("\nPresiona cualquier tecla para salir...");
Console.ReadKey();
}
static async Task DemostrarOperacionesCrud(LibroService servicio)
{
Console.WriteLine("1. === OPERACIÓN CREATE ===");
// Crear libros individuales
var libro1 = await servicio.CrearLibroRapidoAsync(
"Don Quijote de la Mancha",
"Miguel de Cervantes",
863,
25.99m);
var libro2 = await servicio.CrearLibroRapidoAsync(
"Cien años de soledad",
"Gabriel García Márquez",
417,
22.50m);
// Crear múltiples libros
await servicio.EjemploCrearVariosLibros();
Console.WriteLine("\n2. === OPERACIÓN READ ===");
// Leer todos los libros
var todosLosLibros = await servicio.ObtenerTodosLosLibrosAsync();
Console.WriteLine($"Total de libros en la base de datos: {todosLosLibros.Count}");
// Buscar por ID
var libroBuscado = await servicio.BuscarLibroPorIdAsync(1);
if (libroBuscado != null)
{
Console.WriteLine($"Libro encontrado: {libroBuscado.Titulo} por {libroBuscado.Autor}");
}
// Búsqueda avanzada
var librosCaros = await servicio.BuscarLibrosAvanzadoAsync(precioMaximo: 25m);
Console.WriteLine($"Libros con precio <= $25: {librosCaros.Count}");
// Estadísticas
var estadisticas = await servicio.ObtenerEstadisticasLibrosAsync();
if (estadisticas != null)
{
Console.WriteLine($"Precio promedio: ${estadisticas.PrecioPromedio:F2}");
Console.WriteLine($"Total de páginas: {estadisticas.PaginasTotal}");
}
Console.WriteLine("\n3. === OPERACIÓN UPDATE ===");
// Actualizar precio
await servicio.ActualizarPrecioLibroAsync(1, 28.99m);
// Actualización masiva
await servicio.ActualizarPreciosAutorAsync("Gabriel García Márquez", 1.1m);
Console.WriteLine("\n4. === OPERACIÓN DELETE ===");
// Para fines de demostración, crear un libro temporal para eliminar
var libroTemporal = await servicio.CrearLibroRapidoAsync(
"Libro Temporal",
"Autor Temporal",
100,
15.00m);
Console.WriteLine($"Libro temporal creado con ID: {libroTemporal.Id}");
// Eliminar el libro temporal (nota: esto requeriría confirmación del usuario)
// await servicio.EliminarLibroAsync(libroTemporal.Id);
// En su lugar, hacemos eliminación suave
await servicio.EliminarSuaveLibroAsync(libroTemporal.Id);
// Mostrar estado final
Console.WriteLine("\n=== ESTADO FINAL ===");
var librosFinales = await servicio.ObtenerTodosLosLibrosAsync();
var librosDisponibles = librosFinales.Count(l => l.Disponible);
Console.WriteLine($"Total de libros: {librosFinales.Count}");
Console.WriteLine($"Libros disponibles: {librosDisponibles}");
Console.WriteLine($"Libros no disponibles: {librosFinales.Count - librosDisponibles}");
}
}
Buenas prácticas y consideraciones
Manejo de errores y validaciones
// Validación antes de operaciones
public async Task<bool> ValidarLibroAsync(Libro libro)
{
var errores = new List<string>();
if (string.IsNullOrWhiteSpace(libro.Titulo))
errores.Add("El título es obligatorio");
if (string.IsNullOrWhiteSpace(libro.Autor))
errores.Add("El autor es obligatorio");
if (libro.Paginas <= 0)
errores.Add("El número de páginas debe ser mayor a cero");
if (libro.Precio < 0)
errores.Add("El precio no puede ser negativo");
// Verificar duplicados
var existe = await _context.Libros
.AnyAsync(l => l.Titulo.ToLower() == libro.Titulo.ToLower() &&
l.Autor.ToLower() == libro.Autor.ToLower());
if (existe)
errores.Add("Ya existe un libro con el mismo título y autor");
if (errores.Any())
{
Console.WriteLine("Errores de validación:");
foreach (var error in errores)
{
Console.WriteLine($"- {error}");
}
return false;
}
return true;
}
Optimización de rendimiento
// Usar AsNoTracking para consultas de solo lectura
public async Task<List<Libro>> ObtenerLibrosSoloLecturaAsync()
{
return await _context.Libros
.AsNoTracking() // No rastrea cambios, mejora rendimiento
.ToListAsync();
}
// Cargar solo las propiedades necesarias
public async Task<List<object>> ObtenerResumenLibrosAsync()
{
return await _context.Libros
.Select(l => new
{
l.Id,
l.Titulo,
l.Autor,
l.Precio
})
.ToListAsync();
}
Resumen
Las operaciones CRUD son la base fundamental para trabajar con datos en cualquier aplicación. En este artículo hemos explorado cómo implementar cada operación utilizando Entity Framework Core: Create para agregar nuevos registros con Add()
y AddRange()
, Read para consultar datos con métodos como Find()
, Where()
y LINQ, Update para modificar registros existentes aprovechando el seguimiento automático de cambios, y Delete para eliminar datos con Remove()
y RemoveRange()
.
Hemos visto técnicas avanzadas como consultas dinámicas, paginación, actualizaciones masivas y eliminación suave, así como buenas prácticas para validación, manejo de errores y optimización de rendimiento. Estas operaciones forman el núcleo de la mayoría de aplicaciones empresariales y su dominio es esencial para cualquier desarrollador que trabaje con bases de datos. En el siguiente artículo profundizaremos en LINQ para consultas de bases de datos, lo que nos permitirá crear consultas más sofisticadas y expresivas.