Problema con relación oneToOne en symfony 2.8

Hola, tengo un problema con la relación entre entidades a la hora de persistir una de ellas. El problema en cuestión es este: A new entity was found through the relationship...

Las entidades relacionadas son linea_producto 1-1 producto. Lo que hago es sacar mediante id el producto que ya tengo en la base de datos y mediante setProducto se lo "agrego" a la linea_pedido. Pero cuando quiero persistir la linea me tira ese error. Nada me funciona para poder persistir la linea sin que salga ese error, y un cascade "persist" me duplica los productos en la base de datos.

Este es mi controlador:

public function addtoCartAction(Request $request,$id_producto)
    {
        $session = $request->getSession();
 
        $producto = $this->getDoctrine()->getRepository('ProductsBundle:Producto')->find($id_producto);
 
        //compruebo si tiene iniciado un pedido
 
        if($session->has('pedido')){
 
            //tiene un pedido sin finalizar
 
            $linea = new Linea_pedido();
 
            $linea->setProducto($producto);
            $linea->setImporte($producto->getPrecio()*$session->get('cantidad'));
            $linea->setCantidad($session->get('cantidad'));
            $linea->setPedido($session->get('pedido'));
            $session->remove('cantidad');
            $session->get('pedido')->addLineasPedido($linea);
 
        }
        else{
 
            //inicia un pedido
 
            $usuario = $this->getDoctrine()->getRepository('UsersBundle:Usuario')->find($session->get('id_usuario'));
            $pedido = new Pedido();
            $session->set('pedido',$pedido);
            $linea = new Linea_pedido();
 
            $linea->setProducto($producto);
 
            $linea->setImporte($producto->getPrecio_descuento()*$session->get('cantidad'));
            $linea->setCantidad($session->get('cantidad'));
            $linea->setPedido($pedido);
            $em=$this->getDoctrine()->getManager();
 
            $em->merge($linea);
            $session->remove('cantidad');
            $pedido->addLineasPedido($linea);
 
        }
 
        return $this->redirect($this->generateUrl('show_cart',array('username'=>$session->get('username'))));       
        }
 
/******************************   FINISH ORDER  ************************************************************/    
 
    public function finishOrderAction(Request $request)
    {
        $session = $request->getSession();
        $pedido = $session->get('pedido');
        $pedido->setImporte($pedido->calcularImporte());
        $pedido->setFecha(date("d-m-Y H:i"));
        $usuario = $this->getDoctrine()->getRepository('UsersBundle:Usuario')->find($session->get('id_usuario'));
        $pedido->setUsuario($usuario);      
        $usuario->addPedido($pedido);
        $session->remove('pedido');     
    /*  $id=$pedido->getLineasPedido()[0]->getProducto()->getId();
        return $this->render('UsersBundle:Users:pruebas.html.twig',array('prueba' => $id));*/
 
        $em=$this->getDoctrine()->getManager();
 
        /*for($i=0;$i<sizeof($pedido->getLineasPedido());$i++){
        $em->detach($pedido->getLineasPedido()[$i]->getProducto());
        }*/
 
        $em->persist($pedido);
        $em->flush();
        $session->getFlashBag()->add('mensaje_exito', 'Pedido realizado con éxito');
        return $this->redirect($this->generateUrl('show_orders', array('username'=>$session->get('username'))));
 
    }

el problema surge en el finishorderaction al persistir el pedido (el cuál contiene líneas) pero el error me lo da en la relación entre las entidades de linea_pedido y producto.

¿Alguna posible solución?

Respuestas

#1

No tengo mucha experiencia con este error de Doctrine, pero en todas las respuestas relacionadas de Doctrine (como por ejemplo en esta respuesta) sugieren o bien configurar el persist con cascade o bien jugar con el merge() del entity manager.

#2

Hola Javier, gracias por tu respuesta. El cascade persist no me vale porque mi producto ya está en la base de datos. El problema es q no se porque me genera una nueva entidad producto cuando persisto el pedido ya que lo único q hago una vez obtengo el producto es linea-setproducto(producto). He probado con merge en todo, tanto con el producto, con línea pedido y pedido, pero no lo soluciono. Puede q no este utilizando bien el merge. Si pudieses aclararme como debería utilizarlo te lo agradecería. Un saludo

#3

Buen día, creo que el problema es que estas tomando el pedido desde la sesión y no desde el EntityManager de doctrine, para efectos de doctrine ese pedido no está manejado por doctrine, por lo que al hacer persist intentará crear el pedido y/o las entidades relacionadas con el pedido (que tambien se cargaron de la sesión).

La solución a esto es consultar el pedido desde el EntityManager, y además quisas guardar en la sesión solo el id del pedido y no toda la entidad (esto último lo puedes manejar como prefieras).

Así lo hace el componente de seguridad de Symfony, aunque el usuario se guarda en sesión luego de iniciar sesión, todas las consultas posteriores lo primero que hacen es refrescar el usuario, esto es, que lo traen de la base de datos a partir del id del usuario que estaba en la sesión.

Saludos!

#4

Hola, gracias por tu propuesta. Lo que saco de la base de datos es el producto, no el pedido. El pedido lo almaceno en sesión por si el mismo contiene más de una línea. Cuando el pedido es de sólo una línea (un producto) el pedido se crea en el controlador y se persiste, dándome el mismo error.

#5

Puedes mostrar el mensaje completo del error? para entender más a que se puede deber el problema.

Saludos!

#6

Hola de nuevo, este es el error completo que me muestra al persistir el pedido:

A new entity was found through the relationship 'UsersBundle\Entity\Linea_pedido#producto' that was not configured to cascade persist operations for entity: ProductsBundle\Entity\Producto@00000000421f31af0000000014239ee7. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'ProductsBundle\Entity\Producto#__toString()' to get a clue.

El cascade: "persist" no es una solución, ya que el producto se encuentra en la base de datos. Lo he probado poniendo cascade "persist" en la relación de linea_pedido a producto y funciona todo correctamente salvo que persiste de nuevo el producto que contiene la línea con id diferente.

La relación entre linea_pedido y producto es one to one unidireccional:

UsersBundle\Entity\Linea_pedido:
    type: entity
    table: linea_pedido

    manyToOne:
       pedido:
            targetEntity: Pedido
            inversedBy: lineas_pedido
            joinColumn:
                name: pedido_id
                referencedColumnName: id      

    oneToOne:
        producto:
            targetEntity: ProductsBundle\Entity\Producto
            joinColumn:
                name: producto_id
                referencedColumnName: id

Esta es la del pedido:

UsersBundle\Entity\Pedido:
    type: entity
    table: pedido

    manyToOne:
        usuario:
            targetEntity: Usuario
            inversedBy: pedidos
            joinColumn:
                name: usuario_id
                referencedColumnName: id

    oneToMany:
        lineas_pedido:
            targetEntity: Linea_pedido
            mappedBy: pedido
            cascade: ["persist"]

En la relación entre pedido y linea_pedido si que necesito el persist en cascada para que una vez se persista el pedido se persistan con el las lineas que lo contienen. Con este código creo una linea de pedido (newOrderAction).

$session = $request->getSession();
 
$em = $this->getDoctrine()->getManager();
 
$producto = $em->find('ProductsBundle:Producto',$id_producto);
 
$pedido = new Pedido();
 
$linea = new Linea_pedido();
 
$linea->setProducto($producto);     
 
$linea->setImporte($producto->getPrecio_descuento()*$session->get('cantidad'));
 
$linea->setCantidad($producto->getCantidad());
 
$linea->setPedido($pedido);
 
$em=$this->getDoctrine()->getManager();
 
$pedido->addLineasPedido($linea);
 
$session->set('pedido',$pedido); --> Paso el pedido a la sesion por si el usuario quiere incluir otro producto en su pedido

Y con este es con el que finalizo el pedido: (finishOrderAction)

$session = $request->getSession();
 
$pedido = $session->get('pedido');
 
$pedido->setImporte($pedido->calcularImporte());
 
$pedido->setFecha(date("d-m-Y H:i"));
 
$usuario = $this->getDoctrine()->getRepository('UsersBundle:Usuario')->find($session->get('id_usuario'));
 
$pedido->setUsuario($usuario);      
 
$usuario->addPedido($pedido);
 
$session->remove('pedido');       
 
$em=$this->getDoctrine()->getManager();
 
$em->persist($pedido);
 
$em->flush();

Gracias por vuestro tiempo, a ver si puedo solucionar esto y perdón por el post, no sé como hacer para que salga el código más legible

#7

En el código no parece haber ningún error, podrías pasar el código de las entidades de Producto y Linea Producto?

Además te recomiendo que hagas un dump de la entidad producto justo antes de persistir el pedido, para verificar si es el mismo que has consultado desde la base de datos o ha cambiado.

Por último sigo pensando que es conveniente que "refresques" el pedido que tienes en la sesión consultandolo en la base de datos, esto con el fin de que doctrine se traiga las relaciones de dicho pedido desde la base de datos y no desde la sesión.

Saludos!

#8

Hola manuel, haciendo pruebas he comprobado que el producto antes de de persistir el pedido es el que obtuve de la base de datos (he comprobado que los id coinciden). El pedido no lo puedo consultar en la base de datos porque no está en ella, el pedido que paso a la sesión aún no está persistido en la base de datos, simplemente lo paso a la sesión para que el usuario pueda seguir incluyendo artículos en el pedido.

Te dejo las entidades:

namespace UsersBundle\Entity;
 
use Doctrine\ORM\Mapping as ORM;
use ProductsBundle\Entity\Producto;
 
/**
 * Linea_pedido
 */
 
class Linea_pedido
{
    /**
     * @var int
     */
 
    private $id;
 
    /**
     * @var int
     */
 
    private $cantidad;
 
    /**
     * @var float
     */
 
    private $importe;
 
    //creo variables para la relación
 
    protected $pedido;
 
    protected $producto;
 
     public function __construct(){
 
     //$this->pedido = $pedido;
 
     //$this->producto = $producto;
 
     $this->cantidad = 0;
 
     $this->importe = 0;
 
     }
 
    /**
     * Get id
     *
     * @return integer 
     */
 
    public function getId()
    {
 
        return $this->id;
    }
 
    /**
     * Set cantidad
     *
     * @param integer $cantidad
     * @return Linea_pedido
     */
    public function setCantidad($cantidad)
    {
 
        $this->cantidad = $cantidad;
 
        return $this;
    }
 
    /**
     * Get cantidad
     *
     * @return integer 
     */
    public function getCantidad()
    {
 
        return $this->cantidad;
    }
 
    /**
     * Set importe
     *
     * @param float $importe
     * @return Linea_pedido
     */
    public function setImporte($importe)
    {
 
        $this->importe = $importe;
 
        return $this;
    }
 
    /**
     * Get importe
     *
     * @return float 
     */
 
    public function getImporte()
    {
 
        return $this->importe;
    }
     /**
     * Set producto
     *
     * @param \ProductsBundle\Entity\Producto $producto
     * @return Linea_pedido
     */
 
    public function setProducto(\ProductsBundle\Entity\Producto $producto)
    {
 
        $this->producto = $producto;
 
        return $this;
    }
 
     /**
     * Get producto
     *
     * @return \ProductsBundle\Entity\Producto 
     */
 
    public function getProducto()
    {
 
        return $this->producto;
    }
 
    /**
     * Set pedido
     *
     * @param \UsersBundle\Entity\Pedido $pedido
     * @return Linea_pedido
     */
    public function setPedido(\UsersBundle\Entity\Pedido $pedido = null)
    {
 
        $this->pedido = $pedido;
 
        return $this;
    }
 
    /**
     * Get pedido
     *
     * @return \UsersBundle\Entity\Pedido 
     */
    public function getPedido()
    {
 
        return $this->pedido;
    }
}
namespace ProductsBundle\Entity;
 
use Doctrine\ORM\Mapping as ORM;
 
/**
 * Producto
 */
 
class Producto
{
    /**
     * @var int
     */
 
    private $id;
 
    /**
     * @var string
     */
 
    private $nombre;
 
    /**
     * @var string
     */
 
    private $descripcion;
 
    /**
     * @var float
     */
 
    private $precio;
 
    /**
     * @var int
     */
 
    private $cantidad;
 
    /**
     * @var bool
     */
 
    private $vendiendo;
 
    /**
     * @var int
     */
 
    private $descuento;
 
    /**
     * @var string
     */
 
    private $imagen;
 
    //creo variable para la relación
 
    protected $linea_pedido;
 
    //constructor
    public function __construct(){
 
    $this->vendiendo = false;
 
    }
 
    public function getPrecio_descuento(){
 
    return $this->precio*((100 - $this->descuento)/100);    
    }
 
    public function getNombretoUrl(){
 
    return str_replace(' ','-', $this->nombre);
    }
 
    /**
     * Get id
     *
     * @return integer 
     */
 
    public function getId()
    {
 
        return $this->id;
    }
 
    /**
     * Set nombre
     *
     * @param string $nombre
     * @return Producto
     */
 
    public function setNombre($nombre)
    {
 
        $this->nombre = $nombre;
 
        return $this;
    }
 
    /**
     * Get nombre
     *
     * @return string 
     */
 
    public function getNombre()
    {
        return $this->nombre;
 
    }
 
    /**
     * Set descripcion
     *
     * @param string $descripcion
     * @return Producto
     */
 
    public function setDescripcion($descripcion)
    {
        $this->descripcion = $descripcion;
 
        return $this;
    }
 
    /**
     * Get descripcion
     *
     * @return string 
     */
 
    public function getDescripcion()
    {
 
        return $this->descripcion;
    }
 
    /**
     * Set precio
     *
     * @param float $precio
     * @return Producto
     */
 
    public function setPrecio($precio)
    {
 
        $this->precio = $precio;
 
        return $this;
    }
 
    /**
     * Get precio
     *
     * @return float 
     */
 
    public function getPrecio()
    {
 
        return $this->precio;
    }
 
    /**
     * Set cantidad
     *
     * @param integer $cantidad
     * @return Producto
     */
 
    public function setCantidad($cantidad)
    {
 
        $this->cantidad = $cantidad;
 
        return $this;
    }
 
    /**
     * Get cantidad
     *
     * @return integer 
     */
 
    public function getCantidad()
    {
 
        return $this->cantidad;
    }
 
    /**
     * Set vendiendo
     *
     * @param boolean $vendiendo
     * @return Producto
     */
 
    public function setVendiendo($vendiendo)
    {
 
        $this->vendiendo = $vendiendo;
 
        return $this;
    }
 
    /**
     * Get vendiendo
     *
     * @return boolean 
     */
 
    public function getVendiendo()
    {
 
        return $this->vendiendo;
    }
 
    /**
     * Set descuento
     *
     * @param integer $descuento
     * @return Producto
     */
 
    public function setDescuento($descuento)
    {
 
        $this->descuento = $descuento;
 
        return $this;
    }
 
    /**
     * Get descuento
     *
     * @return integer 
     */
 
    public function getDescuento()
    {
 
        return $this->descuento;
    }
 
    /**
     * Set imagen
     *
     * @param string $imagen
     * @return Producto
     */
 
    public function setImagen($imagen)
    {
 
        $this->imagen = $imagen;
 
        return $this;
    }
 
    /**
     * Get imagen
     *
     * @return string 
     */
 
    public function getImagen()
    {
 
        return $this->imagen;
    }
 
    /**
     * Set linea_pedido
     *
     * @param \UsersBundle\Entity\Linea_pedido $lineaPedido
     * @return Producto
     */
 
    public function setLineaPedido(\UsersBundle\Entity\Linea_pedido $lineaPedido = null)
    {
 
        $this->linea_pedido = $lineaPedido;
 
        return $this;
    }
 
    /**
     * Get linea_pedido
     *
     * @return \UsersBundle\Entity\Linea_pedido 
     */
 
    public function getLineaPedido()
    {
        return $this->linea_pedido;
    }
 
    /**
     * @var \Doctrine\Common\Collections\Collection
     */
 
    private $lineas;
 
    /**
     * Add lineas
     *
     * @param \UsersBundle\Entity\Linea_pedido $lineas
     * @return Producto
     */
 
    public function addLinea(\UsersBundle\Entity\Linea_pedido $lineas)
    {
        $this->lineas[] = $lineas;
 
        return $this;
    }
 
    /**
     * Remove lineas
     *
     * @param \UsersBundle\Entity\Linea_pedido $lineas
     */
 
    public function removeLinea(\UsersBundle\Entity\Linea_pedido $lineas)
    {
 
        $this->lineas->removeElement($lineas);
    }
 
    /**
     * Get lineas
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
 
    public function getLineas()
    {
 
        return $this->lineas;
    }
}

Al final de la entidad producto hay unos metodos relacionados con las lineas, esto es porque al principio cree una relacion one to one bidireccional, pero finalmente la relacion la cambie a unidireccional, por lo que esas lineas no tienen significado alguno, no se utilizan en el código

#9

el código se ve fatal, como puedo pegarlo y que se vea bien?

#10

Debes colocar asi:

```php // salto de linea, // codigo, //sato de linea, ```
#11

Ok, creo que el problema es que mientras vas agregando lineas al pedido, todo lo mantienes en sesión, y en el momento de persistir el pedido al final, como viene de la sesión, las lineas del pedido vienen tambien de la sesión, y los productos asociados a esas lineas de pedido vienen de la sesión.

Como dichos productos se cargan por medio de la sesión y no desde el entity manager, Doctrine al persitir el pedido, asume que esos productos son nuevos y da el error.

La solución que te propongo es que guardes el pedido con un status de "pendiente" o algo parecido, y cuando lo vayas a procesar/finalizar, simplemente la cambias el status. Esto te permitirá dejar de usar la sesión (que no se la lleva muy bien con las entidades de doctrine), y a su vez el usuario podrá continuar creando un pedido pendiente si habia cerrado su sesión.

Si no puedes dejar de usar la sesión, te toca entonces, antes de persistir el pedido, encontrar todas las entidades (productos, no se si hay más) y hacerles merge contra el EntityManager antes de persistir el pedido.

Saludos!

#12

hola manuel, muchas gracias por tu propuesta y el tiempo que me estás dedicando.

Lo de hacer merge a las entidades ya lo había probado:

for($i=0;$i<sizeof($pedido->getLineasPedido());$i++){
        $em->merge($pedido->getLineasPedido()[$i]);
        $em->merge($pedido->getLineasPedido()[$i]->getProducto());
}

Eso es lo que hacía justo antes de persistir el pedido y el error se mantenía. No se si el merge se hace así o me estoy confundiendo, si no lo hago bien, podrías darme un ejemplo de como usarlo?

Espero tu respuesta y, en caso de que usando merge no funcione, probaré no pasando el pedido a la sesión añadiendo un atributo "estado".

La idea de pasar el pedido a la sesión es para no tener que estar persistiendo y recuperando el pedido de la base de datos continuamente. Además, si un usuario, añade una (o varias) líneas a su pedido pero no llega a finalizarlo (pulsando el botón de finalizar compra) el pedido quedaría almacenado en la base de datos, algo que no sería correcto.

Aunque, si un usuario empieza un pedido:

$pedido = new Pedido();

$pedido->addLineas($linea);

El usuario sigue navegando por el sitio, y tras un rato, decide finalizar el pedido... mi objeto pedido seguiría en memoria? Por esto es por lo que uso la sesión para el pedido, para que el pedido que empezó "se mantenga activo" y también para poder pasarlo entre controladores, ya que el pedido se crea y se persiste en controladores distintos.

Un saludo y muchas gracias de nuevo =)

#13

Debería mantenerse en memoria mientras no se borre manualmente de la sesión y mientras no cierre sesión el usuario.

Con respecto al merge, como el producto se guarda en sesión no se si se está perdiendo el id del producto.

Puedes intentar hacer un dump antes de la linea:

dump($pedido->getLineasPedido()[$i]->getProducto());
$em->merge($pedido->getLineasPedido()[$i]->getProducto());

A ver si el producto que vas a mergear tiene el id. ya que si se pierde el id, el merge no va a funcionar.

Por otro lado te recomiendo usar foreach en vez de for para una mejor claridad del código:

foreach($pedido->getLineasPedido() as $lineaPedido){
    $em->merge($lineaPedido);
    $em->merge($lineaPedido->getProducto());
}

Saludos!

#14

Copiando y pegando este código en mi controlador dentro del for

dump($pedido->getLineasPedido()[$i]->getProducto());
$em->merge($pedido->getLineasPedido()[$i]->getProducto());

El problema sigue ahí. No sé exactamente si te referías a hacer eso, la línea de dump, que es lo que hace? De todas formas, el id del producto de la línea_pedido que contiene el pedido se corresponde con el id del producto que saco de la base de datos, lo he comprobado justo antes de persistir el pedido:

$id=$pedido->getLineasPedido()[0]->getProducto()->getId();
return $this->render('UsersBundle:Users:pruebas.html.twig',array('prueba' => $id));
#15

El dump es como un var_dump de php, pero no muestra en la pantalla directamente sino que agrega la información a la barra de debug de Symfony (el Web Profiler), debes buscar en el profiler la petición que hizo el dump y buscar los dumps generados.

Paso un link de imagenes de google para que más o menos veas lo que deberías tener y donde buscar:

https://www.google.co.ve/search?q=symfony+dump&client=firefox-b-ab&source=lnms&tbm=isch&sa=X&ved=0ahUKEwizjO-D2bDQAhXI1ywKHfwKAckQ_AUICCgB&biw=1440&bih=765#tbm=isch&q=symfony+toolbar+dump

Saludos!

#16

gracias por la aclaración sobre dump y sí, el identificador del producto que contiene el pedido justo antes de persistir el pedido es el mismo que el del producto que obtuve de la base de datos (comprobado en el profiler) No sé porqué se crea esa nueva entidad... todo esto me parece muy extraño

#17

Alguna otra idea?

#18

Buen día, se me ocurre que el problema tambien podría ser el tipo de relación entre Linea y Producto.

Dices que una linea se relaciona con un producto de la forma OneToOne, quizas no sería mejor una relación ManyToOne? por el hecho de que varias lineas (De varios pedidos obviamente) pueden asociarse a un mismo producto.

Quisas el OneToOne Obliga a doctrine a crear el producto porque no puede existir más de una linea en toda la base de datos para cada producto.

Saludos!

#19

Many líneas - one producto? Creo que eso lo probé pero no se me crea la clave foránea en líneas hacia producto. Lo probaré de nuevo, gracias

#20

Hola, he creado una relación many to one entre lineas y producto, pero el error no desaparece.

Funciona bien si dejo de pasar la entidad pedido a la sesión, pero necesito alguna forma de pasar la entidad de un action a otro ya que el pedido tengo que crearlo en uno y persistirlo en otro ya que el procedimiento es:

1- el usuario selecciona el producto y lo agrega al carrito (addtoCartAction) --> se muestra la pagina del carrito

2- puede seguir comprando o finalizar el pedido

3- si elige seguir comprando vuelve al paso 1.

4- si decide finalizar el pedido, el pedido se persiste (finishOrderAction) --> en twig el finalizar pedido es un div con href="{{path('finish_order')}}"> Finalizar compra

Existe alguna forma de pasar la entidad entre actions sin utilizar la sesión ni tener que almacenarla en la base de datos para luego obtenerla?

También he pensado en dejar todo en el mismo action y meterle un formulario con sólo un botón finalizar compra (supongo que podré hacer el formulario con sólo el botón de submit) y mediante if ($form->isSubmitted()) crear y persistir el pedido...

Un saludo y gracias por la ayuda, ya estoy empezando a ver la luz