Skip to main content

rustorm_core/
relation.rs

1//! Типы связей между моделями.
2//!
3//! Реализация хранит информацию о связи и предоставляет методы
4//! для загрузки связанных записей.
5
6use crate::error::OrmResult;
7use crate::query::QueryBuilder;
8use sqlx::PgPool;
9use std::marker::PhantomData;
10
11// ---------------------------------------------------------------------------
12// BelongsTo<Related, FK>
13// ---------------------------------------------------------------------------
14
15/// Связь «принадлежит». Post belongs to User через `user_id`.
16///
17/// ```rust
18/// type Author = BelongsTo<User, "user_id">;
19/// let author = post.author().load(&pool).await?;
20/// ```
21pub struct BelongsToRef<'a, Owner, Related> {
22    fk_value: i64,
23    _owner: PhantomData<&'a Owner>,
24    _related: PhantomData<Related>,
25}
26
27impl<'a, Owner, Related> BelongsToRef<'a, Owner, Related>
28where
29    Related: crate::model::Model + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
30{
31    pub fn new(fk_value: i64) -> Self {
32        Self {
33            fk_value,
34            _owner: PhantomData,
35            _related: PhantomData,
36        }
37    }
38
39    pub async fn load(self, pool: &PgPool) -> OrmResult<Option<Related>> {
40        Related::find(self.fk_value, pool).await
41    }
42
43    pub async fn load_or_fail(self, pool: &PgPool) -> OrmResult<Related> {
44        Related::find_or_fail(self.fk_value, pool).await
45    }
46}
47
48// ---------------------------------------------------------------------------
49// HasMany<Related, FK>
50// ---------------------------------------------------------------------------
51
52/// Связь «имеет много». User has many Posts через `user_id`.
53pub struct HasManyRef<'a, Owner, Related> {
54    owner_id: i64,
55    fk_col: &'static str,
56    extra_filters: Vec<crate::column::FilterExpr>,
57    _owner: PhantomData<&'a Owner>,
58    _related: PhantomData<Related>,
59}
60
61impl<'a, Owner, Related> HasManyRef<'a, Owner, Related>
62where
63    Related: crate::model::Model
64        + crate::query::HasColumns
65        + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>
66        + Send
67        + Sync
68        + Unpin
69        + 'static,
70{
71    pub fn new(owner_id: i64, fk_col: &'static str) -> Self {
72        Self {
73            owner_id,
74            fk_col,
75            extra_filters: vec![],
76            _owner: PhantomData,
77            _related: PhantomData,
78        }
79    }
80
81    pub fn filter<F>(mut self, f: F) -> Self
82    where
83        F: FnOnce(&Related::Columns) -> crate::column::FilterExpr,
84    {
85        let cols = Related::columns_proxy();
86        self.extra_filters.push(f(&cols));
87        self
88    }
89
90    pub fn order_by<F>(self, _f: F) -> Self {
91        self // TODO: forward to query builder
92    }
93
94    pub async fn load(self, pool: &PgPool) -> OrmResult<Vec<Related>> {
95        let fk_filter = crate::column::FilterExpr::new(
96            format!("\"{}\" = $1", self.fk_col),
97            vec![crate::column::SqlValue::Int(self.owner_id)],
98        );
99        let mut qb = Related::query().filter_raw(fk_filter.sql);
100        // Дополнительные фильтры
101        for f in self.extra_filters {
102            qb = qb.filter_raw(f.sql);
103        }
104        qb.fetch_all(pool).await
105    }
106
107    pub async fn count(self, pool: &PgPool) -> OrmResult<i64> {
108        let fk_filter = crate::column::FilterExpr::new(
109            format!("\"{}\" = $1", self.fk_col),
110            vec![crate::column::SqlValue::Int(self.owner_id)],
111        );
112        Related::query().filter_raw(fk_filter.sql).count(pool).await
113    }
114
115    pub async fn create<N: serde::Serialize>(
116        self,
117        _new_record: N,
118        _pool: &PgPool,
119    ) -> OrmResult<Related> {
120        // Генерируется конкретным кодом модели через макрос
121        unimplemented!("Используйте Related::create()")
122    }
123}
124
125// ---------------------------------------------------------------------------
126// ManyToMany
127// ---------------------------------------------------------------------------
128
129/// Связь «многие ко многим» через pivot-таблицу.
130pub struct ManyToManyRef<'a, Owner, Related> {
131    owner_id: i64,
132    pivot_table: &'static str,
133    owner_fk: &'static str,
134    related_fk: &'static str,
135    _owner: PhantomData<&'a Owner>,
136    _related: PhantomData<Related>,
137}
138
139impl<'a, Owner, Related> ManyToManyRef<'a, Owner, Related>
140where
141    Related: crate::model::Model
142        + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>
143        + Send
144        + Sync
145        + Unpin
146        + 'static,
147{
148    pub fn new(
149        owner_id: i64,
150        pivot_table: &'static str,
151        owner_fk: &'static str,
152        related_fk: &'static str,
153    ) -> Self {
154        Self {
155            owner_id,
156            pivot_table,
157            owner_fk,
158            related_fk,
159            _owner: PhantomData,
160            _related: PhantomData,
161        }
162    }
163
164    pub async fn load(self, pool: &PgPool) -> OrmResult<Vec<Related>> {
165        let sql = format!(
166            r#"SELECT "{t}".* FROM "{t}"
167               INNER JOIN "{pivot}" ON "{pivot}"."{rfk}" = "{t}"."{pk}"
168               WHERE "{pivot}"."{ofk}" = $1"#,
169            t = Related::table_name(),
170            pivot = self.pivot_table,
171            rfk = self.related_fk,
172            pk = Related::primary_key(),
173            ofk = self.owner_fk,
174        );
175        sqlx::query_as::<_, Related>(&sql)
176            .bind(self.owner_id)
177            .fetch_all(pool)
178            .await
179            .map_err(crate::error::OrmError::from_sqlx)
180    }
181
182    pub async fn attach(self, related_id: i64, pool: &PgPool) -> OrmResult<()> {
183        let sql = format!(
184            "INSERT INTO \"{}\" (\"{}\", \"{}\") VALUES ($1, $2) ON CONFLICT DO NOTHING",
185            self.pivot_table, self.owner_fk, self.related_fk,
186        );
187        sqlx::query(&sql)
188            .bind(self.owner_id)
189            .bind(related_id)
190            .execute(pool)
191            .await
192            .map_err(crate::error::OrmError::from_sqlx)?;
193        Ok(())
194    }
195
196    pub async fn attach_many(self, related_ids: &[i64], pool: &PgPool) -> OrmResult<()> {
197        for &id in related_ids {
198            let sql = format!(
199                "INSERT INTO \"{}\" (\"{}\", \"{}\") VALUES ($1, $2) ON CONFLICT DO NOTHING",
200                self.pivot_table, self.owner_fk, self.related_fk,
201            );
202            sqlx::query(&sql)
203                .bind(self.owner_id)
204                .bind(id)
205                .execute(pool)
206                .await
207                .map_err(crate::error::OrmError::from_sqlx)?;
208        }
209        Ok(())
210    }
211
212    pub async fn detach(self, related_id: i64, pool: &PgPool) -> OrmResult<()> {
213        let sql = format!(
214            "DELETE FROM \"{}\" WHERE \"{}\" = $1 AND \"{}\" = $2",
215            self.pivot_table, self.owner_fk, self.related_fk,
216        );
217        sqlx::query(&sql)
218            .bind(self.owner_id)
219            .bind(related_id)
220            .execute(pool)
221            .await
222            .map_err(crate::error::OrmError::from_sqlx)?;
223        Ok(())
224    }
225
226    pub async fn detach_all(self, pool: &PgPool) -> OrmResult<()> {
227        let sql = format!(
228            "DELETE FROM \"{}\" WHERE \"{}\" = $1",
229            self.pivot_table, self.owner_fk,
230        );
231        sqlx::query(&sql)
232            .bind(self.owner_id)
233            .execute(pool)
234            .await
235            .map_err(crate::error::OrmError::from_sqlx)?;
236        Ok(())
237    }
238
239    pub async fn sync(self, related_ids: &[i64], pool: &PgPool) -> OrmResult<()> {
240        let del_sql = format!(
241            "DELETE FROM \"{}\" WHERE \"{}\" = $1",
242            self.pivot_table, self.owner_fk,
243        );
244        sqlx::query(&del_sql)
245            .bind(self.owner_id)
246            .execute(pool)
247            .await
248            .map_err(crate::error::OrmError::from_sqlx)?;
249
250        for &id in related_ids {
251            let ins_sql = format!(
252                "INSERT INTO \"{}\" (\"{}\", \"{}\") VALUES ($1, $2)",
253                self.pivot_table, self.owner_fk, self.related_fk,
254            );
255            sqlx::query(&ins_sql)
256                .bind(self.owner_id)
257                .bind(id)
258                .execute(pool)
259                .await
260                .map_err(crate::error::OrmError::from_sqlx)?;
261        }
262        Ok(())
263    }
264}