Associations through join models
A :through association reaches a second model by way of a join model. A user
subscribes to magazines through subscriptions; the subscription is a real model
with its own row.
Setup
The join model belongs to both sides:
| class Subscription is Model {
submethod BUILD {
self.belongs-to: user => class-name => 'User';
self.belongs-to: magazine => class-name => 'Magazine';
}
}
|
Each side declares the direct has-many to the join model, then a second
has-many that travels through it:
1
2
3
4
5
6
7
8
9
10
11
12
13 | class User is Model {
submethod BUILD {
self.has-many: subscriptions => class-name => 'Subscription';
self.has-many: magazines => %(through => :subscriptions, class-name => 'Magazine');
}
}
class Magazine is Model {
submethod BUILD {
self.has-many: subscriptions => class-name => 'Subscription';
self.has-many: users => %(through => :subscriptions, class-name => 'User');
}
}
|
The join table carries the two foreign keys:
| self.create-table: 'subscriptions', [
user => { :reference },
magazine => { :reference },
];
self.add-index: 'subscriptions', <user_id magazine_id> => { :unique };
|
Reading through the join
Create the join rows, then read straight through:
| my $user = User.create({fname => 'Greg'});
my $mag = Magazine.create({title => 'Mad'});
Subscription.create({user => $user, magazine => $mag});
$user.magazines.first.id; # $mag.id
|
The collection behaves like any other: .elems, .map, .grep.
| $user.magazines.elems;
$user.magazines.map(*.attrs<title>).sort;
|
Eager loading a through association
preload loads the whole graph in batches and also caches the intermediate
join collection, so the join rows are there too:
| my @users = User.where({}).preload(:magazines).all;
my $alice = @users.first({ .attrs<fname> eq 'Alice' });
$alice.magazines.elems; # no extra query
$alice.subscriptions.elems; # the join rows are cached as well
|
See Eager loading for preload vs includes vs
eager-load.
has-one :through
The singular form works the same way. A user has one account through their
profile:
1
2
3
4
5
6
7
8
9
10
11
12 | class Profile is Model {
submethod BUILD {
self.belongs-to: user => class-name => 'User';
self.belongs-to: account => %(class-name => 'Account', optional => True);
}
}
class User is Model {
submethod BUILD {
self.has-one: account => %(through => :profile);
}
}
|
| my $account = Account.create({name => 'gdonald'});
Profile.create({user => $user, account => $account, bio => 'Raku enthusiast'});
User.find($user.id).account.id; # $account.id
|
When no join row exists, the singular accessor returns an undefined value:
| User.find($other.id).account.defined; # False
|