0 follower

Active Record relational

Am aratat deja cu se foloseste Active Record (AR) pentru a selecta date dintr-o singura tabela a bazei de date. In aceasta sectiune, descriem cum se foloseste AR pentru a face join intre mai multe tabele din baza de date si pentru a intoarce setul de date compus.

Pentru a folosi AR relational, estenecesar ca relatiile dintre cheile primare de tip foreign sa fie clar definite intre tabelele carora li se aplica join. AR se bazeaza pe metadatele despre aceste relatii pentru a determina cum se aplica join acestor tabele.

Nota: Incepand cu versiunea 1.0.1 a Yii, putem folosi AR relational chiar daca nu definim constrangeri intre cheile foreign in baza de date.

Pentru simplicate, vom folosi schema bazei de date din diagrama ER (entity-relationship) de mai jos in exemplele din aceasta sectiune.

ER Diagram

ER Diagram

Info: Suportul pentru constrangeri cu chei foreign depinde de DBMS.

SQLite nu are suport pentru astfel de constrangeri. Dar putem totusi declara constrangerile atunci cand cream tabelele. AR poate exploata aceste declaratii pentru a aduce un suport pentru cererile relationale.

MySQL are suport pentru astfel de constrangeri doar cu engine-ul InnoDB. De aceea este recomandat sa folosim InnoDB in bazele de date MySQL. Atunci cand se foloseste MyISAM, putem sa exploatam urmatorul truc pentru a putea sa executam cereri relationale folosind AR:

CREATE TABLE Foo
(
  id INTEGER NOT NULL PRIMARY KEY
);
CREATE TABLE bar
(
  id INTEGER NOT NULL PRIMARY KEY,
  fooID INTEGER
     COMMENT 'CONSTRAINT FOREIGN KEY (fooID) REFERENCES Foo(id)'
);

In cele de mai sus, folosim cuvantul cheie COMMENT pentru a descrie constrangerea foreign care poate fi citita de catre AR pentru a recunoaste relatia descrisa.

1. Declararea relatiei

Inainte de a folosi AR pentru a executa cereri relationale, trebuie sa informam AR despre tipul de relatie dintre clasele AR.

Relatia dintre doua clase AR este direct asociata cu relatia dintre tabelele bazei de date reprezentate de catre clasele AR. Din punctul de vedere al bazei de date, o relatie dintre doua tabele A si B este de trei tipuri: one-to-many (ex. User si Post), one-to-one (ex. User si Profile) si many-to-many (ex. Category si Post). In AR, exista patru tipuri de relatii:

  • BELONGS_TO: Daca relatia dintre tabelele A si B este one-to-many, atunci B apartine lui A (ex. Post apartine lui User);

  • HAS_MANY: daca relatia dintre tabelele A si B este one-to-many, atunci A are mai multi B (ex. User are multe Post);

  • HAS_ONE: acesta este un caz special al lui HAS_MANY, in care A are cel mult un B (ex. User are cel mult un Profile);

  • MANY_MANY: acesta corespunde cu relatia many-to-many din baza de date. O tabela asociativa este necesara pentru a sparge o relatie many-to-many in relatii one-to-many, din moment ce majoritatea DBMS nu au suport pentru relatii many-to-many direct. In schema bazei de date din exemplul nostru, PostCategory serves for this purpose. In AR terminology, we can explain MANY_MANY as the combination of BELONGS_TO and HAS_MANY. For example, Post belongs to many Category and Category has many Post.

Declararea relatiei in AR implica suprascrierea metodei relations() din clasa CActiveRecord. Metoda returneaza un array cu configuratiile de relatii. Fiecare element din array reprezinta o singura relatie cu urmatorul format:

'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...optiuni aditionale)

VarName este numele relatiei; RelationType specifica tipul relatiei, care poate fi unul din patru constante: self::BELONGS_TO, self::HAS_ONE, self::HAS_MANY si self::MANY_MANY; ClassName este numele clasei AR in relatie cu aceasta clasa AR; si ForeignKey precizeaza cheile foreign key implicate in relatie. Optiuni aditionale pot fi specificate la sfarsitul fiecarei relatii (se va descrie mai tarziu acest lucru).

Urmatorul cod arata cum declaram relatiile pentru clasele User si Post.

class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'author'=>array(self::BELONGS_TO, 'User', 'authorID'),
            'categories'=>array(self::MANY_MANY, 'Category', 'PostCategory(postID, categoryID)'),
        );
    }
}
 
class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'ownerID'),
        );
    }
}

Info: O cheie foreign poate fi compusa, fiind formata din doua sau mai multe coloane. In acest caz, ar trebui sa concatenam numele coloanelor care contin cheile foreign si sa separam cu spatiu sau cu virgula. Pentru tipul de relatie MANY_MANY, tabela asociativa trebuie sa fie specificata de asemenea in cheia foreign. De exemplu, relatia categories din Post este specificata cu cheia foreign PostCategory(postID, categoryID).

Declararea relatiilor intr-o clasa AR adauga implicit o proprietate clasei pentru fiecare relatie. Dupa ce este executata o cerere relationala, proprietatea corespunzatoare va fi populata cu instantele AR cu care s-a facut legatura. De exemplu, daca $author reprezinta o instanta AR User, putem folosi $author->posts pentru a accesa instantele sale Post.

2. Executarea cererilor relationale

Cel mai simplu mod de executie al unei cereri relationale este prin citirea proprietatii relationale dintr-o instanta AR. Daca proprietatea nu este accesata anterior, va fi initiata o cerere relationala care aplica join celor doua tabele si filtreaza dupa cheia primara a instantei AR curente. Rezultatul cererii va fi salvat in proprietate ca instanta (sau instante) ale clasei (claselor) AR respective. Aceasta abordare este cunoscuta sub termenul de lazy loading (incarcare pt puturosi:D), aceasta insemnand ca cererea relationala este executata atunci cand obiectele respective sunt accesate initial. Exemplul de mai jos arata cum sa folosim aceasta abordare:

// extragem post-ul cu ID=10
$post=Post::model()->findByPk(10);
// extragem autorul post-ului: o cerere relationala va fi executata aici
$author=$post->author;

Info: Daca nu este nici o instanta reprezantand relatia respectiva, proprietatea va fi null sau un array gol. Pentru relatiile BELONGS_TO si HAS_ONE, proprietatea va fi null; pentru relatiile HAS_MANY si MANY_MANY, proprietatea va fi un array gol.

Abordarea lazy loading este foarte convenabila, dar in unele scenarii nu este eficienta deloc. De exemplu, daca vrem sa accesam informatiile despre autor pentru N post-uri, folosind abordarea lazy ar implica executarea a N cereri join. In acest caz, abordarea eager loading este de preferat.

Abordarea eager loading extrage instantele AR de legatura in acelasi timp cu instanta AR principala. Acest lucru este facut folosind metoda with() impreuna cu una dintre metodele find sau findAll din AR. De exemplu:

$posts=Post::model()->with('author')->findAll();

Codul de mai sus va returna un array de instante Post. Spre deosebire de abordarea lazy, proprietatea author din fiecare instanta Post este deja populata cu instantele corespunzatoare User inainte ca noi sa accesam proprietatea. In loc de a executa o cerere join pentru fiecare post, prin abordarea eager loading se extrag toate post-urile cu autorii lor intr-un singura cerere join!

Putem specifica mai multe nume de relatii in metoda with(). Astfel, abordarea eager loading va crea toate relatiile impreuna in acelasi timp. De exemplu, urmatorul cod va extrage toate post-urile impreuna cu autorii si categoriile lor:

$posts=Post::model()->with('author','categories')->findAll();

Putem de asemenea sa facem eager loading pe nivele. In loc sa furnizam o lista de nume de relatii, furnizam o reprezentare ierarhica de nume de relatii catre metoda with(), ca in exemplul urmator:

$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->findAll();

Codul de mai sus va extrage toate post-urile impreuna cu autorul si categoriile lor. De asemenea, vor fi extrase post-urile fiecarui autor si profilul sau.

Nota: Folosirea metodei with() a fost modificata incepand cu versiunea 1.0.2 a Yii. Trebuie citita cu atentie documentatia API in cauza.

Implementarea AR din Yii este foarte eficienta. Atunci cand se aplica eager loading cu o ierarhie de obiecte aflate in N relatii HAS_MANY sau MANY_MANY vor fi necesare N+1 cereri SQL pentru a obtine rezultatele necesare. Aceasta inseamna ca, in exemplul anterior, trebuie executate 3 cereri SQL din cauza proprietatilor posts si categories. Alte framework-uri au o abordare mult mai radicala folosind doar o singura cerere SQL. La prima vedere, aceasta abordare pare mai eficienta, pentru ca ar fi implicata doar o singura cerere SQL. In realitate, nu este deloc practic din doua motive. In primul rand, sunt multe coloane de date repetitive in rezultat care necesita un timp in plus pentru a fi transmise si procesate. In al doilea rand, numarul de randuri din setul de rezultate creste exponential cu numarul de tabele implicate. Daca sunt mai multe relatii implicate, totul devine atat de greoi si complex incat nu mai poate fi gestionat corespunzator.

Din versiunea 1.0.2 a Yii, putem de asemenea sa fortam o cerere relationala sa fie facuta intr-o singura cerere SQL. Trebuie doar sa adaugam un apel together() dupa after with(). De exemplu:example,

$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->together()->findAll();

Codul de mai sus va fi facut intr-o singura cerere SQL. Fara apelarea together, ar fi fost necesare doua cereri SQL: una in care se aplica join intre tabelele Post, User si Profile, iar cealalta in care se aplica join intre tabelele User si Post.

3. Optiuni in cererile relationale

Am mentionat ca pot fi specificate optiuni aditionale in declaratia relatiei. Aceste optiuni, specificate intr-un array de perechi key-value, sunt folosite pentru a customiza cererea relationala. Avem un sumar mai jos.

  • select: o lista de coloane care vor fi selectate pentru clasa AR de legatura. Implicit, aceasta lista este '*', adica toate coloanele. Numele de coloane ar trebui sa fie diferentiate folosind aliasToken daca apar intr-o expresie (ex. COUNT(??.name) AS nameCount).

  • params: parametrii care vor fi legati la instructiunea SQL. Ar trebui sa primeasca un array cu perechi nume-valoare. Aceasta optiune este disponibila incepand cu versiunea 1.0.3.

  • condition: clauza WHERE. Implicit nu contine nimic, Referintele catre coloane trebuie sa fie diferentiate folosind aliasToken (ex. ??.id=10).

  • on: clauza ON. Conditia specificata aici va fi adaugata la conditia join folosind operatorul AND. Aceasta optiune este disponibila incepand cu versiunea 1.0.2 a Yii.

  • order: clauza ORDER BY. implicit nu contine nimic. Referintele catre coloane trebuie sa fie diferentiate folosind aliasToken (ex. ??.age DESC).

  • with: o lista cu obiectele inrudite care ar trebui incarcate impreuna cu acest obiect. Aceasta lista este creata doar prin abordarea lazy loading, nu eager loading.

  • joinType: tipul de join pentru aceasta relatie. Implcit este LEFT OUTER JOIN.

  • aliasToken: placeholder pentru prefix de coloana. Va fi inlocuit cu alias-ul tabelei corespunzatoare pentru a se putea discrimina referintele la coloane. Implicit este '??.'.

  • alias: alias pentru tabela asociata cu aceasta relatie. Aceasta optiune este disponibila din versiunea 1.0.1 a Yii. Implicit este null, adica alias-ul tabelei este generat automat. Este diferit fata de aliasToken. aliasToken este doar un placeholder si va fi inlocuit cu alias-ul tabelei in cauza.

  • together: daca tabela asociata cu aceasta relatie should ar trebui sa faca un join fortat cu tabela primara. Aceasta optiune are sens pentru relatiile HAS_MANY si MANY_MANY. Daca optiunea nu este setata sau este false, fiecare relatie HAS_MANY sau MANY_MANY va avea instructiunea ei JOIN proprie pentru a imbunatati performanta. Aceasta optiune este disponibila incepand cu versiunea 1.0.3.

In plus, sunt disponibile urmatoarele optiuni pentru anumite relatii in timpul abordarii lazy loading:

  • group: clauza GROUP BY. Implicit nu contine nimic. De notat ca referintele la coloane trebuie diferentiate folosind aliasToken (ex. ??.age). Aceasta optiune este valabila doar in cazul relatiilor HAS_MANY si MANY_MANY.

  • having: clauza HAVING. Implicit nu contine nimic. De notat ca referintele la coloane trebuie sa fie diferentiate folosind aliasToken (ex. ??.age). Aceasta optiune este valabila doar in cazul relatiilor HAS_MANY si MANY_MANY. Este disponibila incepand cu versiunea 1.0.1 a Yii.

  • limit: clauza limit pentru limitarea randurilor selectate. Aceasta optiune NU se aplica relatiei BELONGS_TO.

  • offset: offset pentru randurile care vor fi selectate. Aceasta optiune NU se aplica relatiei BELONGS_TO.

Mai jos, modificam declaratia de relatie posts din User prin includerea unor optiuni de mai sus:

class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID'
                            'order'=>'??.createTime DESC',
                            'with'=>'categories'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'ownerID'),
        );
    }
}

Acum, daca accesam $author->posts, ar trebui sa obtinem post-urile autorului sortate dupa timpul de creare, in ordine descendenta. Fiecare instanta post are de asemenea categoriile incarcate deja.

Info: Atunci cand un nume de coloana apare in doua sau mai multe tabele care au fost legate printr-un JOIN, trebuie sa fie diferentiate. Acest lucru il facem prin prefixarea numelui de coloana cu numele tabelei. De exemplu, id devine Team.id. Totusi, in cererile relationale AR nu avem aceasta libertate deoarce instructiunile SQL sunt generate automat de catre AR, deci fiecare tabela va primi automat un alias. De aceea, pentru a evita eventuale conflicte dintre numele coloanelor, folosin un placeholder pentru a indica existenta unei coloane care trebuie sa fie diferentiata fata de celelalte. AR va inlocui placeholder-ul cu un alias de tabela corespunzator pentru a diferentia corect coloana in cauza.

4. Optiuni pentru cereri relationale dinamice

Incepand cu versiunea 1.0.2, Putem folosi optiuni pentru cereri relationale dinamice si in with() si in optiunea with. Optiunile dinamice vor suprascrie optiunile existente specificate in metoda relations(). De exemplu, in cazul modelului User de mai sus, daca vrem sa folosim abordarea eager loading pentru a incarca toate post-urile care apartin unui autor, ascending order (optiunea order din specificatia relatiei este setata cu ordine desecendenta), putem face in felul urmator:

User::model()->with(array(
    'posts'=>array('order'=>'??.createTime DESC'),
    'profile',
))->findAll();

Found a typo or you think this page needs improvement?
Edit it on github !