{"id":308,"date":"2026-05-12T08:01:39","date_gmt":"2026-05-12T12:01:39","guid":{"rendered":"https:\/\/juredev.com\/blog\/?p=308"},"modified":"2026-05-12T08:03:18","modified_gmt":"2026-05-12T12:03:18","slug":"doctrine-orm-no-es-para-todo-dbal-y-el-patron-de-repositorio-hibrido","status":"publish","type":"post","link":"https:\/\/juredev.com\/blog\/2026\/05\/doctrine-orm-no-es-para-todo-dbal-y-el-patron-de-repositorio-hibrido\/","title":{"rendered":"Doctrine ORM no es para todo: DBAL y el patr\u00f3n de repositorio h\u00edbrido"},"content":{"rendered":"\n<p>En los art\u00edculos anteriores vimos por qu\u00e9 el ORM consume tanta memoria y c\u00f3mo mitigarlo con <code>flush()<\/code> por bloques, <code>clear()<\/code> y <code>toIterable()<\/code>. Si llegaste hasta aqu\u00ed, ya est\u00e1s convencido del problema.<\/p>\n\n\n\n<p>Este art\u00edculo es el siguiente paso: c\u00f3mo salir del ORM conscientemente cuando no lo necesitas, sin abandonar el ecosistema Doctrine ni romper la arquitectura del proyecto.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">El punto de partida: ya conoces el costo<\/h2>\n\n\n\n<p>No vamos a repetir por qu\u00e9 <code>findAll()<\/code> puede destruir tu memoria o por qu\u00e9 <code>flush()<\/code> con 50.000 entidades tarda siglos. Eso ya qued\u00f3 claro.<\/p>\n\n\n\n<p>La pregunta ahora es concreta:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Tengo una consulta que solo lee datos. No voy a modificar nada. No necesito tracking ni relaciones. \u00bfQu\u00e9 hago?<\/p>\n<\/blockquote>\n\n\n\n<p>La respuesta es <strong>DBAL<\/strong>, y en este art\u00edculo vas a ver exactamente c\u00f3mo usarlo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Qu\u00e9 es DBAL y qu\u00e9 no es<\/h2>\n\n\n\n<p>Doctrine <strong>DBAL<\/strong> (Database Abstraction Layer) es la capa que vive por debajo del ORM. Doctrine ORM la usa internamente para ejecutar SQL, pero t\u00fa tambi\u00e9n puedes acceder directamente a ella.<\/p>\n\n\n\n<p>Lo que DBAL te da:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Conexi\u00f3n a la base de datos con manejo de errores.<\/li>\n\n\n\n<li>Prepared statements y parameter binding seguro.<\/li>\n\n\n\n<li>Soporte para transacciones.<\/li>\n\n\n\n<li>Portabilidad entre motores (MySQL, PostgreSQL, SQLite).<\/li>\n\n\n\n<li>Integraci\u00f3n nativa con Symfony.<\/li>\n<\/ul>\n\n\n\n<p>Lo que DBAL <strong>no hace<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>No hidrata entidades.<\/li>\n\n\n\n<li>No mantiene Identity Map.<\/li>\n\n\n\n<li>No genera snapshots.<\/li>\n\n\n\n<li>No rastrea cambios.<\/li>\n\n\n\n<li>No gestiona relaciones.<\/li>\n<\/ul>\n\n\n\n<p>En otras palabras: <strong>ejecuta SQL y te devuelve arrays<\/strong>. Sin intermediarios.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Acceder a DBAL desde el EntityManager<\/h2>\n\n\n\n<p>Si ya tienes Doctrine ORM en tu proyecto, DBAL est\u00e1 incluido. No necesitas instalar nada extra:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Desde el EntityManager (cualquier contexto)\n$conn = $entityManager->getConnection();\n\n\/\/ En Symfony, tambi\u00e9n puedes inyectarlo directamente\nuse Doctrine\\DBAL\\Connection;\n\nclass UserReadRepository\n{\n    public function __construct(private readonly Connection $connection) {}\n}<\/code><\/pre>\n\n\n\n<p>Inyectar <code>Connection<\/code> directamente es preferible en repositorios de lectura: deja claro en la firma que esta clase no usa el ORM.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tu primera consulta con DBAL<\/h2>\n\n\n\n<p>Comparemos el mismo caso con ORM y con DBAL.<\/p>\n\n\n\n<p><strong>Con ORM<\/strong> (para una tabla simple de usuarios):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Hidrata entidades completas, snapshots, Identity Map...\n$users = $this->entityManager\n    ->getRepository(User::class)\n    ->findAll();<\/code><\/pre>\n\n\n\n<p><strong>Con DBAL<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$users = $this->connection->fetchAllAssociative('\n    SELECT id, name, email\n    FROM users\n    WHERE active = :active\n    ORDER BY name ASC\n', &#91;'active' => true]);\n\n\/\/ Resultado: array de arrays asociativos\n\/\/ &#91;\n\/\/   &#91;'id' => 1, 'name' => 'Ana Garc\u00eda', 'email' => 'ana@example.com'],\n\/\/   &#91;'id' => 2, 'name' => 'Luis Mart\u00ednez', 'email' => 'luis@example.com'],\n\/\/ ]<\/code><\/pre>\n\n\n\n<p>Sin entidades, sin proxies, sin UnitOfWork. Solo los datos que pediste.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Los m\u00e9todos m\u00e1s \u00fatiles de DBAL<\/h2>\n\n\n\n<p>DBAL ofrece varios m\u00e9todos seg\u00fan lo que necesites:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ M\u00faltiples filas \u2192 array de arrays asociativos\n$rows = $conn->fetchAllAssociative($sql, $params);\n\n\/\/ Una sola fila \u2192 array asociativo (o false si no existe)\n$row = $conn->fetchAssociative($sql, $params);\n\n\/\/ Una sola columna de m\u00faltiples filas \u2192 array plano\n$ids = $conn->fetchFirstColumn('SELECT id FROM users WHERE active = 1');\n\n\/\/ Un \u00fanico valor escalar\n$count = $conn->fetchOne('SELECT COUNT(*) FROM users');\n\n\/\/ Para INSERT, UPDATE, DELETE \u2192 devuelve filas afectadas\n$affected = $conn->executeStatement(\n    'UPDATE users SET active = :active WHERE id = :id',\n    &#91;'active' => false, 'id' => 42]\n);<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">DTOs: del array al objeto sin ORM<\/h2>\n\n\n\n<p>Los arrays son c\u00f3modos para casos simples, pero en una aplicaci\u00f3n real querr\u00e1s objetos tipados. Aqu\u00ed entran los <strong>DTOs<\/strong> (Data Transfer Objects).<\/p>\n\n\n\n<p>Un DTO para lecturas de usuario:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>final class UserSummaryDTO\n{\n    public function __construct(\n        public readonly int $id,\n        public readonly string $name,\n        public readonly string $email,\n    ) {}\n\n    public static function fromRow(array $row): self\n    {\n        return new self(\n            id: (int) $row&#91;'id'],\n            name: $row&#91;'name'],\n            email: $row&#91;'email'],\n        );\n    }\n}<\/code><\/pre>\n\n\n\n<p>Y en el repositorio:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public function findActiveSummaries(): array\n{\n    $rows = $this->connection->fetchAllAssociative('\n        SELECT id, name, email\n        FROM users\n        WHERE active = 1\n        ORDER BY name ASC\n    ');\n\n    return array_map(UserSummaryDTO::fromRow(...), $rows);\n}<\/code><\/pre>\n\n\n\n<p>El resultado es un array de objetos tipados, sin hydration de Doctrine, sin tracking, con autocompletado en tu IDE y sin sorpresas de memoria.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Consultas m\u00e1s complejas: donde DBAL realmente brilla<\/h2>\n\n\n\n<p>El ORM empieza a crujir cuando las consultas se complican. DBAL, en cambio, escala con naturalidad:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public function getOrderStats(\\DateTimeImmutable $from, \\DateTimeImmutable $to): array\n{\n    return $this->connection->fetchAllAssociative('\n        SELECT\n            DATE(o.created_at)      AS day,\n            COUNT(o.id)             AS total_orders,\n            SUM(o.total)            AS revenue,\n            AVG(o.total)            AS avg_order_value,\n            COUNT(DISTINCT o.user_id) AS unique_customers\n        FROM orders o\n        WHERE o.created_at BETWEEN :from AND :to\n          AND o.status = :status\n        GROUP BY DATE(o.created_at)\n        ORDER BY day DESC\n    ', &#91;\n        'from'   => $from->format('Y-m-d'),\n        'to'     => $to->format('Y-m-d'),\n        'status' => 'completed',\n    ]);\n}<\/code><\/pre>\n\n\n\n<p>Intentar expresar esto con DQL y entidades no solo ser\u00eda m\u00e1s verboso: ser\u00eda m\u00e1s lento, m\u00e1s dif\u00edcil de optimizar y m\u00e1s dif\u00edcil de leer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">El patr\u00f3n de repositorio h\u00edbrido<\/h2>\n\n\n\n<p>Aqu\u00ed est\u00e1 el n\u00facleo arquitect\u00f3nico de este art\u00edculo.<\/p>\n\n\n\n<p>La idea es simple: <strong>un repositorio de escritura con ORM, un repositorio de lectura con DBAL<\/strong>. Los dos coexisten en el mismo proyecto, cada uno especializado en lo que hace bien.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Repositorio de escritura (ORM)<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>final class UserRepository\n{\n    public function __construct(\n        private readonly EntityManagerInterface $entityManager,\n    ) {}\n\n    public function save(User $user): void\n    {\n        $this->entityManager->persist($user);\n        $this->entityManager->flush();\n    }\n\n    public function findById(int $id): ?User\n    {\n        return $this->entityManager->find(User::class, $id);\n    }\n\n    public function remove(User $user): void\n    {\n        $this->entityManager->remove($user);\n        $this->entityManager->flush();\n    }\n}<\/code><\/pre>\n\n\n\n<p>Este repositorio trabaja con entidades reales. Tiene tracking, cascadas, eventos del ciclo de vida. Aqu\u00ed el ORM aporta su m\u00e1ximo valor.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Repositorio de lectura (DBAL)<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>final class UserReadRepository\n{\n    public function __construct(\n        private readonly Connection $connection,\n    ) {}\n\n    public function findActiveSummaries(): array\n    {\n        $rows = $this->connection->fetchAllAssociative('\n            SELECT id, name, email\n            FROM users\n            WHERE active = 1\n            ORDER BY name ASC\n        ');\n\n        return array_map(UserSummaryDTO::fromRow(...), $rows);\n    }\n\n    public function findWithOrderStats(int $userId): ?UserStatsDTO\n    {\n        $row = $this->connection->fetchAssociative('\n            SELECT\n                u.id,\n                u.name,\n                u.email,\n                COUNT(o.id)  AS total_orders,\n                SUM(o.total) AS total_spent\n            FROM users u\n            LEFT JOIN orders o ON o.user_id = u.id\n            WHERE u.id = :id\n            GROUP BY u.id\n        ', &#91;'id' => $userId]);\n\n        return $row ? UserStatsDTO::fromRow($row) : null;\n    }\n\n    public function countActiveByCountry(): array\n    {\n        return $this->connection->fetchAllAssociative('\n            SELECT country, COUNT(*) AS total\n            FROM users\n            WHERE active = 1\n            GROUP BY country\n            ORDER BY total DESC\n        ');\n    }\n}<\/code><\/pre>\n\n\n\n<p>Este repositorio ni siquiera conoce que existe una entidad <code>User<\/code>. Solo sabe hacer SQL eficiente y devolver datos.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">C\u00f3mo conviven en un servicio<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>final class UserService\n{\n    public function __construct(\n        private readonly UserRepository $users,          \/\/ ORM: writes\n        private readonly UserReadRepository $userReads,  \/\/ DBAL: reads\n    ) {}\n\n    \/\/ Escritura \u2192 ORM\n    public function register(string $name, string $email): void\n    {\n        $user = new User($name, $email);\n        $this->users->save($user);\n    }\n\n    \/\/ Lectura simple \u2192 DBAL\n    public function getActiveSummaries(): array\n    {\n        return $this->userReads->findActiveSummaries();\n    }\n\n    \/\/ Lectura con l\u00f3gica de negocio \u2192 ORM\n    public function deactivate(int $userId): void\n    {\n        $user = $this->users->findById($userId);\n        $user->deactivate(); \/\/ l\u00f3gica de dominio en la entidad\n        $this->users->save($user);\n    }\n}<\/code><\/pre>\n\n\n\n<p>La regla es clara: si la operaci\u00f3n <strong>modifica estado o necesita l\u00f3gica de dominio<\/strong>, usa el repositorio ORM. Si solo <strong>lee datos para mostrarlos<\/strong>, usa el repositorio DBAL.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">QueryBuilder de DBAL: SQL din\u00e1mico sin concatenar strings<\/h2>\n\n\n\n<p>Cuando los filtros son din\u00e1micos (b\u00fasquedas, paginaci\u00f3n, ordenamiento variable), DBAL ofrece un QueryBuilder propio, m\u00e1s ligero que el del ORM:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public function search(UserSearchCriteria $criteria): array\n{\n    $qb = $this->connection->createQueryBuilder()\n        ->select('id', 'name', 'email', 'created_at')\n        ->from('users')\n        ->where('active = :active')\n        ->setParameter('active', true)\n        ->orderBy('name', 'ASC')\n        ->setMaxResults($criteria->limit)\n        ->setFirstResult($criteria->offset);\n\n    if ($criteria->country !== null) {\n        $qb->andWhere('country = :country')\n           ->setParameter('country', $criteria->country);\n    }\n\n    if ($criteria->search !== null) {\n        $qb->andWhere('name LIKE :search OR email LIKE :search')\n           ->setParameter('search', '%' . $criteria->search . '%');\n    }\n\n    return array_map(\n        UserSummaryDTO::fromRow(...),\n        $qb->fetchAllAssociative()\n    );\n}<\/code><\/pre>\n\n\n\n<p>Sin concatenaci\u00f3n de strings, sin riesgo de inyecci\u00f3n SQL, sin el overhead del ORM.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Transacciones con DBAL<\/h2>\n\n\n\n<p>DBAL tambi\u00e9n maneja transacciones, algo importante cuando necesitas atomicidad en operaciones de escritura que no pasan por el ORM:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public function transferCredits(int $fromId, int $toId, int $amount): void\n{\n    $this->connection->transactional(function (Connection $conn) use ($fromId, $toId, $amount): void {\n        $conn->executeStatement(\n            'UPDATE accounts SET credits = credits - :amount WHERE id = :id',\n            &#91;'amount' => $amount, 'id' => $fromId]\n        );\n\n        $conn->executeStatement(\n            'UPDATE accounts SET credits = credits + :amount WHERE id = :id',\n            &#91;'amount' => $amount, 'id' => $toId]\n        );\n    });\n}<\/code><\/pre>\n\n\n\n<p>Si cualquiera de las dos operaciones falla, la transacci\u00f3n se revierte autom\u00e1ticamente.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Cu\u00e1ndo usar cada herramienta: la gu\u00eda r\u00e1pida<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Situaci\u00f3n<\/th><th>Herramienta<\/th><\/tr><tr><td>Crear, modificar o eliminar entidades<\/td><td>ORM<\/td><\/tr><tr><td>L\u00f3gica de dominio (invariantes, reglas de negocio)<\/td><td>ORM<\/td><\/tr><tr><td>Relaciones complejas con cascadas<\/td><td>ORM<\/td><\/tr><tr><td>Leer datos para una API o listado<\/td><td>DBAL<\/td><\/tr><tr><td>Dashboards, reportes, analytics<\/td><td>DBAL<\/td><\/tr><tr><td>Aggregaciones (COUNT, SUM, AVG)<\/td><td>DBAL<\/td><\/tr><tr><td>Exports masivos<\/td><td>DBAL + toIterable()<\/td><\/tr><tr><td>Consultas con m\u00faltiples JOINs sin l\u00f3gica de dominio<\/td><td>DBAL<\/td><\/tr><tr><td>ETL o procesamiento batch de lectura<\/td><td>DBAL<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Lo que ganamos con esta separaci\u00f3n<\/h2>\n\n\n\n<p>Para entender el impacto real, veamos qu\u00e9 ocurre al consultar 10.000 usuarios con tres columnas simples (<code>id<\/code>, <code>name<\/code>, <code>email<\/code>) en una tabla sin relaciones. Los n\u00fameros son ilustrativos pero representan proporciones reales que puedes reproducir en cualquier proyecto mediano:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>M\u00e9trica<\/th><th>ORM (findAll())<\/th><th>DBAL (fetchAllAssociative)<\/th><th>Diferencia<\/th><\/tr><tr><td>Memoria pico<\/td><td>~85 MB<\/td><td>~8 MB<\/td><td>~10\u00d7 menos<\/td><\/tr><tr><td>Tiempo de ejecuci\u00f3n<\/td><td>~620 ms<\/td><td>~55 ms<\/td><td>~11\u00d7 m\u00e1s r\u00e1pido<\/td><\/tr><tr><td>Objetos en memoria<\/td><td>~10.000 entidades + proxies + snapshots<\/td><td>~10.000 arrays simples<\/td><td>Sin overhead del UoW<\/td><\/tr><tr><td>Trabajo del ORM<\/td><td>Hydration, Identity Map, UnitOfWork, snapshots<\/td><td>Ninguno<\/td><td>\u2014<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>La diferencia de memoria se explica por lo que Doctrine construye para cada entidad: la instancia del objeto, una copia snapshot para detectar cambios, la entrada en el Identity Map y, si hay relaciones, proxies adicionales. Con DBAL, una fila es simplemente un array asociativo PHP. Sin m\u00e1s.<\/p>\n\n\n\n<p><strong>El multiplicador empeora con relaciones<\/strong>. Si <code>User<\/code> tuviera una relaci\u00f3n <code>EAGER<\/code> con <code>Address<\/code>, el ORM hidrata tambi\u00e9n 10.000 objetos <code>Address<\/code>. DBAL solo devuelve lo que el <code>SELECT<\/code> pide expl\u00edcitamente.<\/p>\n\n\n\n<p>La diferencia se amplifica en exports y workers. En un proceso que itera 100.000 registros, pasar de ORM a DBAL puede ser la diferencia entre un worker que crashea por memoria y uno que termina sin incidentes.<\/p>\n\n\n\n<p>M\u00e1s all\u00e1 de los n\u00fameros:<\/p>\n\n\n\n<p><strong>Legibilidad<\/strong>: el SQL vive donde tiene sentido y es f\u00e1cil de auditar, optimizar con EXPLAIN, o pasar a un DBA.<\/p>\n\n\n\n<p><strong>Claridad arquitect\u00f3nica<\/strong>: cuando alguien lee <code>UserReadRepository<\/code>, sabe inmediatamente que esa clase nunca va a modificar datos. La intenci\u00f3n est\u00e1 en la firma.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">No es ORM vs SQL, es cada herramienta en su lugar<\/h2>\n\n\n\n<p>Doctrine ORM sigue siendo una herramienta excelente para lo que fue dise\u00f1ada: persistir y recuperar objetos de dominio con estado, relaciones y comportamiento.<\/p>\n\n\n\n<p>El problema nunca fue el ORM. El problema es usarlo para absolutamente todo, incluyendo escenarios donde solo necesitas leer datos y devolverlos.<\/p>\n\n\n\n<p>El patr\u00f3n h\u00edbrido, ORM para escrituras y l\u00f3gica de dominio, DBAL para lecturas, no requiere sobrearquitectura ni reescribir el proyecto. Puedes introducirlo de forma incremental: la pr\u00f3xima vez que tengas una consulta de lectura costosa, extr\u00e1ela a un repositorio DBAL. Mide la diferencia. Y a partir de ah\u00ed decides hasta d\u00f3nde llevar el patr\u00f3n.<\/p>\n\n\n\n<p>Escalar una aplicaci\u00f3n muchas veces no significa cambiar la base de datos ni a\u00f1adir cach\u00e9. Significa dejar de hacer trabajo innecesario antes de llegar a ella.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Preguntas frecuentes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">\u00bfPuedo usar DBAL y ORM en la misma transacci\u00f3n?<\/h3>\n\n\n\n<p>S\u00ed. Comparten la misma conexi\u00f3n subyacente, por lo que operaciones de ORM y DBAL pueden participar en la misma transacci\u00f3n:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$this->entityManager->beginTransaction();\ntry {\n    $this->entityManager->persist($entity);\n    $this->entityManager->flush();\n    $this->connection->executeStatement('UPDATE stats SET total = total + 1');\n    $this->entityManager->commit();\n} catch (\\Throwable $e) {\n    $this->entityManager->rollback();\n    throw $e;\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">\u00bfDBAL protege contra SQL injection?<\/h3>\n\n\n\n<p>S\u00ed, siempre que uses par\u00e1metros con <code>:nombre<\/code> o <code>?<\/code> y no concatenes valores directamente en el SQL. Los m\u00e9todos <code>fetchAllAssociative<\/code>, <code>executeStatement <\/code>y el QueryBuilder de DBAL usan prepared statements internamente.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u00bfNecesito instalar algo extra para DBAL?<\/h3>\n\n\n\n<p>No. Si tienes <code>doctrine\/orm<\/code> en tu proyecto, <code>doctrine\/dbal<\/code> ya est\u00e1 incluido como dependencia. Solo necesitas inyectar <code>Doctrine\\DBAL\\Connection<\/code> en tu clase.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>En los art\u00edculos anteriores vimos por qu\u00e9 el ORM consume tanta memoria y c\u00f3mo mitigarlo con flush() por bloques, clear() y toIterable(). Si llegaste hasta aqu\u00ed, ya est\u00e1s convencido del problema. Este art\u00edculo es el siguiente paso: c\u00f3mo salir del ORM conscientemente cuando no lo necesitas, sin abandonar el ecosistema Doctrine ni romper la arquitectura [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[15,20],"class_list":["post-308","post","type-post","status-publish","format-standard","hentry","category-desarrollo","tag-php","tag-sql"],"_links":{"self":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/308","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/comments?post=308"}],"version-history":[{"count":0,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/308\/revisions"}],"wp:attachment":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/media?parent=308"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/categories?post=308"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/tags?post=308"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}