Doctrine2: Najlepszy sposób obsługi wielu do wielu za pomocą dodatkowych kolumn w tabeli referencyjnej


282

Zastanawiam się, jaki jest najlepszy, najczystszy i najprostszy sposób pracy z relacjami wiele do wielu w Doctrine2.

Załóżmy, że mamy album typu Master of Puppets Metalliki z kilkoma utworami. Należy jednak pamiętać, że jeden utwór może pojawiać się w więcej niż jednym albumie, podobnie jak Battery Metalliki - trzy albumy zawierają ten utwór.

Potrzebuję więc relacji wiele do wielu między albumami i ścieżkami, używając trzeciej tabeli z dodatkowymi kolumnami (np. Pozycji ścieżki w określonym albumie). Właściwie muszę użyć, jak sugeruje dokumentacja Doctrine, podwójnej relacji jeden do wielu, aby osiągnąć tę funkcjonalność.

/** @Entity() */
class Album {
    /** @Id @Column(type="integer") */
    protected $id;

    /** @Column() */
    protected $title;

    /** @OneToMany(targetEntity="AlbumTrackReference", mappedBy="album") */
    protected $tracklist;

    public function __construct() {
        $this->tracklist = new \Doctrine\Common\Collections\ArrayCollection();
    }

    public function getTitle() {
        return $this->title;
    }

    public function getTracklist() {
        return $this->tracklist->toArray();
    }
}

/** @Entity() */
class Track {
    /** @Id @Column(type="integer") */
    protected $id;

    /** @Column() */
    protected $title;

    /** @Column(type="time") */
    protected $duration;

    /** @OneToMany(targetEntity="AlbumTrackReference", mappedBy="track") */
    protected $albumsFeaturingThisTrack; // btw: any idea how to name this relation? :)

    public function getTitle() {
        return $this->title;
    }

    public function getDuration() {
        return $this->duration;
    }
}

/** @Entity() */
class AlbumTrackReference {
    /** @Id @Column(type="integer") */
    protected $id;

    /** @ManyToOne(targetEntity="Album", inversedBy="tracklist") */
    protected $album;

    /** @ManyToOne(targetEntity="Track", inversedBy="albumsFeaturingThisTrack") */
    protected $track;

    /** @Column(type="integer") */
    protected $position;

    /** @Column(type="boolean") */
    protected $isPromoted;

    public function getPosition() {
        return $this->position;
    }

    public function isPromoted() {
        return $this->isPromoted;
    }

    public function getAlbum() {
        return $this->album;
    }

    public function getTrack() {
        return $this->track;
    }
}

Przykładowe dane:

             Album
+----+--------------------------+
| id | title                    |
+----+--------------------------+
|  1 | Master of Puppets        |
|  2 | The Metallica Collection |
+----+--------------------------+

               Track
+----+----------------------+----------+
| id | title                | duration |
+----+----------------------+----------+
|  1 | Battery              | 00:05:13 |
|  2 | Nothing Else Matters | 00:06:29 |
|  3 | Damage Inc.          | 00:05:33 |
+----+----------------------+----------+

              AlbumTrackReference
+----+----------+----------+----------+------------+
| id | album_id | track_id | position | isPromoted |
+----+----------+----------+----------+------------+
|  1 |        1 |        2 |        2 |          1 |
|  2 |        1 |        3 |        1 |          0 |
|  3 |        1 |        1 |        3 |          0 |
|  4 |        2 |        2 |        1 |          0 |
+----+----------+----------+----------+------------+

Teraz mogę wyświetlić listę albumów i powiązanych z nimi utworów:

$dql = '
    SELECT   a, tl, t
    FROM     Entity\Album a
    JOIN     a.tracklist tl
    JOIN     tl.track t
    ORDER BY tl.position ASC
';

$albums = $em->createQuery($dql)->getResult();

foreach ($albums as $album) {
    echo $album->getTitle() . PHP_EOL;

    foreach ($album->getTracklist() as $track) {
        echo sprintf("\t#%d - %-20s (%s) %s\n", 
            $track->getPosition(),
            $track->getTrack()->getTitle(),
            $track->getTrack()->getDuration()->format('H:i:s'),
            $track->isPromoted() ? ' - PROMOTED!' : ''
        );
    }   
}

Wyniki są takie, jakich się spodziewam, tj .: lista albumów z ich utworami w odpowiedniej kolejności i promowanymi oznaczonymi jako promowane.

The Metallica Collection
    #1 - Nothing Else Matters (00:06:29) 
Master of Puppets
    #1 - Damage Inc.          (00:05:33) 
    #2 - Nothing Else Matters (00:06:29)  - PROMOTED!
    #3 - Battery              (00:05:13) 

Więc co jest nie tak?

Ten kod pokazuje, co jest nie tak:

foreach ($album->getTracklist() as $track) {
    echo $track->getTrack()->getTitle();
}

Album::getTracklist()zwraca tablicę AlbumTrackReferenceobiektów zamiast Trackobiektów. Nie mogę utworzyć metod proxy, co by było, gdyby jedno Albumi drugie Trackmiałoby getTitle()metodę? Mógłbym wykonać dodatkowe przetwarzanie w ramach Album::getTracklist()metody, ale jaki jest najprostszy sposób, aby to zrobić? Czy jestem zmuszony napisać coś takiego?

public function getTracklist() {
    $tracklist = array();

    foreach ($this->tracklist as $key => $trackReference) {
        $tracklist[$key] = $trackReference->getTrack();

        $tracklist[$key]->setPosition($trackReference->getPosition());
        $tracklist[$key]->setPromoted($trackReference->isPromoted());
    }

    return $tracklist;
}

// And some extra getters/setters in Track class

EDYTOWAĆ

@beberlei zasugerował użycie metod proxy:

class AlbumTrackReference {
    public function getTitle() {
        return $this->getTrack()->getTitle()
    }
}

To byłby dobry pomysł, ale używam tego „obiektu referencyjnego” z obu stron: $album->getTracklist()[12]->getTitle()i $track->getAlbums()[1]->getTitle()dlatego getTitle()metoda powinna zwracać różne dane w zależności od kontekstu wywołania.

Musiałbym zrobić coś takiego:

 getTracklist() {
     foreach ($this->tracklist as $trackRef) { $trackRef->setContext($this); }
 }

 // ....

 getAlbums() {
     foreach ($this->tracklist as $trackRef) { $trackRef->setContext($this); }
 }

 // ...

 AlbumTrackRef::getTitle() {
      return $this->{$this->context}->getTitle();
 }

I to nie jest bardzo czysty sposób.


2
Jak radzisz sobie z AlbumTrackReference? Na przykład $ album-> addTrack () czy $ album-> removeTrack ()?
Daniel

Nie rozumiem twojego komentarza na temat kontekstu. W moim przypadku dane nie zależą od kontekstu. About $album->getTracklist()[12]is AlbumTrackRefobject, więc $album->getTracklist()[12]->getTitle()zawsze zwraca tytuł ścieżki (jeśli używasz metody proxy). Chociaż $track->getAlbums()[1]jest Albumobiektem, więc $track->getAlbums()[1]->getTitle()zawsze zwraca tytuł albumu.
Vinícius Fagundes,

Innym pomysłem jest użycie AlbumTrackReferencedwóch metod proxy getTrackTitle()i getAlbumTitle.
Vinícius Fagundes,

Odpowiedzi:


158

Otworzyłem podobne pytanie na liście dyskusyjnej użytkowników Doctrine i otrzymałem naprawdę prostą odpowiedź;

rozważ relację wiele do wielu jako samą istotę, a następnie uświadomisz sobie, że masz 3 obiekty, połączone między nimi relacją jeden do wielu i wiele do jednego.

http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

Gdy relacja ma dane, nie jest już relacją!


Czy ktoś wie, jak mogę uzyskać narzędzie wiersza polecenia doktryny do wygenerowania nowego bytu jako pliku schematu yml? To polecenie: app/console doctrine:mapping:import AppBundle ymlnadal generuj relację manyToMany dla dwóch oryginalnych tabel i po prostu zignoruj ​​trzecią tabelę zamiast traktować ją jako całość:/
Stphane

Czym różni foreach ($album->getTracklist() as $track) { echo $track->getTrack()->getTitle(); }się @Crozin od consider the relationship as an entity? Myślę, że chciałby zapytać, jak pominąć relacyjny byt i odzyskać tytuł ścieżki, używającforeach ($album->getTracklist() as $track) { echo $track->getTitle(); }
panda

6
„Gdy relacja ma dane, nie jest już relacją” To było naprawdę pouczające. Po prostu nie mogłem myśleć o relacji z perspektywy bytu!
Cebula

Co, jeśli relacja została już utworzona i była używana od wielu do wielu. Zdaliśmy sobie sprawę, że potrzebujemy dodatkowych pól u wielu do wielu, dlatego stworzyliśmy inny byt. Problem polega na tym, że przy istniejących danych i istniejącej tabeli o tej samej nazwie nie wydaje się, aby chcieć być przyjaciółmi. Czy ktoś już tego próbował?
tyleryzm

Dla tych, którzy zastanawiają się: tworzenie bytu z (już istniejącym) łączeniem wielu do wielu, gdy działa jego tabela, jednak podmioty posiadające wiele do wielu muszą być przystosowane, aby zamiast tego jeden do wielu do nowego bytu. również interfejsy na zewnątrz (osoby pobierające / ustawiające dla byłych wielu do wielu) najprawdopodobniej muszą zostać dostosowane.
Jakumi

17

Z $ album-> getTrackList () otrzymasz również encje „AlbumTrackReference”, więc co z dodawaniem metod ze ścieżki i proxy?

class AlbumTrackReference
{
    public function getTitle()
    {
        return $this->getTrack()->getTitle();
    }

    public function getDuration()
    {
        return $this->getTrack()->getDuration();
    }
}

W ten sposób twoja pętla znacznie się upraszcza, podobnie jak wszystkie inne kody związane z zapętlaniem ścieżek albumu, ponieważ wszystkie metody są po prostu proxy wewnątrz AlbumTrakcReference:

foreach ($album->getTracklist() as $track) {
    echo sprintf("\t#%d - %-20s (%s) %s\n", 
        $track->getPosition(),
        $track->getTitle(),
        $track->getDuration()->format('H:i:s'),
        $track->isPromoted() ? ' - PROMOTED!' : ''
    );
}

Btw Powinieneś zmienić nazwę AlbumTrackReference (na przykład „AlbumTrack”). Jest to oczywiście nie tylko odniesienie, ale zawiera dodatkową logikę. Ponieważ prawdopodobnie istnieją również utwory, które nie są podłączone do albumu, ale są dostępne tylko na płycie CD lub w inny sposób, co pozwala na czystsze rozdzielenie.


1
Metody proxy nie rozwiązują problemu w 100% (sprawdź moją edycję). Btw You should rename the AlbumT(...)- dobry punkt
Crozin

3
Dlaczego nie masz dwóch metod? getAlbumTitle () i getTrackTitle () na obiekcie AlbumTrackReference? Oba proxy do odpowiednich podobiektów.
beberlei

Celem jest najbardziej naturalny obiektowy interfejs API. $album->getTracklist()[1]->getTrackTitle()jest tak dobry / zły jak $album->getTracklist()[1]->getTrack()->getTitle(). Wydaje się jednak, że musiałbym mieć dwie różne klasy: jedną dla referencji albumu> i drugą dla referencji ścieżki> albumów - i to jest zbyt trudne do wdrożenia. To chyba najlepsze jak dotąd rozwiązanie ...
Crozin

13

Nic nie przebije ładnego przykładu

Dla osób szukających czystego kodowania przykładu powiązań jeden do wielu / wielu do jednego między 3 uczestniczącymi klasami w celu przechowywania dodatkowych atrybutów w relacji sprawdź tę witrynę:

dobry przykład skojarzeń jeden do wielu / wielu do jednego między 3 uczestniczącymi klasami

Pomyśl o swoich podstawowych kluczach

Pomyśl także o swoim kluczu podstawowym. Często możesz używać kluczy kompozytowych do takich relacji. Doctrine natywnie to popiera. Możesz przekształcić swoje odwołania w identyfikatory. Sprawdź dokumentację dotyczącą kluczy kompozytowych tutaj


10

Myślę, że wybrałbym sugestię @ beberlei dotyczącą korzystania z metod proxy. Aby uprościć ten proces, należy zdefiniować dwa interfejsy:

interface AlbumInterface {
    public function getAlbumTitle();
    public function getTracklist();
}

interface TrackInterface {
    public function getTrackTitle();
    public function getTrackDuration();
}

Następnie zarówno Ty, jak Albumi Ty Trackmożesz je zaimplementować, podczas gdy AlbumTrackReferencenadal mogą implementować oba, w następujący sposób:

class Album implements AlbumInterface {
    // implementation
}

class Track implements TrackInterface {
    // implementation
}

/** @Entity whatever */
class AlbumTrackReference implements AlbumInterface, TrackInterface
{
    public function getTrackTitle()
    {
        return $this->track->getTrackTitle();
    }

    public function getTrackDuration()
    {
        return $this->track->getTrackDuration();
    }

    public function getAlbumTitle()
    {
        return $this->album->getAlbumTitle();
    }

    public function getTrackList()
    {
        return $this->album->getTrackList();
    }
}

W ten sposób, poprzez usunięcie logiki, która jest bezpośrednio odnoszącego się Trackalbo Albumi po prostu zastąpienie go tak, że używa TrackInterfacelub AlbumInterface, masz do wykorzystania swojej AlbumTrackReferencew każdym możliwym przypadku. Będziesz potrzebował nieco rozróżnić metody między interfejsami.

To nie rozróżnia logiki DQL ani repozytorium, ale twoje usługi po prostu zignorują fakt, że przekazujesz an Albumlub an AlbumTrackReference, albo a Tracklub an, AlbumTrackReferenceponieważ ukryłeś wszystko za interfejsem :)

Mam nadzieję że to pomoże!


7

Po pierwsze, w większości zgadzam się z beberlei w sprawie jego sugestii. Możesz jednak projektować się w pułapkę. Wydaje się, że Twoja domena uważa ten tytuł za naturalny klucz do ścieżki, co prawdopodobnie ma miejsce w 99% scenariuszy, które napotkasz. Co jednak, jeśli Battery on Master of the Puppets to inna wersja (inna długość, live, akustyczna, remiks, remasterowana itp.) Niż wersja z kolekcji The Metallica .

W zależności od tego, jak chcesz obsłużyć (lub zignorować) ten przypadek, możesz albo wybrać sugerowaną trasę beberlei, albo po prostu skorzystać z proponowanej dodatkowej logiki w Album :: getTracklist (). Osobiście uważam, że dodatkowa logika jest uzasadniona dla utrzymania interfejsu API w czystości, ale oba mają swoje zalety.

Jeśli chcesz uwzględnić mój przypadek użycia, możesz mieć Ścieżki zawierające samodzielne odniesienia OneToMany do innych Ścieżek, być może $ podobnych Ścieżek. W tym przypadku byłyby dwa podmioty dla utworu Battery , jeden dla The Metallica Collection i jeden dla Master of the Puppets . Następnie każda podobna jednostka śledzenia zawierałaby odniesienie do siebie. Pozbyłoby się to także bieżącej klasy AlbumTrackReference i wyeliminowałoby obecny problem. Zgadzam się, że po prostu przenosi złożoność do innego punktu, ale jest w stanie obsłużyć przypadek użycia, którego wcześniej nie był w stanie.


6

Pytasz o „najlepszy sposób”, ale nie ma najlepszego sposobu. Istnieje wiele sposobów, a niektóre z nich już odkryłeś. To, jak chcesz zarządzać i / lub enkapsulować zarządzanie powiązaniami podczas korzystania z klas powiązań, zależy wyłącznie od Ciebie i Twojej konkretnej domeny, obawiam się, że nikt nie może pokazać Ci „najlepszego sposobu”.

Poza tym pytanie można znacznie uprościć, usuwając Doctrine i relacyjne bazy danych z równania. Istota twojego pytania sprowadza się do pytania o to, jak postępować z klasami asocjacyjnymi w zwykłym OOP.


6

Zacząłem od konfliktu z adnotacją zdefiniowaną w klasie asocjacji (z dodatkowymi polami niestandardowymi) i tabelą asocjacji zdefiniowaną w adnotacji „wiele do wielu”.

Wydaje się, że definicje odwzorowań w dwóch elementach z bezpośrednią relacją wiele do wielu powodują automatyczne tworzenie tabeli łączenia za pomocą adnotacji „joinTable”. Jednak tabela łączenia została już zdefiniowana przez adnotację w podstawowej klasie encji i chciałem, aby użyła ona własnych definicji pól tej klasy encji, aby rozszerzyć tabelę łączenia o dodatkowe pola niestandardowe.

Wyjaśnienie i rozwiązanie zostało podane powyżej przez FMaz008. W mojej sytuacji było to dzięki temu postowi na forum „ Doctrine Adnotation Question ”. Ten post zwraca uwagę na dokumentację Doctrine dotyczącą jednokierunkowych relacji ManyToMany . Spójrz na uwagę dotyczącą podejścia polegającego na zastosowaniu „klasy encji asocjacyjnej”, zastępując w ten sposób mapowanie adnotacji wiele do wielu bezpośrednio między dwiema klasami encji przez adnotację jeden do wielu w klasach encji głównych i dwie „wiele do -jedne ”adnotacje w klasie jednostek asocjacyjnych. W tym poście znajduje się przykład Modele asocjacyjne z dodatkowymi polami :

public class Person {

  /** @OneToMany(targetEntity="AssignedItems", mappedBy="person") */
  private $assignedItems;

}

public class Items {

    /** @OneToMany(targetEntity="AssignedItems", mappedBy="item") */
    private $assignedPeople;
}

public class AssignedItems {

    /** @ManyToOne(targetEntity="Person")
    * @JoinColumn(name="person_id", referencedColumnName="id")
    */
private $person;

    /** @ManyToOne(targetEntity="Item")
    * @JoinColumn(name="item_id", referencedColumnName="id")
    */
private $item;

}

3

To naprawdę przydatny przykład. Brakuje w doktrynie dokumentacji 2.

Bardzo ci dziękuję.

Dla funkcji proxy można wykonać:

class AlbumTrack extends AlbumTrackAbstract {
   ... proxy method.
   function getTitle() {} 
}

class TrackAlbum extends AlbumTrackAbstract {
   ... proxy method.
   function getTitle() {}
}

class AlbumTrackAbstract {
   private $id;
   ....
}

i

/** @OneToMany(targetEntity="TrackAlbum", mappedBy="album") */
protected $tracklist;

/** @OneToMany(targetEntity="AlbumTrack", mappedBy="track") */
protected $albumsFeaturingThisTrack;

3

Chodzi o metadane, dane o danych. Miałem ten sam problem z projektem, nad którym obecnie pracuję, i musiałem poświęcić trochę czasu, próbując go rozgryźć. Jest to zbyt wiele informacji, aby opublikować tutaj, ale poniżej znajdują się dwa linki, które mogą być przydatne. Odnoszą się one do frameworka Symfony, ale są oparte na Doctrine ORM.

http://melikedev.com/2010/04/06/symfony-saving-metadata-during-form-save-sort-ids/

http://melikedev.com/2009/12/09/symfony-w-doctrine-saving-many-to-many-mm-relationships/

Powodzenia i miłych referencji Metalliki!


3

Rozwiązanie znajduje się w dokumentacji Doctrine. W FAQ możesz to zobaczyć:

http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table

A samouczek jest tutaj:

http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html

Więc już nie róbcie, manyToManyale musicie stworzyć dodatkowy byt i umieścić manyToOnew swoich dwóch bytach.

DODAJ do komentarza @ f00bar:

to proste, musisz zrobić coś takiego:

Article  1--N  ArticleTag  N--1  Tag

Tworzysz więc encję ArticleTag

ArticleTag:
  type: entity
  id:
    id:
      type: integer
      generator:
        strategy: AUTO
  manyToOne:
    article:
      targetEntity: Article
      inversedBy: articleTags
  fields: 
    # your extra fields here
  manyToOne:
    tag:
      targetEntity: Tag
      inversedBy: articleTags

Mam nadzieję, że to pomoże



Właśnie tego szukałem, dziękuję! Niestety nie ma żadnego przykładu dla trzeciego przypadku użycia! :(Czy ktoś mógłby udostępnić przykład trzeciego przypadku użycia w formacie yml? Naprawdę przyjechałbym:#
Stphane

dodałem do odpowiedzi twoją sprawę;)
Mirza Selimovic

To jest niepoprawne. Jednostka nie musi mieć identyfikatora (id) AUTO. To źle, próbuję stworzyć właściwy przykład
Gatunox


3

Jednokierunkowy. Wystarczy dodać inversedBy: (obca nazwa kolumny), aby stała się dwukierunkowa.

# config/yaml/ProductStore.dcm.yml
ProductStore:
  type: entity
  id:
    product:
      associationKey: true
    store:
      associationKey: true
  fields:
    status:
      type: integer(1)
    createdAt:
      type: datetime
    updatedAt:
      type: datetime
  manyToOne:
    product:
      targetEntity: Product
      joinColumn:
        name: product_id
        referencedColumnName: id
    store:
      targetEntity: Store
      joinColumn:
        name: store_id
        referencedColumnName: id

Mam nadzieję, że to pomoże. Do zobaczenia.


2

Możesz osiągnąć to, co chcesz, dzięki dziedziczeniu tabeli klas, w której zmieniasz AlbumTrackReference na AlbumTrack:

class AlbumTrack extends Track { /* ... */ }

I getTrackList()zawierałby AlbumTrackobiekty, których możesz użyć tak, jak chcesz:

foreach($album->getTrackList() as $albumTrack)
{
    echo sprintf("\t#%d - %-20s (%s) %s\n", 
        $albumTrack->getPosition(),
        $albumTrack->getTitle(),
        $albumTrack->getDuration()->format('H:i:s'),
        $albumTrack->isPromoted() ? ' - PROMOTED!' : ''
    );
}

Musisz to dokładnie zbadać, aby upewnić się, że nie cierpisz pod względem wydajności.

Twoja obecna konfiguracja jest prosta, wydajna i łatwa do zrozumienia, nawet jeśli niektóre semantyki nie pasują do ciebie.


0

Podczas pobierania wszystkich ścieżek albumu z klasy albumu, wygenerujesz jeszcze jedno zapytanie o jeszcze jeden rekord. Wynika to z metody proxy. Jest inny przykład mojego kodu (patrz ostatni post w temacie): http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

Czy jest jakaś inna metoda rozwiązania tego? Czy jedno połączenie nie jest lepszym rozwiązaniem?


1
Chociaż teoretycznie może to odpowiedzieć na pytanie, lepiej byłoby zawrzeć tutaj istotne części odpowiedzi i podać odnośnik.
Spontifixus,

0

Oto rozwiązanie opisane w dokumentacji Doctrine2

<?php
use Doctrine\Common\Collections\ArrayCollection;

/** @Entity */
class Order
{
    /** @Id @Column(type="integer") @GeneratedValue */
    private $id;

    /** @ManyToOne(targetEntity="Customer") */
    private $customer;
    /** @OneToMany(targetEntity="OrderItem", mappedBy="order") */
    private $items;

    /** @Column(type="boolean") */
    private $payed = false;
    /** @Column(type="boolean") */
    private $shipped = false;
    /** @Column(type="datetime") */
    private $created;

    public function __construct(Customer $customer)
    {
        $this->customer = $customer;
        $this->items = new ArrayCollection();
        $this->created = new \DateTime("now");
    }
}

/** @Entity */
class Product
{
    /** @Id @Column(type="integer") @GeneratedValue */
    private $id;

    /** @Column(type="string") */
    private $name;

    /** @Column(type="decimal") */
    private $currentPrice;

    public function getCurrentPrice()
    {
        return $this->currentPrice;
    }
}

/** @Entity */
class OrderItem
{
    /** @Id @ManyToOne(targetEntity="Order") */
    private $order;

    /** @Id @ManyToOne(targetEntity="Product") */
    private $product;

    /** @Column(type="integer") */
    private $amount = 1;

    /** @Column(type="decimal") */
    private $offeredPrice;

    public function __construct(Order $order, Product $product, $amount = 1)
    {
        $this->order = $order;
        $this->product = $product;
        $this->offeredPrice = $product->getCurrentPrice();
    }
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.