Curso de Laravel

Eloquent ORM en Laravel: modelos y operaciones básicas

Por Víctor Peña · Publicado el

Hola, ¿cómo están? Continuando con el curso de Laravel, hoy llegamos a Eloquent, el ORM del framework. Es probablemente la funcionalidad que más define la experiencia de trabajar con Laravel.

¡Empecemos!

Qué es un ORM

ORM significa Object-Relational Mapping: un traductor entre las tablas de la base de datos y los objetos de tu código.

Sin ORM escribes SQL:

$sql = "SELECT * FROM pacientes WHERE ciudad = 'Cochabamba' ORDER BY apellido";
$pacientes = $pdo->query($sql)->fetchAll();

Con Eloquent escribes esto:

$pacientes = Paciente::where('ciudad', 'Cochabamba')
    ->orderBy('apellido')
    ->get();

Cada tabla tiene su modelo, y cada fila se convierte en un objeto con el que trabajas como con cualquier otro.

La ventaja no es solo escribir menos: es que las relaciones entre tablas se recorren como propiedades, y el código queda legible para quien no conoce el esquema.

Crear un modelo

php artisan make:model Paciente

Se crea en app/Models/Paciente.php. Y con las banderas que vimos en la lección de Artisan:

php artisan make:model Paciente -mfs   # con migración, factory y seeder
php artisan make:model Paciente --all  # además controlador y requests

Las convenciones

Eloquent funciona por convención. Si sigues las reglas, no configuras nada:

Elemento Convención Ejemplo
Modelo Singular, PascalCase Paciente
Tabla Plural, snake_case pacientes
Clave primaria id id
Clave foránea modelo_id paciente_id
Marcas de tiempo created_at, updated_at automáticas

Un detalle con nombres en español: Laravel pluraliza en inglés, así que a veces falla. Doctor se convierte en doctors, no en doctores. Cuando pase, decláralo:

protected $table = 'doctores';

Es preferible eso a renombrar la tabla para complacer al framework.

Si tu clave primaria no es id:

protected $primaryKey = 'codigo';
public $incrementing = false;   // si no es autoincremental
protected $keyType = 'string';

Y si la tabla no tiene created_at ni updated_at:

public $timestamps = false;

fillable: la protección obligatoria

class Paciente extends Model
{
    protected $fillable = [
        'cedula',
        'nombre',
        'apellido',
        'fecha_nacimiento',
        'telefono',
        'correo',
    ];
}

Sin esta propiedad, Paciente::create($request->all()) lanza un error de asignación masiva.

Por qué existe: ese método intenta asignar todos los campos que llegaron del formulario. Si alguien añade a mano un campo id o rol en la petición, podría modificar datos que no le corresponden. $fillable es la lista blanca de lo que sí se puede asignar en masa.

Existe la alternativa $guarded = [], que permite todo. Evítala: renuncias a la protección por comodidad.

casts: convertir tipos automáticamente

MySQL devuelve casi todo como texto. casts() le dice a Laravel cómo interpretarlo:

protected function casts(): array
{
    return [
        'fecha_nacimiento' => 'date',
        'activo' => 'boolean',
        'costo' => 'decimal:2',
        'preferencias' => 'array',
    ];
}

Sin esto, $paciente->activo devolvería "1" en lugar de true, y las comparaciones estrictas fallarían.

El cast date es especialmente útil: convierte la columna en un objeto de fecha con métodos propios:

$paciente->fecha_nacimiento->format('d/m/Y');
$paciente->fecha_nacimiento->age;           // la edad, calculada
$paciente->created_at->diffForHumans();     // «hace 3 días»

Ese diffForHumans() sale gratis y queda muy bien en cualquier interfaz.

Leer registros

Paciente::all();                    // todos
Paciente::find(1);                  // por id, null si no existe
Paciente::findOrFail(1);            // por id, lanza 404 si no existe
Paciente::first();                  // el primero
Paciente::count();                  // cuántos hay

Paciente::where('ciudad', 'Cochabamba')->get();
Paciente::where('activo', true)->orderBy('apellido')->get();
Paciente::latest()->take(10)->get();

findOrFail() merece atención. Si el registro no existe, lanza una excepción que Laravel convierte automáticamente en una página 404. Es exactamente lo que quieres en un controlador:

public function show(int $id)
{
    $paciente = Paciente::findOrFail($id);
    return view('pacientes.show', compact('paciente'));
}

Sin él tendrías que comprobar si es null y devolver el 404 a mano.

Crear registros

Dos formas.

Con create(), que es la habitual:

$paciente = Paciente::create([
    'cedula' => '1234567',
    'nombre' => 'Ana',
    'apellido' => 'Torres',
    'fecha_nacimiento' => '1990-06-15',
]);

Devuelve el objeto creado, con su id ya asignado.

Instanciando y guardando, útil cuando construyes el objeto por pasos:

$paciente = new Paciente();
$paciente->cedula = '7654321';
$paciente->nombre = 'Luis';
$paciente->save();

También existen dos métodos muy prácticos:

// Busca; si no existe, lo crea
Paciente::firstOrCreate(
    ['cedula' => '1234567'],
    ['nombre' => 'Ana', 'apellido' => 'Torres']
);

// Busca y actualiza; si no existe, lo crea
Paciente::updateOrCreate(
    ['cedula' => '1234567'],
    ['telefono' => '76980507']
);

El primer arreglo es la condición de búsqueda; el segundo, los datos. Son el equivalente al ON DUPLICATE KEY UPDATE de MySQL, y evitan tener que comprobar antes si el registro existe.

Actualizar

$paciente = Paciente::findOrFail(1);
$paciente->telefono = '76980510';
$paciente->save();

// O en una línea
$paciente->update(['telefono' => '76980510']);

// Varios registros a la vez
Cita::where('fecha', '<', now())
    ->where('estado', 'programada')
    ->update(['estado' => 'cancelada']);

Ese último caso es importante: actualiza directamente en la base de datos, sin traer los registros. Con muchas filas, la diferencia de rendimiento es enorme.

Eliminar

$paciente = Paciente::findOrFail(1);
$paciente->delete();

Paciente::destroy(1);
Paciente::destroy([1, 2, 3]);

Cita::where('estado', 'cancelada')->delete();

Borrado lógico

Es el borrado lógico que vimos en MySQL, y Laravel lo trae resuelto. En la migración:

$table->softDeletes();

Y en el modelo:

use Illuminate\Database\Eloquent\SoftDeletes;

class Paciente extends Model
{
    use SoftDeletes;
}

A partir de ahí:

$paciente->delete();          // marca deleted_at, no borra
Paciente::all();              // no incluye los eliminados
Paciente::withTrashed()->get();  // los incluye
Paciente::onlyTrashed()->get();  // solo los eliminados
$paciente->restore();         // lo recupera
$paciente->forceDelete();     // ahora sí lo borra de verdad

En sistemas con historial —médicos, contables, legales— esto no es opcional. Borrar un paciente no debería hacer desaparecer su historia clínica.

Comprobar el estado de un objeto

$paciente->isDirty();          // ¿tiene cambios sin guardar?
$paciente->isDirty('telefono');
$paciente->wasChanged();       // ¿cambió al guardar?
$paciente->getOriginal('telefono');  // el valor antes del cambio

Se usan sobre todo dentro de eventos del modelo, para reaccionar solo cuando algo cambió de verdad.

Errores comunes

  • Olvidar $fillable y encontrarte con el error de asignación masiva.
  • No declarar $table cuando la pluralización en inglés no coincide.
  • Usar find() sin comprobar null, cuando findOrFail() resuelve el caso.
  • Traer registros para actualizarlos uno por uno, en lugar de un update() sobre la consulta.
  • Borrar en lugar de usar borrado lógico cuando hacía falta conservar el historial.
  • $guarded = [] para evitar configurar $fillable.

Para cerrar

Eloquent convierte el trabajo con la base de datos en trabajo con objetos. Con lo de esta lección ya puedes hacer un CRUD completo sobre cualquier tabla.

Lo importante: $fillable siempre, casts() para los tipos, y findOrFail() en los controladores.

En la siguiente lección veremos los seeders, para llenar la base con datos iniciales.

Saludos y éxitos.

Norvic Software

Desarrollamos el software que tu empresa necesita

Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.

Solicitar cotizaciónVer todos los servicios

Cotización sin costo · Respuesta directa por WhatsApp