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
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.
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, sambandetcategories
iPost
är specificerat med referensattributetPostCategory(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.
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
ochHAS_ONE
, är resultatet null; förHAS_MANY
ochMANY_MANY
, är det en tom array. Märk att sambandstypernaHAS_MANY
ochMANY_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
.
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
blirTeam.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.
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'));
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.
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.
Signup or Login in order to comment.