diff --git a/app.sql b/app.sql index e00493e..2839a4a 100644 --- a/app.sql +++ b/app.sql @@ -49,4 +49,29 @@ VALUES (1, '标题-GLKUSroPOR', '内容-GLKUSroPOR'), (2, '标题-pM0TURxhwC', '内容-pM0TURxhwC'), (1, '标题-svNJmWaqRo', '内容-svNJmWaqRo'), (3, '标题-8XWiTUSfhB', '内容-8XWiTUSfhB'), - (2, '标题-yvwE32TLkg', '内容-yvwE32TLkg'); \ No newline at end of file + (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); \ No newline at end of file diff --git a/src/entity/article.rs b/src/entity/article.rs index 95526d0..b924bca 100644 --- a/src/entity/article.rs +++ b/src/entity/article.rs @@ -44,6 +44,19 @@ impl Related for Entity { } } + +impl Related for Entity { + fn to() -> RelationDef { + super::article_tag::Relation::Tag.def() + } + + fn via() -> Option { + Some(super::article_tag::Relation::Article.def().rev()) + } +} + + + // INSERT/UPDATE/DELETE 等属于写操作,需使用 ActiveModel。 /// [DeriveEntityModel] 这个 derive 根据我们定义的[Model],生成了包括 [ActiveModel]在内的多个数据结构 impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/src/entity/article_tag.rs b/src/entity/article_tag.rs new file mode 100644 index 0000000..d661626 --- /dev/null +++ b/src/entity/article_tag.rs @@ -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 {} + + + diff --git a/src/entity/mod.rs b/src/entity/mod.rs index e4cb3ec..6694a54 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -1,2 +1,4 @@ pub mod category; -pub mod article; \ No newline at end of file +pub mod article; +pub mod article_tag; +pub mod tag; \ No newline at end of file diff --git a/src/entity/tag.rs b/src/entity/tag.rs new file mode 100644 index 0000000..3adff57 --- /dev/null +++ b/src/entity/tag.rs @@ -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告诉 SeaORM如何去找所需要的文章实体。 +impl Related for Entity { + // 某个实体是否与另一个实体相关 + fn to()-> RelationDef { + super::article_tag::Relation::Article.def() + } + + // 某个实体是否通过另一个实体关联 即tag通过article_tag进行关联 + // 表示通过标签实体关联到某个实体的关系,通过 Tag 的反向关系定义进行关联。 + fn via() -> Option { + Some(super::article_tag::Relation::Tag.def().rev()) + } +} + + +impl ActiveModelBehavior for ActiveModel {} + + + diff --git a/src/handler/article.rs b/src/handler/article.rs index 970d43d..c48c7bb 100644 --- a/src/handler/article.rs +++ b/src/handler/article.rs @@ -5,7 +5,7 @@ use axum::Form; use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, NotSet, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect}; use sea_orm::ActiveValue::Set; -use crate::entity::{article, category}; +use crate::entity::{article, category, tag}; use crate::err::AppError; use crate::form::ArticleForm; use crate::handler::{get_conn, HtmlResponse, log_error, redirect, RedirectResponse, render}; @@ -95,4 +95,36 @@ pub async fn add( .map_err(log_error(handler_name))?; redirect("/article?msg=文章添加成功") +} + +pub async fn list_with_tags( State(state): State>) -> Result { + 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::>() + .join(",") + .to_string(); + + let s = format!( + "文章ID: {}, 文章标题: {}, 标签: {}", + &article.id, &article.title, tags, + ); + ss.push(s); + } + + Ok(ss.join("\n").to_string()) } \ No newline at end of file diff --git a/src/router.rs b/src/router.rs index 617b601..dfc2a17 100644 --- a/src/router.rs +++ b/src/router.rs @@ -20,7 +20,8 @@ pub fn init() -> axum::Router> { let article_router = axum::Router::new() .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() .route("/", get(handler::index))