Próbuję zbudować bardzo podstawowy CMS przy użyciu Symfony i Doctrine. Mam jednostki emulujące strukturę moich witryn w następujący sposób:

Jednostka: strona

/**
 * @ORM\Entity(repositoryClass="App\Repository\ContentTree\PageRepository")
 */
class Page extends SortableBase
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     * @ORM\OrderBy({"sort_order" = "ASC"})
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Website", inversedBy="pages", cascade={"all"}, fetch="EAGER")
     */
    private $website;

    /**
     * Owning side of relation
     * @ORM\OneToMany(targetEntity="Section", mappedBy="page", fetch="EAGER")
     */
    private $sections;


    public function __construct()
    {
    ...

Jednostka: sekcja

/**
 * @ORM\Entity(repositoryClass="App\Repository\ContentTree\SectionRepository")
 */
class Section extends SortableBase
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Page", inversedBy="sections", cascade={"all"}, fetch="EAGER")
     */
    private $page;

    /**
     * Owning side of relation
     * @ORM\OneToMany(targetEntity="Entry", mappedBy="section", fetch="EAGER")
     */
    private $entries;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\SectionTemplating\SectionType", fetch="EAGER")
     */
    private $sectionType;


    public function __construct()
    {

Jednostka: wpis

/**
 * @ORM\Entity(repositoryClass="App\Repository\ContentTree\EntryRepository")
 */
class Entry extends SortableBase
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Section", inversedBy="entries", cascade={"all"}, fetch="EAGER")
     */
    private $section;

    /**
     * Owning side of relation
     * @ORM\OneToMany(targetEntity="App\Entity\ContentTypes\Content", mappedBy="entry", fetch="EAGER")
     */
    private $contents;


    public function __construct()
    {
    ...

Więc w końcu Page może mieć Sections, które może mieć Entries i tak dalej. Teraz z tego, co zebrałem z docs Zakładałem, że przy konfiguracji cascades mogę po prostu przejść i użyć EntityManager, aby usunąć wystąpienie dowolnego podmiotu (powiedzmy Section) i automatycznie usunie to wystąpienie, jak również wszystkie zawarte Entries.

Jednak gdy robię coś takiego:

$section = $this->getSectionByID($sectionid);

$entityManager = $this->getDoctrine()->getManager();
$entityManager->remove($section);
$entityManager->flush();

Jak tylko Section będzie miał jakiekolwiek Entries, otrzymam:

An exception occurred while executing 'DELETE FROM section WHERE id = ?' with params [10]:

SQLSTATE[23000]: Integrity constraint violation: 
1451 Cannot delete or update a parent row: 
a foreign key constraint fails (`webdata`.`entry`, CONSTRAINT `FK_2B219D70D823E37A` 
FOREIGN KEY (`section_id`) REFERENCES `section` (`id`))

Wiem, co to oznacza, ale po prostu nie mogę dowiedzieć się, co powinienem zrobić inaczej, aby zmusić EntityManager do przejścia w dół mojego wykresu encji i usunięcia wszystkiego od dołu do góry.

1
pdresselhaus 8 marzec 2020, 01:40

2 odpowiedzi

Najlepsza odpowiedź

Okazuje się, że aby Doctrine przeprowadzić kaskadowo remove w dół wykresu relacji encji, należałoby po prostu opisać stronę będącą właścicielem w następujący sposób:

/**
 * Owning side of relation
 * @ORM\OneToMany(targetEntity="Page", mappedBy="website", cascade={"all"}, fetch="EAGER")
 */
private $pages;

To jednak tak naprawdę mówi ORMowi, co chcesz wydarzyć podczas np. remove. To nie wystarczy do modelowania tego, co stanie się podczas rzeczywistego wywołania bazy danych. W tym celu trzeba będzie dodać dodatkowe @ORM\JoinColumn(onDelete="CASCADE") na odwrotnej stronie, na przykład:

/**
 * @ORM\ManyToOne(targetEntity="Website", inversedBy="pages", fetch="EAGER")
 * @ORM\JoinColumn(onDelete="CASCADE")
 */
private $website;

Dzięki temu wszystko działa zgodnie z przeznaczeniem. W końcu mogę też ręcznie usuwać wpisy przez phpMyAdmin, co również dawało mi wcześniej błędy.

0
pdresselhaus 9 marzec 2020, 17:12

Kaskada jest zasadniczo wodospadem w tym sensie, że określa, gdzie operacje będą przebiegać dalej.

Cascade persist oznacza: kiedy ten wpis zostanie utrwalony, pozwól operacji również spłynąć w dół do jednostek podrzędnych powiązanych z tym. Pozostałe operacje działają w podobny sposób.

W twoim przypadku kaskadowanie wydaje się być zainicjowane przez byty podrzędne. Kiedy zadzwonisz:

$entityManager->remove($section);

Menedżer encji zauważa tylko operacje kaskadowe dla page, a nie dla entries.

Kiedy zamiast tego umieścisz operację kaskadową na $entries Section, możesz skończyć z kaskadowym usunięciem, które usuwa twoją stronę z podobnych powodów.

Edycja: pod względem adnotacji spowodowałoby to:

Na stronie:

/**
 * @ORM\ManyToOne(targetEntity="Website", inversedBy="pages", fetch="EAGER")
 */
private $website;

/**
 * Owning side of relation
 * @ORM\OneToMany(targetEntity="Section", mappedBy="page", fetch="EAGER" , cascade={"all"})
 */
private $sections;

W sekcji:

/**
 * @ORM\ManyToOne(targetEntity="Page", inversedBy="sections", fetch="EAGER")
 */
private $page;

/**
 * Owning side of relation
 * @ORM\OneToMany(targetEntity="Entry", mappedBy="section", fetch="EAGER", cascade={"all"})
 */
private $entries;

Na wejściu:

/**
 * @ORM\ManyToOne(targetEntity="Section", inversedBy="entries", fetch="EAGER")
 */
private $section;

Czy na pewno chcesz zawsze chętnie wszystko pobierać, przy okazji?

1
Stratadox 8 marzec 2020, 16:01