Skip to content

Commit d4e8b04

Browse files
committed
Add the ability to load and group a child association for many parents
This is somewhat analogous to the `includes` method from Active Record. However, it does not allow arbitrarily deep nesting. That means you can load as many child associations for the parent as you want, but not grandchildren or great grandchildren (you can certainly use this infrastructure to do that, we just don't provide helpers for it out of the box) There's still several enhancements I'd like to make. For instance, having to write `#[derive(Identifiable)]` feels funky, as you'd never use that on its own. We could maybe do it on `#[derive(Queryable)]` if the model has a field with the same name as the primary key, but that feels really gross. Probably the best place for us to do it automatically would be in `#[has_many]`, but that can be a separate PR. This also does not generate the optimal query for PG, as it creates an `IN` statement instead of `= ANY`. We eventually want to change the node returned by `eq_any` to do the right thing for PG, but it needs specialization to be done cleanly. Perhaps for 0.7 we can do that optimization if `feature = "unstable"`. This does not handle cases where the foreign key is nullable. We skip implementing `BelongsTo` in that case, meaning the associations can be loaded but not grouped. I have some ideas on how we can handle this in the future, but that will be a separate PR. I'd like to find a way to eliminate the need to specify the struct type for the association twice. This could either be by moving `BelongingToDsl` over to the table, or by somehow carrying the type that the method was called on. Finally, I think we can add an additional layer on top of this to handle the common case of "load and group the associations without additional filtering" into something like `users.load_associated::<Post>(&conn)`
1 parent 06e3205 commit d4e8b04

14 files changed

Lines changed: 237 additions & 19 deletions

File tree

diesel/src/associations/mod.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use std::hash::Hash;
2+
3+
pub trait Identifiable {
4+
type Id: Hash + Eq + Copy;
5+
6+
fn id(&self) -> Self::Id;
7+
}
8+
9+
pub trait BelongsTo<Parent: Identifiable> {
10+
fn foreign_key(&self) -> Parent::Id;
11+
}
12+
13+
pub trait GroupedBy<Parent>: IntoIterator + Sized {
14+
fn grouped_by(self, parents: &[Parent]) -> Vec<Vec<Self::Item>>;
15+
}
16+
17+
impl<Parent, Child> GroupedBy<Parent> for Vec<Child> where
18+
Child: BelongsTo<Parent>,
19+
Parent: Identifiable,
20+
{
21+
fn grouped_by(self, parents: &[Parent]) -> Vec<Vec<Child>> {
22+
use std::collections::HashMap;
23+
24+
let id_indices: HashMap<_, _> = parents.iter().enumerate().map(|(i, u)| (u.id(), i)).collect();
25+
let mut result = parents.iter().map(|_| Vec::new()).collect::<Vec<_>>();
26+
for child in self {
27+
let index = id_indices[&child.foreign_key()];
28+
result[index].push(child);
29+
}
30+
result
31+
}
32+
}

diesel/src/expression/array_comparison.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use backend::Backend;
22
use expression::*;
3+
use expression::helper_types::SqlTypeOf;
34
use query_builder::{QueryBuilder, QueryFragment, BuildQueryResult};
45
use result::QueryResult;
56
use types::Bool;
@@ -9,6 +10,8 @@ pub struct In<T, U> {
910
values: U,
1011
}
1112

13+
pub type EqAny<T, U> = In<T, <U as AsInExpression<SqlTypeOf<T>>>::InExpression>;
14+
1215
impl<T, U> In<T, U> {
1316
pub fn new(left: T, values: U) -> Self {
1417
In {

diesel/src/expression/helper_types.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
use super::{Expression, AsExpression};
55
use types;
66

7-
pub type AsExpr<Item, TargetExpr> = AsExprOf<Item, <TargetExpr as Expression>::SqlType>;
7+
pub type SqlTypeOf<Expr> = <Expr as Expression>::SqlType;
8+
pub type AsExpr<Item, TargetExpr> = AsExprOf<Item, SqlTypeOf<TargetExpr>>;
89
pub type AsExprOf<Item, Type> = <Item as AsExpression<Type>>::Expression;
910

1011
macro_rules! gen_helper_type {
@@ -35,4 +36,7 @@ pub type Between<Lhs, Rhs> = super::predicates::Between<Lhs,
3536
pub type NotBetween<Lhs, Rhs> = super::predicates::NotBetween<Lhs,
3637
super::predicates::And<AsExpr<Rhs, Lhs>, AsExpr<Rhs, Lhs>>>;
3738

39+
#[doc(inline)]
3840
pub use super::predicates::{IsNull, IsNotNull, Asc, Desc};
41+
#[doc(inline)]
42+
pub use super::array_comparison::EqAny;

diesel/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod macros;
99
#[macro_use]
1010
pub mod query_builder;
1111

12+
pub mod associations;
1213
pub mod backend;
1314
pub mod connection;
1415
#[macro_use]
@@ -72,6 +73,7 @@ pub mod helper_types {
7273

7374
pub mod prelude {
7475
//! Re-exports important traits and types. Meant to be glob imported when using Diesel.
76+
pub use associations::GroupedBy;
7577
pub use connection::Connection;
7678
pub use expression::{Expression, SelectableExpression, BoxableExpression};
7779
pub use expression::expression_methods::*;

diesel/src/query_dsl/belonging_to_dsl.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use query_builder::AsQuery;
22

3-
pub trait BelongingToDsl<T> {
3+
pub trait BelongingToDsl<T: ?Sized> {
44
type Output: AsQuery;
55

66
fn belonging_to(other: &T) -> Self::Output;

diesel_codegen/src/associations/belongs_to.rs

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ use syntax::ptr::P;
1010

1111
use model::Model;
1212
use super::{parse_association_options, AssociationOptions, to_foreign_key};
13-
use util::ty_param_of_option;
13+
use util::{ty_param_of_option, is_option_ty};
1414

1515
pub fn expand_belongs_to(
1616
cx: &mut ExtCtxt,
1717
span: Span,
1818
meta_item: &MetaItem,
1919
annotatable: &Annotatable,
20-
push: &mut FnMut(Annotatable)
20+
push: &mut FnMut(Annotatable),
2121
) {
2222
let options = parse_association_options("belongs_to", cx, span, meta_item, annotatable);
2323
if let Some((model, options)) = options {
@@ -28,9 +28,12 @@ pub fn expand_belongs_to(
2828
span: span,
2929
};
3030

31-
push(Annotatable::Item(belonging_to_dsl_impl(&builder)));
31+
belonging_to_dsl_impl(&builder, push);
3232
push(Annotatable::Item(join_to_impl(&builder)));
33-
for item in selectable_column_hack(&builder).into_iter() {
33+
if let Some(item) = belongs_to_impl(&builder) {
34+
push(Annotatable::Item(item));
35+
}
36+
for item in selectable_column_hack(&builder) {
3437
push(Annotatable::Item(item));
3538
}
3639
}
@@ -113,15 +116,18 @@ fn capitalize_from_association_name(name: String) -> String {
113116
result
114117
}
115118

116-
fn belonging_to_dsl_impl(builder: &BelongsToAssociationBuilder) -> P<ast::Item> {
119+
fn belonging_to_dsl_impl(
120+
builder: &BelongsToAssociationBuilder,
121+
push: &mut FnMut(Annotatable),
122+
) {
117123
let parent_struct_name = builder.parent_struct_name();
118124
let child_struct_name = builder.child_struct_name();
119125
let child_table = builder.child_table();
120126
let foreign_key = builder.foreign_key();
121127
let primary_key_type = builder.primary_key_type();
122128
let primary_key_name = builder.primary_key_name();
123129

124-
quote_item!(builder.cx,
130+
let item = quote_item!(builder.cx,
125131
impl ::diesel::BelongingToDsl<$parent_struct_name> for $child_struct_name {
126132
type Output = ::diesel::helper_types::FindBy<
127133
$child_table,
@@ -133,7 +139,63 @@ fn belonging_to_dsl_impl(builder: &BelongsToAssociationBuilder) -> P<ast::Item>
133139
$child_table.filter($foreign_key.eq(model.$primary_key_name.clone()))
134140
}
135141
}
136-
).unwrap()
142+
).unwrap();
143+
push(Annotatable::Item(item));
144+
145+
let item = quote_item!(builder.cx,
146+
impl ::diesel::BelongingToDsl<Vec<$parent_struct_name>> for $child_struct_name {
147+
type Output = ::diesel::helper_types::Filter<
148+
$child_table,
149+
::diesel::expression::helper_types::EqAny<
150+
$foreign_key,
151+
Vec<$primary_key_type>,
152+
>,
153+
>;
154+
155+
fn belonging_to(parents: &Vec<$parent_struct_name>) -> Self::Output {
156+
let ids = parents.iter().map(|p| p.$primary_key_name.clone()).collect::<Vec<_>>();
157+
$child_table.filter($foreign_key.eq_any(ids))
158+
}
159+
}
160+
).unwrap();
161+
push(Annotatable::Item(item));
162+
163+
let item = quote_item!(builder.cx,
164+
impl ::diesel::BelongingToDsl<[$parent_struct_name]> for $child_struct_name {
165+
type Output = ::diesel::helper_types::Filter<
166+
$child_table,
167+
::diesel::expression::helper_types::EqAny<
168+
$foreign_key,
169+
Vec<$primary_key_type>,
170+
>,
171+
>;
172+
173+
fn belonging_to(parents: &[$parent_struct_name]) -> Self::Output {
174+
let ids = parents.iter().map(|p| p.$primary_key_name.clone()).collect::<Vec<_>>();
175+
$child_table.filter($foreign_key.eq_any(ids))
176+
}
177+
}
178+
).unwrap();
179+
push(Annotatable::Item(item));
180+
}
181+
182+
fn belongs_to_impl(builder: &BelongsToAssociationBuilder) -> Option<P<ast::Item>> {
183+
let parent_struct_name = builder.parent_struct_name();
184+
let child_struct_name = builder.child_struct_name();
185+
let primary_key_type = builder.primary_key_type();
186+
let foreign_key_name = builder.foreign_key_name();
187+
188+
if is_option_ty(&builder.foreign_key_type()) {
189+
None
190+
} else {
191+
Some(quote_item!(builder.cx,
192+
impl ::diesel::associations::BelongsTo<$parent_struct_name> for $child_struct_name {
193+
fn foreign_key(&self) -> $primary_key_type {
194+
self.$foreign_key_name
195+
}
196+
}
197+
).unwrap())
198+
}
137199
}
138200

139201
fn join_to_impl(builder: &BelongsToAssociationBuilder) -> P<ast::Item> {

diesel_codegen/src/identifiable.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use syntax::ast;
2+
use syntax::codemap::Span;
3+
use syntax::ext::base::{Annotatable, ExtCtxt};
4+
use syntax::ext::build::AstBuilder;
5+
6+
use model::Model;
7+
8+
pub fn expand_derive_identifiable(
9+
cx: &mut ExtCtxt,
10+
span: Span,
11+
_meta_item: &ast::MetaItem,
12+
annotatable: &Annotatable,
13+
push: &mut FnMut(Annotatable)
14+
) {
15+
if let Some(model) = Model::from_annotable(cx, span, annotatable) {
16+
let struct_name = model.name;
17+
let primary_key_name = model.primary_key_name();
18+
let primary_key_type = match model.attr_named(primary_key_name) {
19+
Some(a) => a.ty.clone(),
20+
None => {
21+
let err_msg = format!(
22+
"Could not find a field named `{}` on `{}`",
23+
primary_key_name,
24+
struct_name,
25+
);
26+
cx.span_err(span, &err_msg);
27+
return;
28+
}
29+
};
30+
31+
let item = quote_item!(cx,
32+
impl ::diesel::associations::Identifiable for $struct_name {
33+
type Id = $primary_key_type;
34+
35+
fn id(&self) -> Self::Id {
36+
self.$primary_key_name
37+
}
38+
}
39+
).unwrap();
40+
push(Annotatable::Item(item));
41+
}
42+
}

diesel_codegen/src/lib.in.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod associations;
22
mod attr;
3+
mod identifiable;
34
mod insertable;
45
mod migrations;
56
mod model;

diesel_codegen/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub fn register(reg: &mut syntex::Registry) {
2929
reg.add_attr("feature(custom_attribute)");
3030

3131
reg.add_decorator("derive_Queryable", queryable::expand_derive_queryable);
32+
reg.add_decorator("derive_Identifiable", identifiable::expand_derive_identifiable);
3233
reg.add_decorator("insertable_into", insertable::expand_insert);
3334
reg.add_decorator("changeset_for", update::expand_changeset_for);
3435
reg.add_decorator("has_many", associations::expand_has_many);
@@ -49,6 +50,10 @@ pub fn register(reg: &mut rustc_plugin::Registry) {
4950
intern("derive_Queryable"),
5051
MultiDecorator(Box::new(queryable::expand_derive_queryable))
5152
);
53+
reg.register_syntax_extension(
54+
intern("derive_Identifiable"),
55+
MultiDecorator(Box::new(identifiable::expand_derive_identifiable))
56+
);
5257
reg.register_syntax_extension(
5358
intern("insertable_into"),
5459
MultiDecorator(Box::new(insertable::expand_insert))
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#![feature(custom_derive, custom_attribute, plugin)]
2+
#![plugin(diesel_codegen)]
3+
4+
#[macro_use]
5+
extern crate diesel;
6+
7+
#[derive(Identifiable)] //~ ERROR Could not find a field named `id` on `User`
8+
pub struct User {
9+
name: String,
10+
hair_color: Option<String>,
11+
}
12+
13+
fn main() {}

0 commit comments

Comments
 (0)