Muchos-a-Muchos pero con Datos Extra
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLas relaciones ManyToMany son el único lugar de Doctrine donde tenemos una tabla en nuestra base de datos - starship_droid
- pero ninguna entidad correspondiente en nuestra aplicación.
Pero hay un problema: no podemos añadir columnas adicionales a esa tabla de unión. Por ejemplo, ¿qué pasaría si quisiéramos saber cuándo se asigna un droide a una nave estelar? Para ello, nuestra tabla de unión necesitaría una columna assignedAt
. Pero no podemos añadirla
Cuando muchos a muchos no es suficiente
La solución es arremangarnos y empezar a manejar las cosas de forma más manual.
Dejaremos de utilizar la relación muchos-a-muchos por completo. En su lugar, vamos a generar una nueva entidad que represente la tabla de unión. Primero, deshaz la relación muchos-a-muchos (pero preocúpate sólo de las propiedades, no de los métodos). En Starship
, despídete de la propiedaddroids
:
// ... lines 1 - 15 | |
class Starship | |
{ | |
// ... lines 18 - 50 | |
/** | |
* @var Collection<int, Droid> | |
*/ | |
#[ORM\ManyToMany(targetEntity: Droid::class, inversedBy: 'starships')] | |
private Collection $droids; | |
// ... lines 56 - 227 | |
} |
Y en Droid
, haz lo mismo con la propiedad muchos-a-muchos starships
:
// ... lines 1 - 10 | |
class Droid | |
{ | |
// ... lines 13 - 23 | |
/** | |
* @var Collection<int, Starship> | |
*/ | |
#[ORM\ManyToMany(targetEntity: Starship::class, mappedBy: 'droids')] | |
private Collection $starships; | |
// ... lines 29 - 89 | |
} |
Borra el constructor en ambos.
Busca tu terminal y ejecuta:
symfony console doctrine:schema:update --dump-sql
Esto te muestra el aspecto que tendría tu migración si la generaras ahora mismo. Es lo que esperamos: no más tabla starship_droid
.
Crear una nueva entidad de unión
Pero ¡no generes todavía esa migración! Queremos la tabla de unión, pero ahora necesitamos crear una entidad que la represente. Ejecuta::
symfony console make:entity StarshipDroid
DroidAssignment
podría ser un nombre más adecuado, pero StarshipDroid
nos ayuda a visualizar lo que estamos haciendo: recrear exactamente la misma relación de base de datos mediante dos ManyToOne
s
Añade assignedAt
junto con dos propiedades más para crear relaciones desde esta tabla de unión a Starship
y Droid
.
Éstas serán relaciones ManyToOne
, y conectaránStarshipDroid
con Starship
y Droid
.
La migración que no hace nada
Ahora, genera esa migración:
symfony console make:migration
Y compruébalo. Puede parecer que hay muchos cambios, pero fíjate bien: sólo se eliminan las restricciones de clave foránea, se añade una clave primaria y se vuelve a crear la clave foránea:
// ... lines 1 - 12 | |
final class Version20250315183623 extends AbstractMigration | |
{ | |
// ... lines 15 - 19 | |
public function up(Schema $schema): void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->addSql('ALTER TABLE starship_droid DROP CONSTRAINT FK_1C7FBE889B24DF5'); | |
$this->addSql('ALTER TABLE starship_droid DROP CONSTRAINT FK_1C7FBE88AB064EF'); | |
$this->addSql('ALTER TABLE starship_droid DROP CONSTRAINT starship_droid_pkey'); | |
$this->addSql('ALTER TABLE starship_droid ADD id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL'); | |
$this->addSql('ALTER TABLE starship_droid ADD assigned_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL'); | |
$this->addSql('ALTER TABLE starship_droid ADD CONSTRAINT FK_1C7FBE889B24DF5 FOREIGN KEY (starship_id) REFERENCES starship (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | |
$this->addSql('ALTER TABLE starship_droid ADD CONSTRAINT FK_1C7FBE88AB064EF FOREIGN KEY (droid_id) REFERENCES droid (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | |
$this->addSql('ALTER TABLE starship_droid ADD PRIMARY KEY (id)'); | |
} | |
// ... lines 32 - 44 | |
} |
Así que, al final, esta migración no cambia nada real en la base de datos.
Ejecútala con:
symfony console doctrine:migrations:migrate
Y ¡boom!
La columna
assignedAt
no puede contener valoresnull
.
Doctrine está haciendo un berrinche debido a las filas existentes en la tablastarship_droid
. Podemos apaciguarlo con un valor por defecto. Actualiza manualmente la migración a, por ejemplo, DEFAULT NOW() NOT NULL
:
// ... lines 1 - 12 | |
final class Version20250315183623 extends AbstractMigration | |
{ | |
// ... lines 15 - 19 | |
public function up(Schema $schema): void | |
{ | |
// ... lines 22 - 26 | |
$this->addSql('ALTER TABLE starship_droid ADD assigned_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NOW() NOT NULL'); | |
// ... lines 28 - 30 | |
} | |
// ... lines 32 - 44 | |
} |
Los toques finales
Vamos a añadir un toque final a StarshipDroid
:
// ... lines 1 - 7 | |
#[ORM\Entity(repositoryClass: StarshipDroidRepository::class)] | |
class StarshipDroid | |
{ | |
#[ORM\Id] | |
#[ORM\GeneratedValue] | |
#[ORM\Column] | |
private ?int $id = null; | |
#[ORM\Column] | |
private ?\DateTimeImmutable $assignedAt = null; | |
#[ORM\ManyToOne(inversedBy: 'starshipDroids')] | |
#[ORM\JoinColumn(nullable: false)] | |
private ?Droid $droid = null; | |
#[ORM\ManyToOne(inversedBy: 'starshipDroids')] | |
#[ORM\JoinColumn(nullable: false)] | |
private ?Starship $starship = null; | |
// ... lines 26 - 66 | |
} |
Este assignedAt
no es realmente algo de lo que debamos preocuparnos. Crea un constructor y configúralo automáticamente: $this->assignedAt = new \DateTimeImmutable();
:
// ... lines 1 - 8 | |
class StarshipDroid | |
{ | |
// ... lines 11 - 26 | |
public function __construct() | |
{ | |
$this->assignedAt = new \DateTimeImmutable(); | |
} | |
// ... lines 31 - 71 | |
} |
Espera, ¡porque esto es enorme! Ahora tenemos exactamente la misma relación en la base de datos que antes. Pero como hemos tomado el control de la entidad join, podemos añadirle nuevos campos. A continuación, veremos cómo asignar droides a naves estelares con esta nueva configuración de entidades. Y, finalmente, ¡nos pondremos elegantes y ocultaremos por completo este detalle de la implementación!
Hello
When I try to migrate by running symfony console make:migration at the end, I get an error:
In TableAlreadyExists.php line 16:
The table named ".starship_droid" already exists.
However, the table has already been deleted from the database.