Merge pull request #104 from SeaQL/linked-api

Represent several relations between same types
This commit is contained in:
Chris Tsang 2021-09-03 14:13:18 +08:00 committed by GitHub
commit 5cbaa6b699
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 431 additions and 29 deletions

View File

@ -1,5 +1,6 @@
use crate::{ColumnTrait, EntityTrait, IdenStatic};
use sea_query::{DynIden, IntoIden};
use sea_query::{Alias, DynIden, Iden, IntoIden, SeaRc};
use std::fmt;
#[derive(Debug, Clone)]
pub enum Identity {
@ -8,6 +9,25 @@ pub enum Identity {
Ternary(DynIden, DynIden, DynIden),
}
impl Iden for Identity {
fn unquoted(&self, s: &mut dyn fmt::Write) {
match self {
Identity::Unary(iden) => {
write!(s, "{}", iden.to_string()).unwrap();
}
Identity::Binary(iden1, iden2) => {
write!(s, "{}", iden1.to_string()).unwrap();
write!(s, "{}", iden2.to_string()).unwrap();
}
Identity::Ternary(iden1, iden2, iden3) => {
write!(s, "{}", iden1.to_string()).unwrap();
write!(s, "{}", iden2.to_string()).unwrap();
write!(s, "{}", iden3.to_string()).unwrap();
}
}
}
}
pub trait IntoIdentity {
fn into_identity(self) -> Identity;
}
@ -19,6 +39,18 @@ where
fn identity_of(self) -> Identity;
}
impl IntoIdentity for String {
fn into_identity(self) -> Identity {
self.as_str().into_identity()
}
}
impl IntoIdentity for &str {
fn into_identity(self) -> Identity {
Identity::Unary(SeaRc::new(Alias::new(self)))
}
}
impl<T> IntoIdentity for T
where
T: IdenStatic,

View File

@ -1,4 +1,4 @@
use crate::{DbErr, EntityTrait, QueryFilter, QueryResult, Related, Select};
use crate::{DbErr, EntityTrait, Linked, QueryFilter, QueryResult, Related, Select};
pub use sea_query::Value;
use std::fmt::Debug;
@ -16,6 +16,13 @@ pub trait ModelTrait: Clone + Debug {
{
<Self::Entity as Related<R>>::find_related().belongs_to(self)
}
fn find_linked<L>(&self, l: L) -> Select<L::ToEntity>
where
L: Linked<FromEntity = Self::Entity>,
{
l.find_linked()
}
}
pub trait FromQueryResult {

View File

@ -2,8 +2,8 @@ pub use crate::{
error::*, ActiveModelBehavior, ActiveModelTrait, ColumnDef, ColumnTrait, ColumnType,
DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity,
DeriveModel, DerivePrimaryKey, EntityName, EntityTrait, EnumIter, ForeignKeyAction, Iden,
IdenStatic, ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult, Related,
RelationDef, RelationTrait, Select, Value,
IdenStatic, Linked, ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult,
Related, RelationDef, RelationTrait, Select, Value,
};
#[cfg(feature = "with-json")]

View File

@ -30,6 +30,22 @@ where
}
}
pub trait Linked {
type FromEntity: EntityTrait;
type ToEntity: EntityTrait;
fn link(&self) -> Vec<RelationDef>;
fn find_linked(&self) -> Select<Self::ToEntity> {
let mut select = Select::new();
for rel in self.link().into_iter().rev() {
select = select.join_rev(JoinType::InnerJoin, rel);
}
select
}
}
pub struct RelationDef {
pub rel_type: RelationType,
pub from_tbl: TableRef,

View File

@ -264,9 +264,12 @@ impl TryGetable for Decimal {
.map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e)))?;
use rust_decimal::prelude::FromPrimitive;
match val {
Some(v) => Decimal::from_f64(v)
.ok_or_else(|| TryGetError::DbErr(DbErr::Query("Failed to convert f64 into Decimal".to_owned()))),
None => Err(TryGetError::Null)
Some(v) => Decimal::from_f64(v).ok_or_else(|| {
TryGetError::DbErr(DbErr::Query(
"Failed to convert f64 into Decimal".to_owned(),
))
}),
None => Err(TryGetError::Null),
}
}
#[cfg(feature = "mock")]

View File

@ -1,6 +1,6 @@
use crate::{
error::*, query::combine, DatabaseConnection, EntityTrait, FromQueryResult, Iterable,
JsonValue, ModelTrait, Paginator, PrimaryKeyToColumn, QueryResult, Select, SelectTwo,
error::*, DatabaseConnection, EntityTrait, FromQueryResult, IdenStatic, Iterable, JsonValue,
ModelTrait, Paginator, PrimaryKeyToColumn, QueryResult, Select, SelectA, SelectB, SelectTwo,
SelectTwoMany, Statement,
};
use sea_query::SelectStatement;
@ -66,8 +66,8 @@ where
fn from_raw_query_result(res: QueryResult) -> Result<Self::Item, DbErr> {
Ok((
M::from_query_result(&res, combine::SELECT_A)?,
N::from_query_result_optional(&res, combine::SELECT_B)?,
M::from_query_result(&res, SelectA.as_str())?,
N::from_query_result_optional(&res, SelectB.as_str())?,
))
}
}
@ -128,7 +128,7 @@ where
E: EntityTrait,
F: EntityTrait,
{
fn into_model<M, N>(self) -> Selector<SelectTwoModel<M, N>>
pub fn into_model<M, N>(self) -> Selector<SelectTwoModel<M, N>>
where
M: FromQueryResult,
N: FromQueryResult,

View File

@ -1,10 +1,31 @@
use crate::{EntityTrait, IntoSimpleExpr, Iterable, QueryTrait, Select, SelectTwo, SelectTwoMany};
use crate::{
EntityTrait, IdenStatic, IntoSimpleExpr, Iterable, QueryTrait, Select, SelectTwo, SelectTwoMany,
};
use core::marker::PhantomData;
pub use sea_query::JoinType;
use sea_query::{Alias, ColumnRef, Iden, Order, SeaRc, SelectExpr, SelectStatement, SimpleExpr};
pub const SELECT_A: &str = "A_";
pub const SELECT_B: &str = "B_";
macro_rules! select_def {
( $ident: ident, $str: expr ) => {
#[derive(Debug, Clone, Copy)]
pub struct $ident;
impl Iden for $ident {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(s, "{}", self.as_str()).unwrap();
}
}
impl IdenStatic for $ident {
fn as_str(&self) -> &str {
$str
}
}
};
}
select_def!(SelectA, "A_");
select_def!(SelectB, "B_");
impl<E> Select<E>
where
@ -37,7 +58,7 @@ where
where
F: EntityTrait,
{
self = self.apply_alias(SELECT_A);
self = self.apply_alias(SelectA.as_str());
SelectTwo::new(self.into_query())
}
@ -45,7 +66,7 @@ where
where
F: EntityTrait,
{
self = self.apply_alias(SELECT_A);
self = self.apply_alias(SelectA.as_str());
SelectTwoMany::new(self.into_query())
}
}
@ -102,7 +123,7 @@ where
S: QueryTrait<QueryStatement = SelectStatement>,
{
for col in <F::Column as Iterable>::iter() {
let alias = format!("{}{}", SELECT_B, col.to_string().as_str());
let alias = format!("{}{}", SelectB.as_str(), col.as_str());
selector.query().expr(SelectExpr {
expr: col.into_simple_expr(),
alias: Some(SeaRc::new(Alias::new(&alias))),

View File

@ -1,11 +1,9 @@
use crate::{
ColumnTrait, EntityTrait, Identity, IntoSimpleExpr, Iterable, ModelTrait, PrimaryKeyToColumn,
RelationDef,
};
use sea_query::{
Alias, Expr, IntoCondition, SeaRc, SelectExpr, SelectStatement, SimpleExpr, TableRef,
ColumnTrait, EntityTrait, Identity, IntoIdentity, IntoSimpleExpr, Iterable, ModelTrait,
PrimaryKeyToColumn, RelationDef,
};
pub use sea_query::{Condition, ConditionalStatement, DynIden, JoinType, Order, OrderedStatement};
use sea_query::{Expr, IntoCondition, SeaRc, SelectExpr, SelectStatement, SimpleExpr, TableRef};
// LINT: when the column does not appear in tables selected from
// LINT: when there is a group by clause, but some columns don't have aggregate functions
@ -55,13 +53,14 @@ pub trait QuerySelect: Sized {
/// r#"SELECT COUNT("cake"."id") AS "count" FROM "cake""#
/// );
/// ```
fn column_as<C>(mut self, col: C, alias: &str) -> Self
fn column_as<C, I>(mut self, col: C, alias: I) -> Self
where
C: IntoSimpleExpr,
I: IntoIdentity,
{
self.query().expr(SelectExpr {
expr: col.into_simple_expr(),
alias: Some(SeaRc::new(Alias::new(alias))),
alias: Some(SeaRc::new(alias.into_identity())),
});
self
}

View File

@ -1,4 +1,4 @@
use crate::{EntityTrait, QuerySelect, Related, Select, SelectTwo, SelectTwoMany};
use crate::{EntityTrait, Linked, QuerySelect, Related, Select, SelectTwo, SelectTwoMany};
pub use sea_query::JoinType;
impl<E> Select<E>
@ -57,6 +57,19 @@ where
{
self.left_join(r).select_with(r)
}
/// Left Join with a Linked Entity and select both Entity.
pub fn find_also_linked<L, T>(self, l: L) -> SelectTwo<E, T>
where
L: Linked<FromEntity = E, ToEntity = T>,
T: EntityTrait,
{
let mut slf = self;
for rel in l.link() {
slf = slf.join(JoinType::LeftJoin, rel);
}
slf.select_also(T::default())
}
}
#[cfg(test)]
@ -220,4 +233,44 @@ mod tests {
.join(" ")
);
}
#[test]
fn join_10() {
let cake_model = cake::Model {
id: 12,
name: "".to_owned(),
};
assert_eq!(
cake_model
.find_linked(cake::CakeToFilling)
.build(DbBackend::MySql)
.to_string(),
[
r#"SELECT `filling`.`id`, `filling`.`name`"#,
r#"FROM `filling`"#,
r#"INNER JOIN `cake_filling` ON `cake_filling`.`filling_id` = `filling`.`id`"#,
r#"INNER JOIN `cake` ON `cake`.`id` = `cake_filling`.`cake_id`"#,
]
.join(" ")
);
}
#[test]
fn join_11() {
assert_eq!(
cake::Entity::find()
.find_also_linked(cake::CakeToFilling)
.build(DbBackend::MySql)
.to_string(),
[
r#"SELECT `cake`.`id` AS `A_id`, `cake`.`name` AS `A_name`,"#,
r#"`filling`.`id` AS `B_id`, `filling`.`name` AS `B_name`"#,
r#"FROM `cake`"#,
r#"LEFT JOIN `cake_filling` ON `cake`.`id` = `cake_filling`.`cake_id`"#,
r#"LEFT JOIN `filling` ON `cake_filling`.`filling_id` = `filling`.`id`"#,
]
.join(" ")
);
}
}

View File

@ -9,7 +9,7 @@ mod select;
mod traits;
mod update;
// pub use combine::*;
pub use combine::{SelectA, SelectB};
pub use delete::*;
pub use helper::*;
pub use insert::*;

View File

@ -73,4 +73,19 @@ impl Related<super::filling::Entity> for Entity {
}
}
pub struct CakeToFilling;
impl Linked for CakeToFilling {
type FromEntity = Entity;
type ToEntity = super::filling::Entity;
fn link(&self) -> Vec<RelationDef> {
vec![
super::cake_filling::Relation::Cake.def().rev(),
super::cake_filling::Relation::Filling.def(),
]
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -83,4 +83,22 @@ impl Related<super::cake::Entity> for Entity {
}
}
pub struct BakedForCustomer;
impl Linked for BakedForCustomer {
type FromEntity = Entity;
type ToEntity = super::customer::Entity;
fn link(&self) -> Vec<RelationDef> {
vec![
super::cakes_bakers::Relation::Baker.def().rev(),
super::cakes_bakers::Relation::Cake.def(),
super::lineitem::Relation::Cake.def().rev(),
super::lineitem::Relation::Order.def(),
super::order::Relation::Customer.def(),
]
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,13 +1,14 @@
use chrono::offset::Utc;
use rust_decimal::prelude::*;
use rust_decimal_macros::dec;
use sea_orm::{entity::*, query::*, FromQueryResult};
use sea_orm::{entity::*, query::*, DbErr, FromQueryResult};
use uuid::Uuid;
pub mod common;
pub use common::{bakery_chain::*, setup::*, TestContext};
// Run the test locally:
// DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test relational_tests
// DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std-native-tls --test relational_tests
#[sea_orm_macros::test]
#[cfg(any(
feature = "sqlx-mysql",
@ -474,3 +475,240 @@ pub async fn having() {
ctx.delete().await;
}
#[sea_orm_macros::test]
#[cfg(any(
feature = "sqlx-mysql",
feature = "sqlx-sqlite",
feature = "sqlx-postgres"
))]
pub async fn linked() -> Result<(), DbErr> {
use common::bakery_chain::Order;
use sea_orm::{SelectA, SelectB};
let ctx = TestContext::new("test_linked").await;
// SeaSide Bakery
let seaside_bakery = bakery::ActiveModel {
name: Set("SeaSide Bakery".to_owned()),
profit_margin: Set(10.4),
..Default::default()
};
let seaside_bakery_res: InsertResult = Bakery::insert(seaside_bakery).exec(&ctx.db).await?;
// Bob's Baker, Cake & Cake Baker
let baker_bob = baker::ActiveModel {
name: Set("Baker Bob".to_owned()),
contact_details: Set(serde_json::json!({
"mobile": "+61424000000",
"home": "0395555555",
"address": "12 Test St, Testville, Vic, Australia"
})),
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
..Default::default()
};
let baker_bob_res: InsertResult = Baker::insert(baker_bob).exec(&ctx.db).await?;
let mud_cake = cake::ActiveModel {
name: Set("Mud Cake".to_owned()),
price: Set(dec!(10.25)),
gluten_free: Set(false),
serial: Set(Uuid::new_v4()),
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
..Default::default()
};
let mud_cake_res: InsertResult = Cake::insert(mud_cake).exec(&ctx.db).await?;
let bob_cakes_bakers = cakes_bakers::ActiveModel {
cake_id: Set(mud_cake_res.last_insert_id as i32),
baker_id: Set(baker_bob_res.last_insert_id as i32),
..Default::default()
};
CakesBakers::insert(bob_cakes_bakers).exec(&ctx.db).await?;
// Bobby's Baker, Cake & Cake Baker
let baker_bobby = baker::ActiveModel {
name: Set("Baker Bobby".to_owned()),
contact_details: Set(serde_json::json!({
"mobile": "+85212345678",
})),
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
..Default::default()
};
let baker_bobby_res: InsertResult = Baker::insert(baker_bobby).exec(&ctx.db).await?;
let cheese_cake = cake::ActiveModel {
name: Set("Cheese Cake".to_owned()),
price: Set(dec!(20.5)),
gluten_free: Set(false),
serial: Set(Uuid::new_v4()),
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
..Default::default()
};
let cheese_cake_res: InsertResult = Cake::insert(cheese_cake).exec(&ctx.db).await?;
let bobby_cakes_bakers = cakes_bakers::ActiveModel {
cake_id: Set(cheese_cake_res.last_insert_id as i32),
baker_id: Set(baker_bobby_res.last_insert_id as i32),
..Default::default()
};
CakesBakers::insert(bobby_cakes_bakers)
.exec(&ctx.db)
.await?;
let chocolate_cake = cake::ActiveModel {
name: Set("Chocolate Cake".to_owned()),
price: Set(dec!(30.15)),
gluten_free: Set(false),
serial: Set(Uuid::new_v4()),
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
..Default::default()
};
let chocolate_cake_res: InsertResult = Cake::insert(chocolate_cake).exec(&ctx.db).await?;
let bobby_cakes_bakers = cakes_bakers::ActiveModel {
cake_id: Set(chocolate_cake_res.last_insert_id as i32),
baker_id: Set(baker_bobby_res.last_insert_id as i32),
..Default::default()
};
CakesBakers::insert(bobby_cakes_bakers)
.exec(&ctx.db)
.await?;
// Kate's Customer, Order & Line Item
let customer_kate = customer::ActiveModel {
name: Set("Kate".to_owned()),
notes: Set(Some("Loves cheese cake".to_owned())),
..Default::default()
};
let customer_kate_res: InsertResult = Customer::insert(customer_kate).exec(&ctx.db).await?;
let kate_order_1 = order::ActiveModel {
bakery_id: Set(seaside_bakery_res.last_insert_id as i32),
customer_id: Set(customer_kate_res.last_insert_id as i32),
total: Set(dec!(15.10)),
placed_at: Set(Utc::now().naive_utc()),
..Default::default()
};
let kate_order_1_res: InsertResult = Order::insert(kate_order_1).exec(&ctx.db).await?;
lineitem::ActiveModel {
cake_id: Set(cheese_cake_res.last_insert_id as i32),
order_id: Set(kate_order_1_res.last_insert_id as i32),
price: Set(dec!(7.55)),
quantity: Set(2),
..Default::default()
}
.save(&ctx.db)
.await?;
let kate_order_2 = order::ActiveModel {
bakery_id: Set(seaside_bakery_res.last_insert_id as i32),
customer_id: Set(customer_kate_res.last_insert_id as i32),
total: Set(dec!(29.7)),
placed_at: Set(Utc::now().naive_utc()),
..Default::default()
};
let kate_order_2_res: InsertResult = Order::insert(kate_order_2).exec(&ctx.db).await?;
lineitem::ActiveModel {
cake_id: Set(chocolate_cake_res.last_insert_id as i32),
order_id: Set(kate_order_2_res.last_insert_id as i32),
price: Set(dec!(9.9)),
quantity: Set(3),
..Default::default()
}
.save(&ctx.db)
.await?;
// Kara's Customer, Order & Line Item
let customer_kara = customer::ActiveModel {
name: Set("Kara".to_owned()),
notes: Set(Some("Loves all cakes".to_owned())),
..Default::default()
};
let customer_kara_res: InsertResult = Customer::insert(customer_kara).exec(&ctx.db).await?;
let kara_order_1 = order::ActiveModel {
bakery_id: Set(seaside_bakery_res.last_insert_id as i32),
customer_id: Set(customer_kara_res.last_insert_id as i32),
total: Set(dec!(15.10)),
placed_at: Set(Utc::now().naive_utc()),
..Default::default()
};
let kara_order_1_res: InsertResult = Order::insert(kara_order_1).exec(&ctx.db).await?;
lineitem::ActiveModel {
cake_id: Set(mud_cake_res.last_insert_id as i32),
order_id: Set(kara_order_1_res.last_insert_id as i32),
price: Set(dec!(7.55)),
quantity: Set(2),
..Default::default()
}
.save(&ctx.db)
.await?;
let kara_order_2 = order::ActiveModel {
bakery_id: Set(seaside_bakery_res.last_insert_id as i32),
customer_id: Set(customer_kara_res.last_insert_id as i32),
total: Set(dec!(29.7)),
placed_at: Set(Utc::now().naive_utc()),
..Default::default()
};
let kara_order_2_res: InsertResult = Order::insert(kara_order_2).exec(&ctx.db).await?;
lineitem::ActiveModel {
cake_id: Set(cheese_cake_res.last_insert_id as i32),
order_id: Set(kara_order_2_res.last_insert_id as i32),
price: Set(dec!(9.9)),
quantity: Set(3),
..Default::default()
}
.save(&ctx.db)
.await?;
#[derive(Debug, FromQueryResult, PartialEq)]
struct BakerLite {
name: String,
}
#[derive(Debug, FromQueryResult, PartialEq)]
struct CustomerLite {
name: String,
}
let baked_for_customers: Vec<(BakerLite, Option<CustomerLite>)> = Baker::find()
.find_also_linked(baker::BakedForCustomer)
.select_only()
.column_as(baker::Column::Name, (SelectA, baker::Column::Name))
.column_as(customer::Column::Name, (SelectB, customer::Column::Name))
.group_by(baker::Column::Id)
.group_by(customer::Column::Id)
.group_by(baker::Column::Name)
.group_by(customer::Column::Name)
.order_by_asc(baker::Column::Id)
.order_by_asc(customer::Column::Id)
.into_model()
.all(&ctx.db)
.await?;
assert_eq!(
baked_for_customers,
vec![
(
BakerLite {
name: "Baker Bob".to_owned(),
},
Some(CustomerLite {
name: "Kara".to_owned(),
})
),
(
BakerLite {
name: "Baker Bobby".to_owned(),
},
Some(CustomerLite {
name: "Kate".to_owned(),
})
),
(
BakerLite {
name: "Baker Bobby".to_owned(),
},
Some(CustomerLite {
name: "Kara".to_owned(),
})
),
]
);
ctx.delete().await;
Ok(())
}