Merge branch 'master' into rename-col-name-n-variant

This commit is contained in:
Billy Chan 2021-09-30 10:48:43 +08:00
commit 54150382b9
No known key found for this signature in database
GPG Key ID: A2D690CAC7DF3CC7
17 changed files with 251 additions and 69 deletions

View File

@ -1,5 +1,7 @@
# Architecture
> Let's dive under the Sea 🤿
To understand the architecture of SeaORM, let's discuss what is an ORM. ORM exists to provide abstractions over common operations you would do against a database and hide the implementation details like the actual SQL queries.
With a good ORM, you shouldn't bother to look under the API surface. Until you do. I hear you say *'abstraction leaks'*, and yes, it does.

View File

@ -239,7 +239,7 @@ fruit::Entity::delete_many()
1. [Design](https://github.com/SeaQL/sea-orm/tree/master/DESIGN.md)
1. [Architecture](https://github.com/SeaQL/sea-orm/tree/master/ARCHITECTURE.md)
1. [Compare with Diesel](https://www.sea-ql.org/SeaORM/docs/internal-design/diesel)
1. [Change Log](https://github.com/SeaQL/sea-orm/tree/master/CHANGELOG.md)
## License

View File

@ -293,7 +293,7 @@ pub trait EntityTrait: EntityName {
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values(
/// DbBackend::Postgres, r#"INSERT INTO "cake" ("name") VALUES ($1)"#, vec!["Apple Pie".into()]
/// DbBackend::Postgres, r#"INSERT INTO "cake" ("name") VALUES ($1) RETURNING "id""#, vec!["Apple Pie".into()]
/// )]);
/// ```
fn insert<A>(model: A) -> Insert<A>
@ -344,7 +344,7 @@ pub trait EntityTrait: EntityName {
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values(
/// DbBackend::Postgres, r#"INSERT INTO "cake" ("name") VALUES ($1), ($2)"#,
/// DbBackend::Postgres, r#"INSERT INTO "cake" ("name") VALUES ($1), ($2) RETURNING "id""#,
/// vec!["Apple Pie".into(), "Orange Scone".into()]
/// )]);
/// ```

View File

@ -1,5 +1,7 @@
use crate::{EntityTrait, QuerySelect, RelationDef, Select};
use sea_query::JoinType;
use crate::{
join_tbl_on_condition, unpack_table_ref, EntityTrait, QuerySelect, RelationDef, Select,
};
use sea_query::{Alias, IntoIden, JoinType, SeaRc};
pub type LinkDef = RelationDef;
@ -12,8 +14,20 @@ pub trait Linked {
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);
for (i, rel) in self.link().into_iter().rev().enumerate() {
let from_tbl = Alias::new(&format!("r{}", i)).into_iden();
let to_tbl = if i > 0 {
Alias::new(&format!("r{}", i - 1)).into_iden()
} else {
unpack_table_ref(&rel.to_tbl)
};
select.query().join_as(
JoinType::InnerJoin,
unpack_table_ref(&rel.from_tbl),
SeaRc::clone(&from_tbl),
join_tbl_on_condition(from_tbl, to_tbl, rel.from_col, rel.to_col),
);
}
select
}

View File

@ -24,7 +24,8 @@ pub trait ModelTrait: Clone + Send + Debug {
where
L: Linked<FromEntity = Self::Entity>,
{
l.find_linked().belongs_to(self)
let tbl_alias = &format!("r{}", l.link().len() - 1);
l.find_linked().belongs_to_tbl_alias(self, tbl_alias)
}
}

View File

@ -1,5 +1,5 @@
use crate::{
error::*, ActiveModelTrait, DatabaseConnection, EntityTrait, Insert, PrimaryKeyTrait,
error::*, ActiveModelTrait, DatabaseConnection, DbBackend, EntityTrait, Insert, PrimaryKeyTrait,
Statement, TryFromU64,
};
use sea_query::InsertStatement;
@ -37,8 +37,7 @@ where
// TODO: extract primary key's value from query
// so that self is dropped before entering await
let mut query = self.query;
#[cfg(feature = "sqlx-postgres")]
if let DatabaseConnection::SqlxPostgresPoolConnection(_) = db {
if db.get_database_backend() == DbBackend::Postgres {
use crate::{sea_query::Query, Iterable};
if <A::Entity as EntityTrait>::PrimaryKey::iter().count() > 0 {
query.returning(
@ -86,14 +85,13 @@ where
{
type PrimaryKey<A> = <<A as ActiveModelTrait>::Entity as EntityTrait>::PrimaryKey;
type ValueTypeOf<A> = <PrimaryKey<A> as PrimaryKeyTrait>::ValueType;
let last_insert_id = match db {
#[cfg(feature = "sqlx-postgres")]
DatabaseConnection::SqlxPostgresPoolConnection(conn) => {
let last_insert_id = match db.get_database_backend() {
DbBackend::Postgres => {
use crate::{sea_query::Iden, Iterable};
let cols = PrimaryKey::<A>::iter()
.map(|col| col.to_string())
.collect::<Vec<_>>();
let res = conn.query_one(statement).await?.unwrap();
let res = db.query_one(statement).await?.unwrap();
res.try_get_many("", cols.as_ref()).unwrap_or_default()
}
_ => {

View File

@ -1,4 +1,4 @@
use crate::{error::*, DatabaseConnection, SelectorTrait};
use crate::{error::*, DatabaseConnection, DbBackend, SelectorTrait};
use async_stream::stream;
use futures::Stream;
use sea_query::{Alias, Expr, SelectStatement};
@ -63,11 +63,8 @@ where
Some(res) => res,
None => return Ok(0),
};
let num_items = match self.db {
#[cfg(feature = "sqlx-postgres")]
DatabaseConnection::SqlxPostgresPoolConnection(_) => {
result.try_get::<i64>("", "num_items")? as usize
}
let num_items = match builder {
DbBackend::Postgres => result.try_get::<i64>("", "num_items")? as usize,
_ => result.try_get::<i32>("", "num_items")? as usize,
};
Ok(num_items)
@ -192,7 +189,7 @@ mod tests {
(db, vec![page1, page2, page3])
}
fn setup_num_items() -> (DatabaseConnection, i32) {
fn setup_num_items() -> (DatabaseConnection, i64) {
let num_items = 3;
let db = MockDatabase::new(DbBackend::Postgres)
.append_query_results(vec![vec![maplit::btreemap! {

View File

@ -335,7 +335,7 @@
//!
//! 1. [Design](https://github.com/SeaQL/sea-orm/tree/master/DESIGN.md)
//! 1. [Architecture](https://github.com/SeaQL/sea-orm/tree/master/ARCHITECTURE.md)
//! 1. [Compare with Diesel](https://www.sea-ql.org/SeaORM/docs/internal-design/diesel)
//! 1. [Change Log](https://github.com/SeaQL/sea-orm/tree/master/CHANGELOG.md)
//!
//! ## License
//!

View File

@ -2,10 +2,11 @@ use crate::{
ColumnTrait, EntityTrait, Identity, IntoIdentity, IntoSimpleExpr, Iterable, ModelTrait,
PrimaryKeyToColumn, RelationDef,
};
pub use sea_query::{Condition, ConditionalStatement, DynIden, JoinType, Order, OrderedStatement};
use sea_query::{
Expr, IntoCondition, LockType, SeaRc, SelectExpr, SelectStatement, SimpleExpr, TableRef,
Alias, Expr, Iden, IntoCondition, LockType, SeaRc, SelectExpr, SelectStatement, SimpleExpr,
TableRef,
};
pub use sea_query::{Condition, ConditionalStatement, DynIden, JoinType, Order, OrderedStatement};
// 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
@ -268,6 +269,48 @@ pub trait QueryFilter: Sized {
/// "SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`id` = 4 OR `cake`.`id` = 5"
/// );
/// ```
///
/// Add a runtime-built condition tree.
/// ```
/// use sea_orm::{entity::*, query::*, tests_cfg::cake, DbBackend};
/// struct Input {
/// name: Option<String>,
/// }
/// let input = Input { name: Some("cheese".to_owned()) };
///
/// let mut conditions = Condition::all();
/// if let Some(name) = input.name {
/// conditions = conditions.add(cake::Column::Name.contains(&name));
/// }
///
/// assert_eq!(
/// cake::Entity::find()
/// .filter(conditions)
/// .build(DbBackend::MySql)
/// .to_string(),
/// "SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`name` LIKE '%cheese%'"
/// );
/// ```
///
/// Add a runtime-built condition tree, functional-way.
/// ```
/// use sea_orm::{entity::*, query::*, tests_cfg::cake, DbBackend};
/// struct Input {
/// name: Option<String>,
/// }
/// let input = Input { name: Some("cheese".to_owned()) };
///
/// assert_eq!(
/// cake::Entity::find()
/// .filter(
/// Condition::all()
/// .add_option(input.name.map(|n| cake::Column::Name.contains(&n)))
/// )
/// .build(DbBackend::MySql)
/// .to_string(),
/// "SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`name` LIKE '%cheese%'"
/// );
/// ```
fn filter<F>(mut self, filter: F) -> Self
where
F: IntoCondition,
@ -287,14 +330,35 @@ pub trait QueryFilter: Sized {
}
self
}
fn belongs_to_tbl_alias<M>(mut self, model: &M, tbl_alias: &str) -> Self
where
M: ModelTrait,
{
for key in <M::Entity as EntityTrait>::PrimaryKey::iter() {
let col = key.into_column();
let expr = Expr::tbl(Alias::new(tbl_alias), col).eq(model.get(col));
self = self.filter(expr);
}
self
}
}
fn join_condition(rel: RelationDef) -> SimpleExpr {
pub(crate) fn join_condition(rel: RelationDef) -> SimpleExpr {
let from_tbl = unpack_table_ref(&rel.from_tbl);
let to_tbl = unpack_table_ref(&rel.to_tbl);
let owner_keys = rel.from_col;
let foreign_keys = rel.to_col;
join_tbl_on_condition(from_tbl, to_tbl, owner_keys, foreign_keys)
}
pub(crate) fn join_tbl_on_condition(
from_tbl: SeaRc<dyn Iden>,
to_tbl: SeaRc<dyn Iden>,
owner_keys: Identity,
foreign_keys: Identity,
) -> SimpleExpr {
match (owner_keys, foreign_keys) {
(Identity::Unary(o1), Identity::Unary(f1)) => {
Expr::tbl(SeaRc::clone(&from_tbl), o1).equals(SeaRc::clone(&to_tbl), f1)

View File

@ -74,8 +74,9 @@ where
#[cfg(test)]
mod tests {
use crate::tests_cfg::{cake, cake_filling, cake_filling_price, filling, fruit};
use crate::tests_cfg::{cake, cake_filling, cake_filling_price, entity_linked, filling, fruit};
use crate::{ColumnTrait, DbBackend, EntityTrait, ModelTrait, QueryFilter, QueryTrait};
use pretty_assertions::assert_eq;
#[test]
fn join_1() {
@ -188,7 +189,7 @@ mod tests {
assert_eq!(
find_filling.build(DbBackend::MySql).to_string(),
[
"SELECT `filling`.`id`, `filling`.`name` FROM `filling`",
"SELECT `filling`.`id`, `filling`.`name`, `filling`.`vendor_id` FROM `filling`",
"INNER JOIN `cake_filling` ON `cake_filling`.`filling_id` = `filling`.`id`",
"INNER JOIN `cake` ON `cake`.`id` = `cake_filling`.`cake_id`",
]
@ -243,15 +244,15 @@ mod tests {
assert_eq!(
cake_model
.find_linked(cake::CakeToFilling)
.find_linked(entity_linked::CakeToFilling)
.build(DbBackend::MySql)
.to_string(),
[
r#"SELECT `filling`.`id`, `filling`.`name`"#,
r#"SELECT `filling`.`id`, `filling`.`name`, `filling`.`vendor_id`"#,
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`"#,
r#"WHERE `cake`.`id` = 12"#,
r#"INNER JOIN `cake_filling` AS `r0` ON `r0`.`filling_id` = `filling`.`id`"#,
r#"INNER JOIN `cake` AS `r1` ON `r1`.`id` = `r0`.`cake_id`"#,
r#"WHERE `r1`.`id` = 12"#,
]
.join(" ")
);
@ -259,14 +260,38 @@ mod tests {
#[test]
fn join_11() {
let cake_model = cake::Model {
id: 18,
name: "".to_owned(),
};
assert_eq!(
cake_model
.find_linked(entity_linked::CakeToFillingVendor)
.build(DbBackend::MySql)
.to_string(),
[
r#"SELECT `vendor`.`id`, `vendor`.`name`"#,
r#"FROM `vendor`"#,
r#"INNER JOIN `filling` AS `r0` ON `r0`.`vendor_id` = `vendor`.`id`"#,
r#"INNER JOIN `cake_filling` AS `r1` ON `r1`.`filling_id` = `r0`.`id`"#,
r#"INNER JOIN `cake` AS `r2` ON `r2`.`id` = `r1`.`cake_id`"#,
r#"WHERE `r2`.`id` = 18"#,
]
.join(" ")
);
}
#[test]
fn join_12() {
assert_eq!(
cake::Entity::find()
.find_also_linked(cake::CakeToFilling)
.find_also_linked(entity_linked::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#"`filling`.`id` AS `B_id`, `filling`.`name` AS `B_name`, `filling`.`vendor_id` AS `B_vendor_id`"#,
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`"#,
@ -274,4 +299,23 @@ mod tests {
.join(" ")
);
}
#[test]
fn join_13() {
assert_eq!(
cake::Entity::find()
.find_also_linked(entity_linked::CakeToFillingVendor)
.build(DbBackend::MySql)
.to_string(),
[
r#"SELECT `cake`.`id` AS `A_id`, `cake`.`name` AS `A_name`,"#,
r#"`vendor`.`id` AS `B_id`, `vendor`.`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`"#,
r#"LEFT JOIN `vendor` ON `filling`.`vendor_id` = `vendor`.`id`"#,
]
.join(" ")
);
}
}

View File

@ -32,20 +32,4 @@ impl Related<super::filling::Entity> for Entity {
}
}
#[derive(Debug)]
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

@ -75,20 +75,4 @@ impl Related<super::filling::Entity> for Entity {
}
}
#[derive(Debug)]
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

@ -0,0 +1,34 @@
use crate::entity::prelude::*;
#[derive(Debug)]
pub struct CakeToFilling;
impl Linked for CakeToFilling {
type FromEntity = super::cake::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(),
]
}
}
#[derive(Debug)]
pub struct CakeToFillingVendor;
impl Linked for CakeToFillingVendor {
type FromEntity = super::cake::Entity;
type ToEntity = super::vendor::Entity;
fn link(&self) -> Vec<RelationDef> {
vec![
super::cake_filling::Relation::Cake.def().rev(),
super::cake_filling::Relation::Filling.def(),
super::filling::Relation::Vendor.def(),
]
}
}

View File

@ -9,6 +9,7 @@ pub struct Entity;
pub struct Model {
pub id: i32,
pub name: String,
pub vendor_id: Option<i32>,
#[sea_orm(ignore)]
pub ignored_attr: i32,
}
@ -18,6 +19,7 @@ pub struct Model {
pub enum Column {
Id,
Name,
VendorId,
}
// Then, customize each column names here.
@ -46,7 +48,9 @@ impl PrimaryKeyTrait for PrimaryKey {
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
pub enum Relation {
Vendor,
}
impl ColumnTrait for Column {
type EntityName = Entity;
@ -55,13 +59,19 @@ impl ColumnTrait for Column {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Name => ColumnType::String(None).def(),
Self::VendorId => ColumnType::Integer.def().nullable(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!()
match self {
Self::Vendor => Entity::belongs_to(super::vendor::Entity)
.from(Column::VendorId)
.to(super::vendor::Column::Id)
.into(),
}
}
}

View File

@ -4,8 +4,10 @@ pub mod cake;
pub mod cake_expanded;
pub mod cake_filling;
pub mod cake_filling_price;
pub mod entity_linked;
pub mod filling;
pub mod fruit;
pub mod vendor;
pub use cake::Entity as Cake;
pub use cake_expanded::Entity as CakeExpanded;
@ -13,3 +15,4 @@ pub use cake_filling::Entity as CakeFilling;
pub use cake_filling_price::Entity as CakeFillingPrice;
pub use filling::Entity as Filling;
pub use fruit::Entity as Fruit;
pub use vendor::Entity as Vendor;

27
src/tests_cfg/vendor.rs Normal file
View File

@ -0,0 +1,27 @@
use crate as sea_orm;
use crate::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "vendor")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!()
}
}
impl Related<super::filling::Entity> for Entity {
fn to() -> RelationDef {
super::filling::Relation::Vendor.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -708,6 +708,26 @@ pub async fn linked() -> Result<(), DbErr> {
]
);
let baker_bob = Baker::find()
.filter(baker::Column::Id.eq(1))
.one(&ctx.db)
.await?
.unwrap();
let baker_bob_customers = baker_bob
.find_linked(baker::BakedForCustomer)
.all(&ctx.db)
.await?;
assert_eq!(
baker_bob_customers,
vec![customer::Model {
id: 2,
name: "Kara".to_owned(),
notes: Some("Loves all cakes".to_owned()),
}]
);
ctx.delete().await;
Ok(())