多对多tag的处理

This commit is contained in:
胡天 2024-02-05 10:16:28 +08:00
parent adf1d94c29
commit 5d75c53356
7 changed files with 156 additions and 4 deletions

25
app.sql
View File

@ -50,3 +50,28 @@ VALUES (1, '标题-GLKUSroPOR', '内容-GLKUSroPOR'),
(1, '标题-svNJmWaqRo', '内容-svNJmWaqRo'), (1, '标题-svNJmWaqRo', '内容-svNJmWaqRo'),
(3, '标题-8XWiTUSfhB', '内容-8XWiTUSfhB'), (3, '标题-8XWiTUSfhB', '内容-8XWiTUSfhB'),
(2, '标题-yvwE32TLkg', '内容-yvwE32TLkg'); (2, '标题-yvwE32TLkg', '内容-yvwE32TLkg');
CREATE TABLE tags
(
id int unsigned PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL UNIQUE
);
INSERT INTO tags (name)
VALUES ('标签1'),
('标签2'),
('标签3');
CREATE TABLE article_tags
( -- 文章标签
id int unsigned PRIMARY KEY AUTO_INCREMENT, -- 自增主键
article_id INT NOT NULL REFERENCES articles (id),
tag_id INT NOT NULL REFERENCES tags (id)
);
INSERT INTO article_tags(article_id, tag_id)
VALUES (1, 1),
(2, 1),
(2, 2),
(3, 1),
(1, 3);

View File

@ -44,6 +44,19 @@ impl Related<super::category::Entity> for Entity {
} }
} }
impl Related<super::tag::Entity> for Entity {
fn to() -> RelationDef {
super::article_tag::Relation::Tag.def()
}
fn via() -> Option<RelationDef> {
Some(super::article_tag::Relation::Article.def().rev())
}
}
// INSERT/UPDATE/DELETE 等属于写操作,需使用 ActiveModel。 // INSERT/UPDATE/DELETE 等属于写操作,需使用 ActiveModel。
/// [DeriveEntityModel] 这个 derive 根据我们定义的[Model],生成了包括 [ActiveModel]在内的多个数据结构 /// [DeriveEntityModel] 这个 derive 根据我们定义的[Model],生成了包括 [ActiveModel]在内的多个数据结构
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

39
src/entity/article_tag.rs Normal file
View File

@ -0,0 +1,39 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "article_tags")]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: u32,
pub article_id: u32,
pub tag_id: u32,
}
#[derive(Debug, Clone, Copy, EnumIter)]
pub enum Relation {
Article,
Tag,
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Relation::Article => Entity::belongs_to(super::article::Entity)
.from(Column::ArticleId)
.to(super::article::Column::Id)
.into(),
Relation::Tag => Entity::belongs_to(super::tag::Entity)
.from(Column::TagId)
.to(super::tag::Column::Id)
.into(),
}
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,2 +1,4 @@
pub mod category; pub mod category;
pub mod article; pub mod article;
pub mod article_tag;
pub mod tag;

40
src/entity/tag.rs Normal file
View File

@ -0,0 +1,40 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "tags")]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: u32,
pub name: String,
}
#[derive(Debug, Clone, Copy, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!()
}
}
// 没有给标签实体定义关系,而是通过 Related<super::article::Entity>告诉 SeaORM如何去找所需要的文章实体。
impl Related<super::article::Entity> for Entity {
// 某个实体是否与另一个实体相关
fn to()-> RelationDef {
super::article_tag::Relation::Article.def()
}
// 某个实体是否通过另一个实体关联 即tag通过article_tag进行关联
// 表示通过标签实体关联到某个实体的关系,通过 Tag 的反向关系定义进行关联。
fn via() -> Option<RelationDef> {
Some(super::article_tag::Relation::Tag.def().rev())
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -5,7 +5,7 @@ use axum::Form;
use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, NotSet, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect}; use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, NotSet, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect};
use sea_orm::ActiveValue::Set; use sea_orm::ActiveValue::Set;
use crate::entity::{article, category}; use crate::entity::{article, category, tag};
use crate::err::AppError; use crate::err::AppError;
use crate::form::ArticleForm; use crate::form::ArticleForm;
use crate::handler::{get_conn, HtmlResponse, log_error, redirect, RedirectResponse, render}; use crate::handler::{get_conn, HtmlResponse, log_error, redirect, RedirectResponse, render};
@ -96,3 +96,35 @@ pub async fn add(
redirect("/article?msg=文章添加成功") redirect("/article?msg=文章添加成功")
} }
pub async fn list_with_tags( State(state): State<Arc<AppState>>) -> Result<String> {
let handler_name = "article/list_with_tags";
let conn = get_conn(&state);
let list = article::Entity::find()
.find_with_related(tag::Entity)
.all(conn)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
let mut ss = vec![];
for item in list {
let (article, tags) = item;
let tags = tags
.iter()
.map(|tag| format!("【#{} - {}", &tag.id, &tag.name))
.collect::<Vec<String>>()
.join(",")
.to_string();
let s = format!(
"文章ID: {}, 文章标题: {}, 标签: {}",
&article.id, &article.title, tags,
);
ss.push(s);
}
Ok(ss.join("\n").to_string())
}

View File

@ -20,7 +20,8 @@ pub fn init() -> axum::Router<Arc<AppState>> {
let article_router = axum::Router::new() let article_router = axum::Router::new()
.route("/", get(handler::article::index)) .route("/", get(handler::article::index))
.route("/add", get(handler::article::add_ui).post(handler::article::add)); .route("/add", get(handler::article::add_ui).post(handler::article::add))
.route("/tags", get(handler::article::list_with_tags));
axum::Router::new() axum::Router::new()
.route("/", get(handler::index)) .route("/", get(handler::index))