0 follower

Relationell Active Record

Vi har redan sett hur man kan anvÀnda Active Record (AR) till att selektera data frÄn en enstaka databastabell. I det hÀr avsnittet, beskrivs hur man anvÀnder AR till att sammanfoga (join) ett antal relaterade databastabeller och lÀmna den resulterande datamÀngden i retur.

För att relationell AR skall kunna anvÀndas, krÀvs det att vÀldefinierade samband etablerats mellan primÀrnyckel resp. referensattribut (foreign key) för de tabeller som behöver förenas. AR förlitar sig pÄ metadata om dessa samband för att avgöra hur tabellerna skall sammanfogas.

MÀrk: FrÄn och med version 1.0.1, Àr det möjligt att anvÀnda relationell AR Àven utan att referensattributrestriktioner (foreign key constraints) har definierats i databasen.

För enkelhets skull kommer databasschemat som visas i följande entity- relationshipdiagram (ER-diagram) att anvÀndas för att illustrera exempel i detta avsnitt.

ER-diagram

ER-diagram

Info: Stödet för referensattributrestriktioner varierar mellan olika databashanterare.

SQLite stöder inte referensattributrestriktioner, men det gÄr ÀndÄ att deklarera restriktionerna nÀr tabeller skapas. AR kan dra fördel av dessa deklarationer för att korrekt stödja frÄgor som involverar tabellsamband.

MySQL stöder referensattributrestriktioner med InnoDB-motorn, men inte med MyISAM. DÀrför rekommenderas anvÀndning av InnoDB för MySQL databaser. NÀr MyISAM anvÀnds kan man dra fördel av följande trick, sÄ att frÄgor som involverar tabellsamband kan utföras med hjÀlp av 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)'
);

I exemplet ovan, anvÀnds nyckelordet COMMENT för att beskriva referensattributrestriktionen som sedan kan lÀsas och ge AR insikt om det beskrivna sambandet.

1. Deklarera tabellsamband ¶

Innan AR kan anvÀndas till att genomföra relationella frÄgor, mÄste AR fÄ veta hur en AR-klass relaterar till en annan.

Samband mellan tvÄ AR-klasser Àr direkt förknippat med sambandet mellan databastabellerna som AR-klasserna representerar. FrÄn databasens synvinkel kan sambandet mellan tvÄ tabeller A and B ha tre typer: en-till-mÄnga (t.ex. User och Post), en-till-en (t.ex. User och Profile) samt mÄnga-till-mÄnga (t.ex. Category och Post). Inom AR finns det fyra sorters samband:

  • BELONGS_TO: om sambandet mellan tabellerna A och B Ă€r en-till-mĂ„nga, sĂ„ Ă€r B tillhörig A (t.ex. Post tillhör User);

  • HAS_MANY: om sambandet mellan tabellerna A och B Ă€r en-till-mĂ„nga, sĂ„ har A mĂ„nga B (t.ex. User har mĂ„nga Post);

  • HAS_ONE: detta Ă€r ett specialfall av HAS_MANY dĂ€r A har som mest en B (t.ex. User har som mest en Profile);

  • MANY_MANY: detta motsvarar mĂ„nga-till-mĂ„ngasambandet i databasen. En assisterande tabell erfordras för att bryta upp ett mĂ„nga-till-mĂ„ngasamband i ett-till-mĂ„ngasamband, eftersom de flesta databashanterare saknar direkt stöd för mĂ„nga-till-mĂ„ngasamband. I vĂ„rt exempelschema, tjĂ€nar PostCategory detta syfte. Med AR terminology kan MANY_MANY förklaras som kombinationen av BELONGS_TO och HAS_MANY. Till exempel, Post tilhör mĂ„nga Category och Category har mĂ„nga Post.

Tabellsamband deklareras i AR genom att metoden relations() i CActiveRecord ÄsidosÀtts. Denna metod returnerar en array med sambandskonfigurationer. Varje element i denna array representerar ett enstaka samband, pÄ följande format:

'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...additional options)

dÀr VarName Àr sambandets namn; RelationType specificerar sambandets typ, som kan vara en av de fyra konstanternas: self::BELONGS_TO, self::HAS_ONE, self::HAS_MANY samt self::MANY_MANY; ClassName Àr namnet pÄ den AR-klass som har samband med denna AR-klass; ForeignKey specificerar det eller de referensattribut som Àr involverade i sambandet. Ytterligare alternativ kan specificeras i slutet av varje sambandsdeklaration (beskrivs lÀngre fram).

Följande kod visar hur sambandet mellan klasserna User och Post deklareras.

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: Ett referensattribut kan vara sammansatt och bestÄ av tvÄ eller flera kolumner. I det fallet skall namnen pÄ kolumner som ingÄr i referensattributet skrivas efter varandra, separerade av blanksteg eller komma. För samband av typen MANY_MANY mÄste namnet pÄ den assisterande tabellen ocksÄ specificeras i referensattributet. Till exempel, sambandet categories i Post Àr specificerat med referensattributet PostCategory(postID, categoryID).

Deklarationen av samband i en AR-klass lÀgger underförstÄtt till en property i klassen för varje samband. NÀr en relationell frÄga har utförts kommer den motsvarande propertyn att innehÄlla den relaterade AR-instansen(-erna). Till exempel, om $author representerar en AR-instans User, kan $author->posts anvÀndas för tillgÄng till dess relaterade Post-instans.

2. Utföra relationell frÄga ¶

Det enklaste sÀttet att utföra en relationell frÄga Àr genom att lÀsa en relationell property i en AR-instans. Om denna property inte har lÀsts tidigare kommer en relationell frÄga att initieras, som slÄr samman de tvÄ relaterade tabellerna och filtrerar med primÀrnyckeln i aktuell AR-instans. FrÄgeresultatet kommer att sparas i propertyn som en eller flera instanser av den relaterade AR- klassen. Detta förfarande Àr kÀnt som lazy loading, dvs den relationella frÄgan utförs först nÀr relaterade objekt refereras till första gÄngen. Exemplet nedan visar hur man anvÀnder detta tillvÀgagÄngssÀtt:

// retrieve the post whose ID is 10
$post=Post::model()->findByPk(10);
// retrieve the post's author: a relational query will be performed here
$author=$post->author;

Info: Om det saknas en relaterad instans i ett samband kan den motsvarande propertyn anta vÀrdet null eller en tom array. För sambanden BELONGS_TO och HAS_ONE , Àr resultatet null; för HAS_MANY och MANY_MANY, Àr det en tom array. MÀrk att sambandstyperna HAS_MANY och MANY_MANY returnerar arrayer av objekt, dÀrför behöver man iterera över resultatet för att komma Ät propertyn. Om man inte gör detta erhÄlls felet "Trying to get property of non-object".

TillvÀgagÄngssÀttet med lazy loading Àr mycket bekvÀmt att anvÀnda, men har lÀgre prestanda i vissa scenarier. Till exempel, om vi vill fÄ tillgÄng till information om författare för N postningar, kommer tillvÀgagÄngssÀttet lazy att omfatta körning av N join-frÄgor. Under dessa omstÀndigheter bör det alternativa tillvÀgagÄngssÀttet, kallat eager loading, anvÀndas.

TillvÀgagÄngssÀttet eager loading hÀmtar in relaterade AR-instanser tillsammans med huvudinstansen (-instanserna). Detta Ästadkommes genom anvÀndning av metoden with() tillsammans med en av find- eller findAll-metoderna i AR. Till exempel,

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

OvanstÄende kod returnerar en array bestÄende av Post-intanser. Till skillnad frÄn tillvÀgagÄngssÀttet lazy, Àr propertyn author i varje instans av Post redan laddad med den relaterade User-instansen redan innan vi refererar till propertyn. I stÀllet för att exekvera en join-frÄga för varje postning, hÀmtar tillvÀgagÄngssÀttet eager loading in samtliga postningar tillsammans med deras respektive författare, alltsammans i en enda join-frÄga!

Man kan specificera flera sambandsnamn till metoden with() och tillvÀgagÄngssÀttet eager loading kommer att hÀmta in dem alla i ett moment. Till exempel, följande kod hÀmtar in postningar tillsammans med deras repektive författare och kategorier:

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

Det gÄr att anvÀnda nÀstlad eager loading. I stÀllet för en lista med sambandsnamn, lÀmnar vi med en hierarkisk representation av sambandsnamnen till metoden with(), som i följande exempel,

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

OvanstÄende exempel hÀmtar in alla postningar tillsammans med deras respektive författare och kategorier. Det hÀmtar Àven in varje författares profil samt postningar.

MÀrk: SÀttet att anvÀnda metoden with() har Àndrats frÄn och med version 1.0.2. Den tillhörande API-dokumentationen bör lÀsas omsorgsfullt.

AR-implementeringen i Yii Àr mycket effektiv. Vid eager loading av en hierarki av relaterade objekt omfattande N HAS_MANY- eller MANY_MANY-samband, behövs N+1 SQL-frÄgor för att uppnÄ önskat resultat. Detta innebÀr att den behöver exekvera 3 SQL-frÄgor i det förra exemplet, pÄ grund av propertyna posts och categories. Andra ramverk tar ett mer radikalt grepp genom att anvÀnda en enda SQL-frÄga. Vid en första anblick, verkar det radikala angreppssÀttet mer effektivt, pÄ grund av att fÀrre frÄgor behöver avkodas och exekveras av databashanteraren. Men det Àr i verkligheten opraktiskt av tvÄ skÀl. För det första, finns det mÄnga repetitiva datakolumner i resultatet, vilka krÀver mer tid att överföra och bearbeta. För det andra, vÀxer antalet rader i resultatmÀngden exponentiellt med antalet involverade tabeller, vilket gör saken ohanterlig i takt med att fler samband omfattas.

Sedan version 1.0.2, gÄr det Àven att tvinga fram att en relationell frÄga utförs med hjÀlp av endast en SQL-frÄga. Detta sker helt enkelt genom att ett anrop till together() lÀggs till efter with(). Till exempel,

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

OvanstÄende frÄga kommer att utföras i en enda SQL-frÄga. Utan anropet till together, skulle det behövas tre SQL-frÄgor: en slÄr samman tabellerna Post, User och Profile, en slÄr samman tabellerna User och Post och en slÄr samman Post, PostCategory och Category.

3. Alternativ för relationella frÄgor ¶

Som nÀmnts kan ytterligare alternativ anges i sambandsdeklarationer. Dessa alternativ, specificerade i form av namn-vÀrdepar, anvÀnds för att anpassa den relationella frÄgan. De sammanfattas nedan.

  • select: en lista med med kolumner som skall selekteras till den relaterade AR-klassen. Den har standardvĂ€rdet '*', vilket innebĂ€r alla kolumner. Kolumnnamn skall göras otvetydiga med hjĂ€lp av aliasToken om de anvĂ€nds i ett uttryck (t.ex. COUNT(??.name) AS nameCount).

  • condition: motsvarar WHERE-ledet. Det Ă€r som standard tomt. MĂ€rk att kolumnreferenser behöver göras otvetydiga med hjĂ€lp av aliasToken (t.ex. ??.id=10).

  • params: parametrarna som skall kopplas ihop med den genererade SQL-satsen. Dessa skall ges som en array bestĂ„ende av namn-vĂ€rdepar. Detta alternativ har varit tillgĂ€ngligt frĂ„n och med version 1.0.3.

  • on: motsvarar ON-ledet. Villkoret som specificeras hĂ€r kommer att lĂ€ggas till sammanslagningsvillkoret med hjĂ€lp av AND-operatorn. MĂ€rk att kolumnreferenser behöver göras otvetydiga med hjĂ€lp av aliasToken (t.ex. ??.id=10). Detta alternativ Ă€r inte relevant vid MANY_MANY-relationer. Det har varit tillgĂ€ngligt frĂ„n och med version 1.0.2.

  • order: motsvarar ORDER BY-ledet. Det Ă€r som standard tomt. MĂ€rk att kolumnreferenser behöver göras otvetydiga med hjĂ€lp av aliasToken (t.ex. ??.age DESC).

  • with: en lista med underordnade relaterade objekt som skall laddas tillsammans med detta objekt. Var uppmĂ€rksam pĂ„ att om detta alternativ anvĂ€nds olĂ€mpligt, kan det leda till en Ă€ndlös slinga av relationer.

  • joinType: typ av sammanslagning för detta samband. Den Ă€r som standard LEFT OUTER JOIN.

  • aliasToken: platshĂ„llare för kolumnprefix. Den ersĂ€tts med motsvarande tabellalias sĂ„ att kolumnreferenser kan göras otvetydiga. StandardvĂ€rde Ă€r '??.'.

  • alias: aliasnamn för tabellen som förknippas med detta samband. Detta alternativ har varit tillgĂ€ngligt frĂ„n och med version 1.0.1. StandardvĂ€rde Ă€r null, vilket innebĂ€r att tabellalias genereras automatiskt. Detta skiljer sig frĂ„n aliasToken pĂ„ sĂ„ sĂ€tt att den senare bara Ă€r en platshĂ„llare och ersĂ€tts med faktiskt tabellalias.

  • together: huruvida tabellen associerad med detta samband skall tvingas till en ovillkorlig sammanslagning (join) med den primĂ€ra tabellen. Detta alternativ Ă€r endast relevant för samband av typerna HAS_MANY och MANY_MANY. Om alternativet inte anges eller sĂ€tts till false, kommer varje HAS_MANY- eller MANY_MANY-samband att, av prestandaskĂ€l, ha sin egen JOIN-sats. Detta alternativ har varit tillgĂ€ngligt frĂ„n och med version 1.0.3.

  • group: motsvarar GROUP BY-ledet. Det Ă€r som standard tomt. MĂ€rk att kolumnreferenser behöver göras otvetydiga med hjĂ€lp av aliasToken (e.g. ??.age).

  • having: motsvarar HAVING-ledet. Det Ă€r som standard tomt. MĂ€rk att kolumnreferenser behöver göras otvetydiga med hjĂ€lp av aliasToken (e.g. ??.age). Detta alternativ har varit tillgĂ€ngligt frĂ„n och med version 1.0.1.

  • index: namnet pĂ„ kolumnen vars vĂ€rden skall anvĂ€ndas som nycklar i den array som lagrar relaterade objekt. Om detta alternativ inte sĂ€tts kommer en relaterad objektarray att anvĂ€nda ett nollbaserat heltalsindex. Detta alternativ kan endast sĂ€ttas för sambandstyperna HAS_MANY och MANY_MANY. Detta alternativ har varit tillgĂ€ngligt sedan version 1.0.7.

Dessutom Àr följande alternativ tillgÀngliga för vissa samband nÀr lazy loading anvÀnds:

  • limit: begrĂ€nsar antalet rader som kan selekteras. Detta alternativ Ă€r INTE tillĂ€mpligt pĂ„ BELONGS_TO-samband.

  • offset: offset till rader som skall selekteras. Detta alternativ Ă€r INTE tillĂ€mpligt pĂ„ BELONGS_TO-samband.

Nedan har deklarationen av sambandet posts i User varierats genom inkludering av nÄgra av ovanstÄende alternativ:

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'),
        );
    }
}

Om vi nu refererar till $author->posts, kommer vi att erhÄlla författarens postningar sorterade i fallande ordning efter tid de skapats. Varje instans av postning har ocksÄ fÄtt sina kategorier laddade.

Info: NÀr ett kolumnnamn upptrÀder i tvÄ eller fler tabeller som slÄs samman (join), behöver det göras otvetydigt. Detta Ästadkoms genom att föregÄ kolumnnamnet med dess tabellnamn. Till exempel, id blir Team.id. I AR:s relationella frÄgor dÀremot, saknas denna frihet eftersom SQL-satserna genereras automatiskt av AR, vilket systematiskt ger varje tabell ett alias. Av denna anledning anvÀnds, för att undvika konflikter mellan kolumnnamn, en platshÄllare för att indikera förekomsten av en kolumn som behöver göras otvetydig. AR ersÀtter platshÄllaren med ett passande tabellalias och gör kolumnen otvetydig.

4. Alternativ för dynamisk relationell frÄga ¶

Med start frÄn och med version 1.0.2, gÄr det att anvÀnda alternativ för dynamisk relationell frÄga bÄde med metoden with() och med with-alternativet. De dynamiska alternativen skriver över existerande alternativ som specificerats i metoden relations(). Till exempel, för att, med ovanstÄende User-modell, anvÀnda tillvÀgagÄngssÀttet eager loading till att hÀmta in postningar tillhörande en författare i stigande ordningsföljd (order-alternativet i sambandet specificerar fallande ordningsföljd), kan man göra följande:

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

Med start fr o m version 1.0.5 kan dynamiska frÄgealternativ Àven anvÀndas med relationella frÄgor som anvÀnder tillvÀgagÄngssÀttet lazy loading. För att göra sÄ, anropa en metod vars namn Àr lika sambandsnamnet och lÀmna med de dynamiska frÄgealternativen som metodparameter. Till exempel returnerar följande kod de av en anvÀndares postningar vars status` Àr lika med 1:

$user=User::model()->findByPk(1);
$posts=$user->posts(array('condition'=>'status=1'));

5. StatistikfrÄga ¶

MÀrk: StatistikfrÄgor har understötts fr o m version 1.0.4.

Utöver relationella frÄgor som beskrivits ovan, stöder Yii ocksÄ sÄ kallade statistikfrÄgor (eller aggregationsfrÄgor). Detta refererar till inhÀmtning av aggregeringsinformation om relaterade objekt, sÄsom antalet kommentarer till varje postning, den genomsnittliga poÀngsÀttningen för varje produkt, etc. StatistikfrÄgor kan endast utföras mot objekt som har sambandstyperna HAS_MANY (t.ex. en postning har mÄnga kommentarer) eller MANY_MANY (t.ex. en postning tillhör mÄnga kategorier och en kategori har mÄnga postningar).

Att genomföra en statistikfrÄga Àr mycket snarlikt till att utföra en relationell frÄga, som tidigare besrivits. Först deklareras en statistikfrÄga i metoden relations() i CActiveRecord precis som vid en relationell frÄga.

class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'commentCount'=>array(self::STAT, 'Comment', 'postID'),
            'categoryCount'=>array(self::STAT, 'Category', 'PostCategory(postID, categoryID)'),
        );
    }
}

Ovan deklareras tvÄ statistikfrÄgor: commentCount berÀknar antalet kommentarer som tillhör en postning och categoryCount berÀknar antalet kategorier en postning tillhör. MÀrk att sambandstypen mellan between Post och Comment Àr HAS_MANY, medan sambandstypen mellan Post och Category Àr MANY_MANY (med hjÀlp av mellantabellen PostCategory). Som tydligt framgÄr Àr deklarationen mycket snarlik de sambandsdeklarationer som beskrivits i tidigare delavsnitt. Den enda skillnaden Àr att sambandstypen STAT anvÀnds hÀr.

Med ovanstÄende deklaration kan vi hÀmta antalet kommentarer till en postning med hjÀlp av uttrycket $post->commentCount. NÀr vi anvÀnder denna property första gÄngen, kommer en SQL-sats att exekveras implicit för att hÀmta in det önskade resultatet. Som bekant Àr detta den sÄ kallade lazy loading-metoden. Vi kan Àven anvÀnda eager loading-metoden om vi behöver avgöra antalet kommentarer för ett flertal postningar:

$posts=Post::model()->with('commentCount', 'categoryCount')->findAll();

OvanstÄende programsats exekverar tre SQL-satser för att leverera alla postningar tillsammans med deras respektive kommentarantal och antal kategorier. Om lazy loading-metoden anvÀnds blir resultatet att 2*N+1 SQL-frÄgor exekveras givet N postningar.

Som standard kalkylerar en statistikfrÄga COUNT-uttrycket (och dÀrmed kommentarantalet och antalet kategorier i ovanstÄende exempel). Detta kan vi anpassa genom att ange ytterligare alternativ nÀr vi deklarerar relations(). De tillgÀngliga alternativen summeras nedan.

  • select: statistikfrĂ„gan. Som standard COUNT(*), innebĂ€rande antalet underordnade objekt.

  • defaultValue: vĂ€rde som skall tilldelas de poster som inte erhĂ„ller ett resultat frĂ„n statistikfrĂ„gan. Till exempel, om en postning inte har nĂ„gra kommentarer, kommer dess commentCount att Ă„sĂ€ttas detta vĂ€rde. StandardvĂ€rde för detta alternativ Ă€r 0.

  • condition: WHERE-ledet. Som standard tomt.

  • params: parametrarna som skall kopplas till den genererade SQL-satsen. De skall anges som en array av namn-vĂ€rdepar.

  • order: ORDER BY-ledet. Som standard tomt.

  • group: GROUP BY-ledet. Som standard tomt.

  • having: HAVING-ledet. Som standard tomt.

6. Relationell frÄga med namngivna omfÄng ¶

MÀrk: Stödet för namngivna omfÄng har varit tillgÀngligt sedan version 1.0.5.

Relationella frÄgor kan Àven utföras i kombination med namngivna omfÄng. Detta kan ske i tvÄ former. I den första formen appliceras namngivna omfÄng pÄ huvudmodellen. I den andra formen appliceras namngivna omfÄng pÄ relaterade modeller.

Följande kod visar hur namngivna omfÄng appliceras pÄ huvudmodellen.

$posts=Post::model()->published()->recently()->with('comments')->findAll();

Detta Àr mycket snarlikt icke-relationella frÄgor. Den enda skillnaden Àr anropet av with() efter kedjan av namngivna omfÄng. OvanstÄende frÄga skulle hÀmta nyligen publicerade postningar tillsammans med dess kommentarer.

FÀljande kod visar hur namngivna omfÄng appliceras pÄ relaterade modeller.

$posts=Post::model()->with('comments:recently:approved')->findAll();

OvanstÄende frÄga skulle hÀmta alla postningar tillsammans med deras för publicering godkÀnda kommentarer. MÀrk att comments refererar till sambandsnamnet, medan recently och approved refererar till tvÄ namngivna omfÄng som deklarerats i modellklassen Comment. Sambandsnamnet och de namngivna omfÄngen skall separeras med kolon.

Namngivna omfÄng kan Àven specificeras med alternativet with i sambandsdeklarationen i CActiveRecord::relations(). I följande exempel kommer - om vi accessar $user->posts - alla postningarnas godkÀnda (för publicering) kommentarer att hÀmtas.

class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID',
                'with'=>'comments:approved'),
        );
    }
}

MÀrk: Namngivna omfÄng som appliceras pÄ relaterade modeller mÄste specificeras i CActiveRecord::scopes. Detta innebÀr ocksÄ att de inte kan parametriseras.