#database-access #diesel #persistence #jpa

rpa

类似于Java中的JPA库,用于访问数据库

17次发布

0.5.1 2021年6月10日
0.4.61 2020年11月2日
0.4.8 2021年6月4日
0.4.7 2020年12月17日
0.2.0 2019年7月27日

#9 in #database-access

自定义许可证

57KB

RPA Version

这是一个包含一些宏和派生类型,以帮助开发者使用RocketDiesel访问数据库的库。这仅适用于具有Rocket和Diesel(Rest API)的项目。此版本适用于所有Diesel支持的数据库(MySQL、PostgreSQL和SQLite)。

想法

由于我们目前在Rust中没有类似JPA(Java持久化API)的解决方案,我们决定使用宏和派生类型来创建一个。基本上,使用宏可以生成所有使用Diesel框架访问数据库的方法,而无需为每个类型实现所有方法。

模块

该库包含2个模块,一个用于派生类型,另一个用于。我们这样做是因为目前Cargo不允许我们发布未分离模块的库。未来,如果Cargo允许,我们可能会将所有内容放在单个仓库中。

依赖项

在你的项目中,你需要导入这些依赖项

# Rocket Webserver
[dependencies.rocket]
version = "0.4.10"
[dependencies.rocket_contrib]
version = "0.4.10"
default-features = false
features = ["json", "diesel_mysql_pool"]
#features = ["json", "diesel_postgres_pool"] if you use PostgreSQL
#features = ["json", "diesel_sqlite_pool"] if you use SQLite

# Json Serialize and Deserialize
[dependencies.serde]
version = "1.0.126"
features = ["derive"]

[dependencies.serde_json]
version = "1.0.64"

[dependencies.diesel]
version = "1.4.6"
features = ["chrono", "numeric"]

# bigdecimal for diesel has to be compatible, for now we have that diesel needs bigdecimal <= 0.2.0
[dependencies.bigdecimal]
version = "<= 0.2.0"
features = ["serde"]

# You can import this dependency like this or download this repo and import it manually
[dependencies.rpa]
version = "0.5.1"

如何使用它

你需要创建一个模型结构,以下以Hero模型为例

#[table_name = "heroes"]
#[derive(
        AsChangeset,
        Serialize,
        Deserialize,
        Queryable,
        QueryableByName,
        Insertable,
        TypeInfo,
        Debug,
        Clone,
        Rpa
)]
pub struct Hero {
    pub id: String,
    pub first_name: String,
    pub last_name: String,
}

正如你所见,我们有一个名为Hero的结构,上面有一些宏。这些宏将为我们创造奇迹。我们使用了Diesel的AsChangeset、Queryable、QueryableByName和Insertable宏,这些是访问数据库所需的,我们还使用了Serde的Serialize和Deserialize,它们用于Json反序列化和序列化。我们使用了TypeInfo、Debug和Clone派生类型,这些是由我们的库使用的。最后,你可以看到Rpa宏,这是一个自定义宏,它将生成让我们访问数据库的基础。你可以在rpa_macros模块中的文件rpa_macros/src/database/rpa.rs下的Rpa特质中看到可用的方法。对于关联,我们无法在特质中定义这些方法,因为它们是动态的,但我们有关于库如何生成这些方法的注释。

定义了实体结构之后,我们现在可以调用生成的方法,如下所示

use rpa::{RpaError, Rpa};
use diesel::MysqlConnection;
use rocket_contrib::json::Json;
...

let result: Result<Hero, RpaError> = Hero::find(&hero_id, &*_connection); // to find
let json: Json<Hero> = result.unwrap().into_json(); // to convert into serde json
let hero: Hero = Hero::from_json(json); // to convert from serde json
...

注意:所有示例中的 &*_connection 是数据库的连接实例。在Rocket中,它由框架通过fairings注入,你需要初始化它。(有关如何操作的更多信息,请参阅rocket 文档)。

目前我们只能假设模式位置始终位于父项目的 src/schema.rs 中。此外,所有结构ID都被假定为字符串。我们将在未来改进以支持更多类型。

关联

我们可以使用来自 Diesel关联 来映射关系。Diesel与其他ORM不同,因为我们没有嵌套结构作为结果,相反,你必须查询父结构,然后使用父类型的实例查询所有子结构。这种方式更好,因为我们可以避免嵌套结构可能带来的许多问题,而且更高效(我们不查询不需要的数据)。我们提供了在库中使用这些关联的方法,如下所示

use rpa::Rpa;

#[table_name = "hero"]
#[derive(
        AsChangeset,
        Serialize,
        Deserialize,
        Queryable,
        QueryableByName,
        Insertable,
        Identifiable,
        TypeInfo,
        Rpa,
        Debug,
        Clone
)]
pub struct Hero {
    pub id: String,
    pub first_name: String,
    pub last_name: String
}

如你所见,我们添加了Identifiable derive,这是Diesel映射我们结构所需的。现在我们需要另一个结构来关联,如下所示

use rpa::Rpa;
use crate::core::models::hero::Hero;

#[table_name = "post"]
#[belongs_to(Hero, foreign_key = "hero_id")]
#[derive(
    AsChangeset,
    Serialize,
    Deserialize,
    Queryable,
    QueryableByName,
    Insertable,
    Identifiable,
    Associations,
    TypeInfo,
    Rpa,
    Debug,
    Clone
)]
pub struct Post {
    pub id: String,
    pub hero_id: String,
    pub text: String
}

我们有一个结构Post,它对Hero结构有一些引用和Identifiable derive,但我们需要添加 Associations derive。这是Diesel的要求,正如你在 关联 文档中看到的那样。

按照上述步骤操作后,您将可以使用以下方法

let hero: Hero = Hero::find(&hero_id, &*_connection).unwrap();
let posts: Vec<Post> = Post::find_for_hero(&vec![hero.clone()], &*_connection).unwrap();
let grouped_posts: Vec<(Hero, Vec<Post>)> = Post::find_grouped_by_hero(&heroes, &*_connection).unwrap();

一些方法是在使用Rpa时生成的,例如 find_for_{parent_name_lowercase},它搜索所有由父项或父项拥有的子项。另一个方法称为 find_grouped_by_{parent_name_lowercase},它类似于上述方法,但按父项对结果进行分组。

不同的数据库

默认情况下,Rpa使用Mysql,但如果你想使用其他数据库,我们支持Diesel数据库,目前支持的是PostgreSQL和SQLite。要更改默认数据库,可以使用一个名为connection_type的属性。该属性在每个结构中只能有一个值("MYSQL","POSTGRESQL","SQLITE")。以下是使用方法

extern crate chrono;

use chrono::NaiveDateTime;
use rpa as Rpa;
use crate::core::models::custom_formats::custom_date_format;

#[table_name = "hero"]
#[connection_type="POSTGRESQL"]
#[derive(
        AsChangeset,
        Serialize,
        Deserialize,
        Queryable,
        QueryableByName,
        Insertable,
        Identifiable,
        TypeInfo,
        Rpa,
        Debug,
        Clone
)]
pub struct Hero {
    pub id: String,
    pub first_name: String,
    pub last_name: String,
    #[serde(with = "custom_date_format")]
    pub birth_date: NaiveDateTime
}

此外,您还需要更改Cargo.toml,将依赖项中的 diesel_postgres_pool 用于 rocket_contrib,以便使其正常工作。您可以使用此库拥有多个数据库,但需要小心混用,您 不能 将具有不同数据库的不同结构混合。

搜索(仅适用于mysql数据库)

我们有一个新功能,允许对实体进行搜索请求。想法是允许用户通过请求查询任何实体中的任何字段。现在有一个名为SearchRequest的新结构,它用于获取实体的查询。

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SearchRequest {
    #[serde(rename="filterFields")]
    pub filter_fields: Vec<FilterField>,
    #[serde(rename="sortFields")]
    pub sort_fields: Vec<SortField>,
    pub pagination: Option<Pagination>
}

该请求现在是Rpa trait中的一个新功能,称为search,trait现在看起来像这样

pub trait Rpa<T, C> where C: diesel::Connection {
    fn into_json(self) -> Json<T>;
    fn from_json(json: Json<T>) -> T;

    fn save(entity: &T, connection: &C) -> Result<T, RpaError>;
    fn save_self(self: Self, connection: &C) -> Result<T, RpaError>;
    fn save_batch(entities: Vec<T>, connection: &C) -> Result<usize, RpaError>;
    fn find(entity_id: &String, connection: &C) -> Result<T, RpaError>;
    fn find_all(connection: &C) -> Result<Vec<T>, RpaError>;
    fn exists(entity_id: &String, connection: &C) -> Result<bool, RpaError>;
    fn update(entity_id: &String, entity: &T, connection: &C) -> Result<usize, RpaError>;
    fn update_self(self: Self, connection: &C) -> Result<usize, RpaError>;
    fn delete(entity_id: &String, connection: &C) -> Result<usize, RpaError>;
    fn delete_self(self: Self, connection: &C) -> Result<usize, RpaError>;

    fn search(search_request: SearchRequest, connection: &C) -> Result<SearchResponse<T>, RpaError>;
}

search方法使用上述请求对实体进行搜索,然后获取包含SearchResponse的结果,该结构如下所示

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SearchResponse<T> {
    pub results: Vec<T>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "totalPages")]
    pub total_pages: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "pageSize")]
    pub page_size: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub page: Option<i64>
}

该结构有一个包含此搜索可能结果数组的数组,然后我们有一些参数,这些参数告诉我们分页信息。这些参数是可选的,因为您可以在请求中指定是否需要分页。如果没有请求分页,则所有结果都将包含在此响应中。所以,例如,我们可以使用以下请求

{
    "filterFields": [
        { 
            "name": "someFieldName",
            "value": "someValue",
            "operator": "LIKE",
            "joiner": "OR"
        }
    ],
    "sortFields": [
        {
            "name": "otherFieldName",
            "order": "DESC"
        }
    ],
    "pagination": {
        "page": 1,
        "pageSize": 2
    }
}

并得到以下响应

{
    "results": [
        {
            "someFieldName": "someValue like this",
            ...other fields
            "otherFieldName": "10"
        },
        {
            "someFieldName": "or someValue like this",
            ...other fields
            "otherFieldName": "9"
        },
        {
            "someFieldName": "or maybe someValue like this",
            ...other fields
            "otherFieldName": "8"
        }
    ],
    "totalPages": 2,
    "pageSize": 3,
    "page": 1
}

此功能旨在由rocket从API使用,因此我们基本上添加了此支持,以便API可以从代码外部动态查询实体。

搜索规范

在本节中,我们指定搜索请求和响应字段,以便了解它们的含义。

对于搜索请求,我们有以下内容

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SearchRequest {
    #[serde(rename="filterFields")]
    pub filter_fields: Vec<FilterField>,
    #[serde(rename="sortFields")]
    pub sort_fields: Vec<SortField>,
    pub pagination: Option<Pagination>
}
过滤字段
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FilterField {
    pub name: String,
    pub value: String,
    pub operator: Operator,
    pub joiner: WhereJoiner
}

这指定了要过滤的字段或字段,如你所见,我们有

  • name: 要过滤的字段名。
  • value: 用于过滤比较的值。
  • operator: 适用于此比较的运算符,运算符可以有以下值之一:LESS(小于)、EQUALS(等于)、GREATER(大于)、DISTINCT(不同)、LESS_OR_EQUALS(小于等于)、GREATER_OR_EQUALS(大于等于)、LIKE(类似)。
  • joiner: 这指定了我们要进行包含过滤还是排除过滤,这意味着我们可以在请求中包含多个字段,然后如果我们有这些字段,我们可以说明我们是否希望同时根据这些值过滤所有这些字段,或者过滤至少一个与值匹配的字段。WhereJoiner可以有以下值之一:OR(或)、AND(与)。
排序字段
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SortField {
    pub name: String,
    pub order: Order,
}

这指定了要排序的字段或字段,这里有

  • name: 要排序的字段名。
  • order: 要应用的顺序。顺序可以有以下值之一:ASC(升序)、DESC(降序)。

注意:排序策略始终是包含的,这意味着我们始终按照请求中指定的相同顺序对所有指定字段进行排序。

分页
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Pagination {
    pub page: i64,
    #[serde(rename="pageSize")]
    pub page_size: i64
}

这指定了请求的分页,现在我们有

  • page: 我们要查询的页码,应大于0。
  • pageSize: 要获取的pageSize,例如,如果我们有一个包含10条记录的表,并使用pageSize为5进行查询,我们将得到2页作为结果。

注意:第一次查询时,我们始终使用1作为页码,因为我们不知道总页数,它将直接取决于pageSize。

我们可以这样使用搜索

let search_request = SearchRequest {
    ....
}
let response: Result<SearchResponse<Hero>, RpaError> = Hero::search(search_request, &*_connection);
if response.is_ok() {
    let response: SearchResponse<Hero> = response.unwrap();
    let heroes: Vec<Hero> = response.results;
}

批量保存

我们具有批量保存的能力来保存多个对象。我们可以这样使用批量保存

let entities: Vec<Hero> = Vec::new();
...
let result: Result<usize, RpaError> = Hero::save_batch(entities, &*_connection);

下一步

  • 从结构中生成模式。
  • 添加更多有用的查询数据方法。
  • 添加一些像Java中Spring对JPARepositories拥有的支持。

依赖项

~18–28MB
~478K SLoC