Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PIP] Simple data loader #1238

Merged
merged 16 commits into from
Dec 28, 2022
5 changes: 3 additions & 2 deletions src/entity/prelude.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
pub use crate::{
error::*, ActiveEnum, ActiveModelBehavior, ActiveModelTrait, ColumnDef, ColumnTrait,
ColumnType, CursorTrait, DatabaseConnection, DbConn, EntityName, EntityTrait, EnumIter,
ForeignKeyAction, Iden, IdenStatic, Linked, ModelTrait, PaginatorTrait, PrimaryKeyToColumn,
PrimaryKeyTrait, QueryFilter, QueryResult, Related, RelationDef, RelationTrait, Select, Value,
ForeignKeyAction, Iden, IdenStatic, Linked, LoaderTrait, ModelTrait, PaginatorTrait,
PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult, Related, RelationDef,
RelationTrait, Select, Value,
};

#[cfg(feature = "macros")]
Expand Down
339 changes: 339 additions & 0 deletions src/query/loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
use crate::{
ColumnTrait, Condition, ConnectionTrait, DbErr, EntityTrait, Identity, ModelTrait, QueryFilter,
Related, RelationType, Select,
};
use async_trait::async_trait;
use sea_query::{Expr, IntoColumnRef, SimpleExpr, ValueTuple};
use std::{collections::HashMap, str::FromStr};

/// A trait for basic Dataloader
#[async_trait]
pub trait LoaderTrait {
/// Source model
type Model: ModelTrait;

/// Used to eager load has_one relations
async fn load_one<R, C>(&self, stmt: Select<R>, db: &C) -> Result<Vec<Option<R::Model>>, DbErr>
where
C: ConnectionTrait,
R: EntityTrait,
R::Model: Send + Sync,
<<Self as LoaderTrait>::Model as ModelTrait>::Entity: Related<R>;

/// Used to eager load has_many relations
async fn load_many<R, C>(&self, stmt: Select<R>, db: &C) -> Result<Vec<Vec<R::Model>>, DbErr>
where
C: ConnectionTrait,
R: EntityTrait,
R::Model: Send + Sync,
<<Self as LoaderTrait>::Model as ModelTrait>::Entity: Related<R>;
}

#[async_trait::async_trait]
tyt2y3 marked this conversation as resolved.
Show resolved Hide resolved
impl<M> LoaderTrait for Vec<M>
where
M: ModelTrait,
Vec<M>: Sync,
{
type Model = M;

async fn load_one<R, C>(&self, stmt: Select<R>, db: &C) -> Result<Vec<Option<R::Model>>, DbErr>
where
C: ConnectionTrait,
R: EntityTrait,
R::Model: Send + Sync,
<<Self as LoaderTrait>::Model as ModelTrait>::Entity: Related<R>,
{
let rel_def = <<<Self as LoaderTrait>::Model as ModelTrait>::Entity as Related<R>>::to();

// we verify that is has_one relation
match (rel_def).rel_type {
RelationType::HasOne => (),
RelationType::HasMany => {
return Err(DbErr::Type("Relation is HasMany instead of HasOne".into()))
}
}

let keys: Vec<ValueTuple> = self
.iter()
.map(|model: &M| extract_key(&rel_def.from_col, model))
.collect();

let condition = prepare_condition::<<R as EntityTrait>::Model>(&rel_def.to_col, &keys);

let stmt = <Select<R> as QueryFilter>::filter(stmt, condition);

let data = stmt.all(db).await?;

let hashmap: HashMap<String, <R as EntityTrait>::Model> = data.into_iter().fold(
HashMap::<String, <R as EntityTrait>::Model>::new(),
|mut acc: HashMap<String, <R as EntityTrait>::Model>,
value: <R as EntityTrait>::Model| {
{
let key = extract_key(&rel_def.to_col, &value);

acc.insert(format!("{:?}", key), value);
}

acc
},
);

let result: Vec<Option<<R as EntityTrait>::Model>> = keys
.iter()
.map(|key| hashmap.get(&format!("{:?}", key)).cloned())
.collect();

Ok(result)
}

async fn load_many<R, C>(&self, stmt: Select<R>, db: &C) -> Result<Vec<Vec<R::Model>>, DbErr>
where
C: ConnectionTrait,
R: EntityTrait,
R::Model: Send + Sync,
<<Self as LoaderTrait>::Model as ModelTrait>::Entity: Related<R>,
{
let rel_def = <<<Self as LoaderTrait>::Model as ModelTrait>::Entity as Related<R>>::to();

// we verify that is has_many relation
match (rel_def).rel_type {
RelationType::HasMany => (),
RelationType::HasOne => {
return Err(DbErr::Type("Relation is HasOne instead of HasMany".into()))
}
}

let keys: Vec<ValueTuple> = self
.iter()
.map(|model: &M| extract_key(&rel_def.from_col, model))
.collect();

let condition = prepare_condition::<<R as EntityTrait>::Model>(&rel_def.to_col, &keys);

let stmt = <Select<R> as QueryFilter>::filter(stmt, condition);

let data = stmt.all(db).await?;

let mut hashmap: HashMap<String, Vec<<R as EntityTrait>::Model>> =
keys.iter()
.fold(HashMap::new(), |mut acc, key: &ValueTuple| {
acc.insert(format!("{:?}", key), Vec::new());

acc
});

data.into_iter()
.for_each(|value: <R as EntityTrait>::Model| {
let key = extract_key(&rel_def.to_col, &value);

let vec = hashmap
.get_mut(&format!("{:?}", key))
.expect("Failed at finding key on hashmap");

vec.push(value);
});

let result: Vec<Vec<R::Model>> = keys
.iter()
.map(|key: &ValueTuple| {
hashmap
.get(&format!("{:?}", key))
.cloned()
.unwrap_or_default()
})
.collect();

Ok(result)
}
}

fn extract_key<Model>(target_col: &Identity, model: &Model) -> ValueTuple
where
Model: ModelTrait,
{
match target_col {
Identity::Unary(a) => {
let column_a =
<<<Model as ModelTrait>::Entity as EntityTrait>::Column as FromStr>::from_str(
&a.to_string(),
)
.unwrap_or_else(|_| panic!("Failed at mapping string to column A:1"));
ValueTuple::One(model.get(column_a))
}
Identity::Binary(a, b) => {
let column_a =
<<<Model as ModelTrait>::Entity as EntityTrait>::Column as FromStr>::from_str(
&a.to_string(),
)
.unwrap_or_else(|_| panic!("Failed at mapping string to column A:2"));
let column_b =
<<<Model as ModelTrait>::Entity as EntityTrait>::Column as FromStr>::from_str(
&b.to_string(),
)
.unwrap_or_else(|_| panic!("Failed at mapping string to column B:2"));
ValueTuple::Two(model.get(column_a), model.get(column_b))
}
Identity::Ternary(a, b, c) => {
let column_a =
<<<Model as ModelTrait>::Entity as EntityTrait>::Column as FromStr>::from_str(
&a.to_string(),
)
.unwrap_or_else(|_| panic!("Failed at mapping string to column A:3"));
let column_b =
<<<Model as ModelTrait>::Entity as EntityTrait>::Column as FromStr>::from_str(
&b.to_string(),
)
.unwrap_or_else(|_| panic!("Failed at mapping string to column B:3"));
let column_c =
<<<Model as ModelTrait>::Entity as EntityTrait>::Column as FromStr>::from_str(
&c.to_string(),
)
.unwrap_or_else(|_| panic!("Failed at mapping string to column C:3"));
ValueTuple::Three(
model.get(column_a),
model.get(column_b),
model.get(column_c),
)
}
}
}

fn prepare_condition<M>(col: &Identity, keys: &[ValueTuple]) -> Condition
where
M: ModelTrait,
{
match col {
Identity::Unary(column_a) => {
let column_a: <M::Entity as EntityTrait>::Column =
<<M::Entity as EntityTrait>::Column as FromStr>::from_str(&column_a.to_string())
.unwrap_or_else(|_| panic!("Failed at mapping string to column *A:1"));
Condition::all().add(ColumnTrait::is_in(
&column_a,
keys.iter().cloned().flatten(),
))
}
Identity::Binary(column_a, column_b) => {
let column_a: <M::Entity as EntityTrait>::Column =
<<M::Entity as EntityTrait>::Column as FromStr>::from_str(&column_a.to_string())
.unwrap_or_else(|_| panic!("Failed at mapping string to column *A:2"));
let column_b: <M::Entity as EntityTrait>::Column =
<<M::Entity as EntityTrait>::Column as FromStr>::from_str(&column_b.to_string())
.unwrap_or_else(|_| panic!("Failed at mapping string to column *B:2"));
Condition::all().add(
Expr::tuple([
SimpleExpr::Column(column_a.into_column_ref()),
SimpleExpr::Column(column_b.into_column_ref()),
])
.in_tuples(keys.iter().cloned()),
)
}
Identity::Ternary(column_a, column_b, column_c) => {
let column_a: <M::Entity as EntityTrait>::Column =
<<M::Entity as EntityTrait>::Column as FromStr>::from_str(&column_a.to_string())
.unwrap_or_else(|_| panic!("Failed at mapping string to column *A:3"));
let column_b: <M::Entity as EntityTrait>::Column =
<<M::Entity as EntityTrait>::Column as FromStr>::from_str(&column_b.to_string())
.unwrap_or_else(|_| panic!("Failed at mapping string to column *B:3"));
let column_c: <M::Entity as EntityTrait>::Column =
<<M::Entity as EntityTrait>::Column as FromStr>::from_str(&column_c.to_string())
.unwrap_or_else(|_| panic!("Failed at mapping string to column *C:3"));
Condition::all().add(
Expr::tuple([
SimpleExpr::Column(column_a.into_column_ref()),
SimpleExpr::Column(column_b.into_column_ref()),
SimpleExpr::Column(column_c.into_column_ref()),
])
.in_tuples(keys.iter().cloned()),
)
}
}
}

#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_load_one() {
use crate::{
entity::prelude::*, tests_cfg::*, DbBackend, IntoMockRow, LoaderTrait, MockDatabase,
};

let db = MockDatabase::new(DbBackend::Postgres)
.append_query_results(vec![vec![
cake::Model {
id: 1,
name: "New York Cheese".to_owned(),
}
.into_mock_row(),
cake::Model {
id: 2,
name: "London Cheese".to_owned(),
}
.into_mock_row(),
]])
.into_connection();

let fruits = vec![fruit::Model {
id: 1,
name: "Apple".to_owned(),
cake_id: Some(1),
}];

let cakes = fruits
.load_one(cake::Entity::find(), &db)
.await
.expect("Should return something");

assert_eq!(
cakes,
vec![Some(cake::Model {
id: 1,
name: "New York Cheese".to_owned(),
})]
);
}

#[tokio::test]
async fn test_load_many() {
use crate::{
entity::prelude::*, tests_cfg::*, DbBackend, IntoMockRow, LoaderTrait, MockDatabase,
};

let db = MockDatabase::new(DbBackend::Postgres)
.append_query_results(vec![vec![fruit::Model {
id: 1,
name: "Apple".to_owned(),
cake_id: Some(1),
}
.into_mock_row()]])
.into_connection();

let cakes = vec![
cake::Model {
id: 1,
name: "New York Cheese".to_owned(),
},
cake::Model {
id: 2,
name: "London Cheese".to_owned(),
},
];

let fruits = cakes
.load_many(fruit::Entity::find(), &db)
.await
.expect("Should return something");

assert_eq!(
fruits,
vec![
vec![fruit::Model {
id: 1,
name: "Apple".to_owned(),
cake_id: Some(1),
}],
vec![]
]
);
}
}
2 changes: 2 additions & 0 deletions src/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod insert;
mod join;
#[cfg(feature = "with-json")]
mod json;
mod loader;
mod select;
mod traits;
mod update;
Expand All @@ -17,6 +18,7 @@ pub use insert::*;
pub use join::*;
#[cfg(feature = "with-json")]
pub use json::*;
pub use loader::*;
pub use select::*;
pub use traits::*;
pub use update::*;
Expand Down
Loading