sea_orm/entity/relation.rs
1use crate::{
2 EntityTrait, Identity, IdentityOf, Iterable, QuerySelect, Select, join_tbl_on_condition,
3};
4use core::marker::PhantomData;
5use sea_query::{
6 Condition, ConditionType, DynIden, ForeignKeyCreateStatement, IntoIden, JoinType,
7 TableForeignKey, TableRef,
8};
9use std::{fmt::Debug, sync::Arc};
10
11/// Defines the type of relationship
12#[derive(Clone, Debug, PartialEq, Eq, Hash)]
13pub enum RelationType {
14 /// An Entity has one relationship
15 HasOne,
16 /// An Entity has many relationships
17 HasMany,
18}
19
20/// Action to perform on a foreign key whenever there are changes
21/// to an ActiveModel
22pub type ForeignKeyAction = sea_query::ForeignKeyAction;
23
24/// Defines the relations of an Entity
25pub trait RelationTrait: Iterable + Debug + 'static {
26 /// Creates a [`RelationDef`]
27 fn def(&self) -> RelationDef;
28
29 /// Name of the relation enum
30 fn name(&self) -> String {
31 format!("{self:?}")
32 }
33}
34
35/// Checks if Entities are related
36pub trait Related<R>
37where
38 R: EntityTrait,
39{
40 /// Check if an entity is related to another entity
41 fn to() -> RelationDef;
42
43 /// Check if an entity is related through another entity
44 fn via() -> Option<RelationDef> {
45 None
46 }
47
48 /// Find related Entities
49 fn find_related() -> Select<R> {
50 Select::<R>::new().join_join_rev(JoinType::InnerJoin, Self::to(), Self::via())
51 }
52}
53
54/// Defines a relationship
55#[derive(derive_more::Debug, Clone)]
56pub struct RelationDef {
57 /// The type of relationship defined in [RelationType]
58 pub rel_type: RelationType,
59 /// Reference from another Entity
60 pub from_tbl: TableRef,
61 /// Reference to another Entity
62 pub to_tbl: TableRef,
63 /// Reference to from a Column
64 pub from_col: Identity,
65 /// Reference to another column
66 pub to_col: Identity,
67 /// Defines the owner of the Relation
68 pub is_owner: bool,
69 /// Specifies if the foreign key should be skipped
70 pub skip_fk: bool,
71 /// Defines an operation to be performed on a Foreign Key when a
72 /// `DELETE` Operation is performed
73 pub on_delete: Option<ForeignKeyAction>,
74 /// Defines an operation to be performed on a Foreign Key when a
75 /// `UPDATE` Operation is performed
76 pub on_update: Option<ForeignKeyAction>,
77 /// Custom join ON condition
78 #[debug("{}", on_condition.is_some())]
79 pub on_condition: Option<Arc<dyn Fn(DynIden, DynIden) -> Condition + Send + Sync>>,
80 /// The name of foreign key constraint
81 pub fk_name: Option<String>,
82 /// Condition type of join on expression
83 pub condition_type: ConditionType,
84}
85
86/// Idiomatically generate the join condition.
87///
88/// This allows using [RelationDef] directly where [`sea_query`] expects an [`IntoCondition`].
89///
90/// ## Examples
91///
92/// ```
93/// use sea_orm::tests_cfg::{cake, fruit};
94/// use sea_orm::{entity::*, sea_query::*};
95///
96/// let query = Query::select()
97/// .from(fruit::Entity)
98/// .inner_join(cake::Entity, fruit::Relation::Cake.def())
99/// .to_owned();
100///
101/// assert_eq!(
102/// query.to_string(MysqlQueryBuilder),
103/// r#"SELECT FROM `fruit` INNER JOIN `cake` ON `fruit`.`cake_id` = `cake`.`id`"#
104/// );
105/// assert_eq!(
106/// query.to_string(PostgresQueryBuilder),
107/// r#"SELECT FROM "fruit" INNER JOIN "cake" ON "fruit"."cake_id" = "cake"."id""#
108/// );
109/// assert_eq!(
110/// query.to_string(SqliteQueryBuilder),
111/// r#"SELECT FROM "fruit" INNER JOIN "cake" ON "fruit"."cake_id" = "cake"."id""#
112/// );
113/// ```
114impl From<RelationDef> for Condition {
115 fn from(mut rel: RelationDef) -> Condition {
116 // Use table alias (if any) to construct the join condition
117 let from_tbl = match rel.from_tbl.sea_orm_table_alias() {
118 Some(alias) => alias,
119 None => rel.from_tbl.sea_orm_table(),
120 };
121 let to_tbl = match rel.to_tbl.sea_orm_table_alias() {
122 Some(alias) => alias,
123 None => rel.to_tbl.sea_orm_table(),
124 };
125 let owner_keys = rel.from_col;
126 let foreign_keys = rel.to_col;
127
128 let mut condition = match rel.condition_type {
129 ConditionType::All => Condition::all(),
130 ConditionType::Any => Condition::any(),
131 };
132
133 condition = condition.add(join_tbl_on_condition(
134 from_tbl.clone(),
135 to_tbl.clone(),
136 owner_keys,
137 foreign_keys,
138 ));
139 if let Some(f) = rel.on_condition.take() {
140 condition = condition.add(f(from_tbl.clone(), to_tbl.clone()));
141 }
142
143 condition
144 }
145}
146
147/// Defines a helper to build a relation
148#[derive(derive_more::Debug)]
149pub struct RelationBuilder<E, R>
150where
151 E: EntityTrait,
152 R: EntityTrait,
153{
154 entities: PhantomData<(E, R)>,
155 rel_type: RelationType,
156 from_tbl: TableRef,
157 to_tbl: TableRef,
158 from_col: Option<Identity>,
159 to_col: Option<Identity>,
160 is_owner: bool,
161 skip_fk: bool,
162 on_delete: Option<ForeignKeyAction>,
163 on_update: Option<ForeignKeyAction>,
164 #[debug("{}", on_condition.is_some())]
165 on_condition: Option<Arc<dyn Fn(DynIden, DynIden) -> Condition + Send + Sync>>,
166 fk_name: Option<String>,
167 condition_type: ConditionType,
168}
169
170impl RelationDef {
171 /// Reverse this relation (swap from and to)
172 pub fn rev(self) -> Self {
173 Self {
174 rel_type: self.rel_type,
175 from_tbl: self.to_tbl,
176 to_tbl: self.from_tbl,
177 from_col: self.to_col,
178 to_col: self.from_col,
179 is_owner: !self.is_owner,
180 skip_fk: self.skip_fk,
181 on_delete: self.on_delete,
182 on_update: self.on_update,
183 on_condition: self.on_condition,
184 fk_name: None,
185 condition_type: self.condition_type,
186 }
187 }
188
189 /// Express the relation from a table alias.
190 ///
191 /// This is a shorter and more discoverable equivalent to modifying `from_tbl` field by hand.
192 ///
193 /// # Examples
194 ///
195 /// Here's a short synthetic example.
196 /// In real life you'd use aliases when the table name comes up twice and you need to disambiguate,
197 /// e.g. https://github.com/SeaQL/sea-orm/discussions/2133
198 ///
199 /// ```
200 /// use sea_orm::{
201 /// DbBackend,
202 /// entity::*,
203 /// query::*,
204 /// tests_cfg::{cake, cake_filling},
205 /// };
206 /// use sea_query::Alias;
207 ///
208 /// let cf = "cf";
209 ///
210 /// assert_eq!(
211 /// cake::Entity::find()
212 /// .join_as(
213 /// JoinType::LeftJoin,
214 /// cake_filling::Relation::Cake.def().rev(),
215 /// cf.clone()
216 /// )
217 /// .join(
218 /// JoinType::LeftJoin,
219 /// cake_filling::Relation::Filling.def().from_alias(cf)
220 /// )
221 /// .build(DbBackend::MySql)
222 /// .to_string(),
223 /// [
224 /// "SELECT `cake`.`id`, `cake`.`name` FROM `cake`",
225 /// "LEFT JOIN `cake_filling` AS `cf` ON `cake`.`id` = `cf`.`cake_id`",
226 /// "LEFT JOIN `filling` ON `cf`.`filling_id` = `filling`.`id`",
227 /// ]
228 /// .join(" ")
229 /// );
230 /// ```
231 pub fn from_alias<A>(mut self, alias: A) -> Self
232 where
233 A: IntoIden,
234 {
235 self.from_tbl = self.from_tbl.alias(alias);
236 self
237 }
238
239 /// Set custom join ON condition.
240 ///
241 /// This method takes a closure with two parameters
242 /// denoting the left-hand side and right-hand side table in the join expression.
243 ///
244 /// This replaces the current condition if it is already set.
245 ///
246 /// # Examples
247 ///
248 /// ```
249 /// use sea_orm::{entity::*, query::*, DbBackend, tests_cfg::{cake, cake_filling}};
250 /// use sea_query::{Expr, ExprTrait, IntoCondition};
251 ///
252 /// assert_eq!(
253 /// cake::Entity::find()
254 /// .join(
255 /// JoinType::LeftJoin,
256 /// cake_filling::Relation::Cake
257 /// .def()
258 /// .rev()
259 /// .on_condition(|_left, right| {
260 /// Expr::col((right, cake_filling::Column::CakeId))
261 /// .gt(10i32)
262 /// .into_condition()
263 /// })
264 /// )
265 /// .build(DbBackend::MySql)
266 /// .to_string(),
267 /// [
268 /// "SELECT `cake`.`id`, `cake`.`name` FROM `cake`",
269 /// "LEFT JOIN `cake_filling` ON `cake`.`id` = `cake_filling`.`cake_id` AND `cake_filling`.`cake_id` > 10",
270 /// ]
271 /// .join(" ")
272 /// );
273 /// ```
274 pub fn on_condition<F>(mut self, f: F) -> Self
275 where
276 F: Fn(DynIden, DynIden) -> Condition + 'static + Send + Sync,
277 {
278 self.on_condition = Some(Arc::new(f));
279 self
280 }
281
282 /// Set the condition type of join on expression
283 ///
284 /// # Examples
285 ///
286 /// ```
287 /// use sea_orm::{entity::*, query::*, DbBackend, tests_cfg::{cake, cake_filling}};
288 /// use sea_query::{Expr, ExprTrait, IntoCondition, ConditionType};
289 ///
290 /// assert_eq!(
291 /// cake::Entity::find()
292 /// .join(
293 /// JoinType::LeftJoin,
294 /// cake_filling::Relation::Cake
295 /// .def()
296 /// .rev()
297 /// .condition_type(ConditionType::Any)
298 /// .on_condition(|_left, right| {
299 /// Expr::col((right, cake_filling::Column::CakeId))
300 /// .gt(10i32)
301 /// .into_condition()
302 /// })
303 /// )
304 /// .build(DbBackend::MySql)
305 /// .to_string(),
306 /// [
307 /// "SELECT `cake`.`id`, `cake`.`name` FROM `cake`",
308 /// "LEFT JOIN `cake_filling` ON `cake`.`id` = `cake_filling`.`cake_id` OR `cake_filling`.`cake_id` > 10",
309 /// ]
310 /// .join(" ")
311 /// );
312 /// ```
313 pub fn condition_type(mut self, condition_type: ConditionType) -> Self {
314 self.condition_type = condition_type;
315 self
316 }
317}
318
319impl<E, R> RelationBuilder<E, R>
320where
321 E: EntityTrait,
322 R: EntityTrait,
323{
324 pub(crate) fn new(rel_type: RelationType, from: E, to: R, is_owner: bool) -> Self {
325 Self {
326 entities: PhantomData,
327 rel_type,
328 from_tbl: from.table_ref(),
329 to_tbl: to.table_ref(),
330 from_col: None,
331 to_col: None,
332 is_owner,
333 skip_fk: false,
334 on_delete: None,
335 on_update: None,
336 on_condition: None,
337 fk_name: None,
338 condition_type: ConditionType::All,
339 }
340 }
341
342 pub(crate) fn from_rel(rel_type: RelationType, rel: RelationDef, is_owner: bool) -> Self {
343 Self {
344 entities: PhantomData,
345 rel_type,
346 from_tbl: rel.from_tbl,
347 to_tbl: rel.to_tbl,
348 from_col: Some(rel.from_col),
349 to_col: Some(rel.to_col),
350 is_owner,
351 skip_fk: false,
352 on_delete: None,
353 on_update: None,
354 on_condition: None,
355 fk_name: None,
356 condition_type: ConditionType::All,
357 }
358 }
359
360 /// Build a relationship from an Entity
361 pub fn from<T>(mut self, identifier: T) -> Self
362 where
363 T: IdentityOf<E>,
364 {
365 self.from_col = Some(identifier.identity_of());
366 self
367 }
368
369 /// Build a relationship to an Entity
370 pub fn to<T>(mut self, identifier: T) -> Self
371 where
372 T: IdentityOf<R>,
373 {
374 self.to_col = Some(identifier.identity_of());
375 self
376 }
377
378 /// Force the foreign key to not be created
379 pub fn skip_fk(mut self) -> Self {
380 self.skip_fk = true;
381 self
382 }
383
384 /// An operation to perform on a foreign key when a delete operation occurs
385 pub fn on_delete(mut self, action: ForeignKeyAction) -> Self {
386 self.on_delete = Some(action);
387 self
388 }
389
390 /// An operation to perform on a foreign key when an update operation occurs
391 pub fn on_update(mut self, action: ForeignKeyAction) -> Self {
392 self.on_update = Some(action);
393 self
394 }
395
396 /// Set custom join ON condition.
397 ///
398 /// This method takes a closure with parameters
399 /// denoting the left-hand side and right-hand side table in the join expression.
400 pub fn on_condition<F>(mut self, f: F) -> Self
401 where
402 F: Fn(DynIden, DynIden) -> Condition + 'static + Send + Sync,
403 {
404 self.on_condition = Some(Arc::new(f));
405 self
406 }
407
408 /// Set the name of foreign key constraint
409 pub fn fk_name(mut self, fk_name: &str) -> Self {
410 self.fk_name = Some(fk_name.to_owned());
411 self
412 }
413
414 /// Set the condition type of join on expression
415 pub fn condition_type(mut self, condition_type: ConditionType) -> Self {
416 self.condition_type = condition_type;
417 self
418 }
419}
420
421impl<E, R> From<RelationBuilder<E, R>> for RelationDef
422where
423 E: EntityTrait,
424 R: EntityTrait,
425{
426 fn from(b: RelationBuilder<E, R>) -> Self {
427 RelationDef {
428 rel_type: b.rel_type,
429 from_tbl: b.from_tbl,
430 to_tbl: b.to_tbl,
431 from_col: b.from_col.expect("Reference column is not set"),
432 to_col: b.to_col.expect("Owner column is not set"),
433 is_owner: b.is_owner,
434 skip_fk: b.skip_fk,
435 on_delete: b.on_delete,
436 on_update: b.on_update,
437 on_condition: b.on_condition,
438 fk_name: b.fk_name,
439 condition_type: b.condition_type,
440 }
441 }
442}
443
444macro_rules! set_foreign_key_stmt {
445 ( $relation: ident, $foreign_key: ident ) => {
446 let from_cols: Vec<String> = $relation
447 .from_col
448 .into_iter()
449 .map(|col| {
450 let col_name = col.to_string();
451 $foreign_key.from_col(col);
452 col_name
453 })
454 .collect();
455 for col in $relation.to_col.into_iter() {
456 $foreign_key.to_col(col);
457 }
458 if let Some(action) = $relation.on_delete {
459 $foreign_key.on_delete(action);
460 }
461 if let Some(action) = $relation.on_update {
462 $foreign_key.on_update(action);
463 }
464 let name = if let Some(name) = $relation.fk_name {
465 name
466 } else {
467 let from_tbl = &$relation.from_tbl.sea_orm_table().clone();
468 format!("fk-{}-{}", from_tbl.to_string(), from_cols.join("-"))
469 };
470 $foreign_key.name(&name);
471 };
472}
473
474impl From<RelationDef> for ForeignKeyCreateStatement {
475 fn from(relation: RelationDef) -> Self {
476 let mut foreign_key_stmt = Self::new();
477 set_foreign_key_stmt!(relation, foreign_key_stmt);
478 foreign_key_stmt
479 .from_tbl(relation.from_tbl)
480 .to_tbl(relation.to_tbl)
481 .take()
482 }
483}
484
485/// Creates a column definition for example to update a table.
486/// ```
487/// use sea_query::{Alias, IntoIden, MysqlQueryBuilder, TableAlterStatement, IntoTableRef, ConditionType};
488/// use sea_orm::{EnumIter, Iden, Identity, PrimaryKeyTrait, RelationDef, RelationTrait, RelationType};
489///
490/// let relation = RelationDef {
491/// rel_type: RelationType::HasOne,
492/// from_tbl: "foo".into_table_ref(),
493/// to_tbl: "bar".into_table_ref(),
494/// from_col: Identity::Unary("bar_id".into_iden()),
495/// to_col: Identity::Unary("bar_id".into_iden()),
496/// is_owner: false,
497/// on_delete: None,
498/// on_update: None,
499/// on_condition: None,
500/// fk_name: Some("foo-bar".to_string()),
501/// skip_fk: false,
502/// condition_type: ConditionType::All,
503/// };
504///
505/// let mut alter_table = TableAlterStatement::new()
506/// .table("foo")
507/// .add_foreign_key(&mut relation.into()).take();
508/// assert_eq!(
509/// alter_table.to_string(MysqlQueryBuilder::default()),
510/// "ALTER TABLE `foo` ADD CONSTRAINT `foo-bar` FOREIGN KEY (`bar_id`) REFERENCES `bar` (`bar_id`)"
511/// );
512/// ```
513impl From<RelationDef> for TableForeignKey {
514 fn from(relation: RelationDef) -> Self {
515 let mut foreign_key = Self::new();
516 set_foreign_key_stmt!(relation, foreign_key);
517 foreign_key
518 .from_tbl(relation.from_tbl.sea_orm_table().clone())
519 .to_tbl(relation.to_tbl.sea_orm_table().clone())
520 .take()
521 }
522}
523
524impl std::hash::Hash for RelationDef {
525 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
526 self.rel_type.hash(state);
527 self.from_tbl.sea_orm_table().hash(state);
528 self.to_tbl.sea_orm_table().hash(state);
529 self.from_col.hash(state);
530 self.to_col.hash(state);
531 self.is_owner.hash(state);
532 }
533}
534
535impl PartialEq for RelationDef {
536 fn eq(&self, other: &Self) -> bool {
537 self.rel_type.eq(&other.rel_type)
538 && self.from_tbl.eq(&other.from_tbl)
539 && self.to_tbl.eq(&other.to_tbl)
540 && itertools::equal(self.from_col.iter(), other.from_col.iter())
541 && itertools::equal(self.to_col.iter(), other.to_col.iter())
542 && self.is_owner.eq(&other.is_owner)
543 }
544}
545
546impl Eq for RelationDef {}
547
548#[cfg(test)]
549mod tests {
550 use crate::{
551 RelationBuilder, RelationDef,
552 tests_cfg::{cake, fruit},
553 };
554
555 #[test]
556 fn assert_relation_traits() {
557 fn assert_send_sync<T: Send + Sync>() {}
558
559 assert_send_sync::<RelationDef>();
560 assert_send_sync::<RelationBuilder<cake::Entity, fruit::Entity>>();
561 }
562}