Sign in

    morm

    Download zip
    Author
    Version
    0.2.2
    License
    Apache-2.0
    Last updated
    8 months ago
    Downloads
    660

    #oboard/morm

    MoonBit 生态下的轻量级 ORM。目标很直接:

    • 用尽量少的属性和 API 覆盖大部分常见场景
    • 保持实现透明,允许随时绕过 ORM 手写 SQL
    • 不使用运行时反射,不隐式修改用户语义

    通过在 struct 上加少量 #morm.* 属性,可以自动生成:

    • impl @morm.Entity 与 table() 元数据
    • 各数据库方言下的建表 / 迁移 SQL
    • 基于 #morm.query 的类型安全 Mapper 方法(含 save / delete)
    • 统一返回 (String, FixedArray[Json]) 的参数化 SQL 构造器

    核心设计取舍:

    • 类型驱动:字段的可空性由 T / T? 决定,而不是依赖大量注解
    • 方言内建:支持 MySQL / PostgreSQL / Sqlite / SQLServer / Oracle 的建表和占位符差异
    • SQL 优先:Query/Insert/Update/Delete/Upsert 只负责构造参数化 SQL,不绑定执行层
    • 生成代码:所有内容都生成成普通 MoonBit 代码(见 example/*.g.mbt),可以直接查看和修改

    该库主要解决三个问题:

    • 统一建表与迁移 SQL 的生成
    • 统一各数据库方言的语法差异
    • 统一 SQL 参数绑定方式

    业务层的查询依然以显式 SQL 为主,由用户自己控制每一条语句。

    #安装与依赖

    在你的应用 moon.pkg.json 中引入:

    { "import": [ "oboard/morm", "oboard/morm/dialect" ] }

    你还需要自己实现一个满足 @oboard/morm/engine.Engine 的驱动,用来真正连到 MySQL / PostgreSQL / Sqlite 等数据库。example/generator_test.mbt 里有一个简化版的 MySQLEngine 示例。

    #实体建模与可空性

    一个典型实体定义如下(摘自 example/entities.mbt):

    ///|
    #morm.entity(name="teacher")
    pub(all) struct Teacher {
    #morm.primary_key
    #morm.auto_increment
    #morm.bigint
    id : Int
    #morm.varchar(length="255")
    name : String
    #morm.varchar(length="255")
    major : String
    age : Int
    #morm.datetime
    birth_date : String?
    } derive(ToJson, FromJson)

    当前版本关于可空性的规则非常简单:

    • 字段类型为 T(非 Option)时:列默认 NOT NULL
    • 字段类型为 T?(Option[T])时:列默认 NULLABLE
    • 主键永远视为非空,即便你写成 id : Int?,也会被当成 NOT NULL 主键

    属性 #morm.not_null 目前不再参与可空性判断,仅作为保留标签;真实行为完全由类型决定。

    #Attributes 使用指南

    下表列出当前支持的 #morm 属性、用法示例与效果说明。

    属性示例说明
    #morm.entity置于 struct 顶部声明该结构体为 ORM 实体,生成 impl @morm.Entity
    #morm.primary_key放在某字段上一行将该列标记为主键;主键总是非空
    #morm.auto_increment放在主键字段上一行将该列设置为自增,同时 primary_key=true、nullable=false
    #morm.not_null放在字段上一行当前版本仅作标记,实际可空性仍由类型 T / T? 决定
    #morm.varchar#morm.varchar(length="255")将该列类型设为 VarChar(255);若未提供参数,默认 255
    #morm.char#morm.char(length="1")将该列类型设为 Char(1);若未提供参数,默认 1
    #morm.text纯标签将该列类型设为 Text
    #morm.mediumtext纯标签将该列类型设为 MediumText
    #morm.longtext纯标签将该列类型设为 LongText
    #morm.binary#morm.binary(length="1")将该列类型设为 Binary(1);若未提供参数,默认 1
    #morm.varbinary#morm.varbinary(length="255")将该列类型设为 VarBinary(255);若未提供参数,默认 255
    #morm.blob纯标签将该列类型设为 Blob
    #morm.tinyint纯标签将该列类型设为 TinyInt
    #morm.smallint纯标签将该列类型设为 SmallInt
    #morm.int纯标签将该列类型设为 Int
    #morm.bigint纯标签将该列类型设为 BigInt
    #morm.float纯标签将该列类型设为 Float
    #morm.double纯标签将该列类型设为 Double
    #morm.decimal#morm.decimal(precision="10", scale="2")将该列类型设为 Decimal(10,2);若未提供参数,默认 10,2
    #morm.boolean纯标签将该列类型设为 Boolean
    #morm.json纯标签将该列类型设为 Json
    #morm.jsonb纯标签将该列类型设为 JsonB
    #morm.uuid纯标签将该列类型设为 VarChar(36)
    #morm.datetime纯标签将该列类型设为 DateTime
    #morm.date纯标签将该列类型设为 Date
    #morm.time纯标签将该列类型设为 Time
    #morm.timestamp纯标签将该列类型设为 Timestamp
    #morm.foreign_key放在外键字段上一行将该字段解析为外键,约束名:fk_<表名>_<列名>,引用列默认 id;引用表按列名约定或类型名推断
    #morm.on_delete_cascade与 #morm.foreign_key 同用外键 on_delete 设为 CASCADE
    #morm.on_update_cascade与 #morm.foreign_key 同用外键 on_update 设为 CASCADE

    说明:目前生成器尚未读取编译器暴露的参数字典,长度/精度等参数采用保守默认与示例形式;未来会增强为真正读取参数值并完全可配置。

    #代码生成:morm-gen CLI

    仓库包含一个生成器二进制 morm-gen(见 main/main.mbt),负责把带 #morm.* 的源码翻译成实体和 mapper 实现。用法:

    morm-gen <input_file> -o <output_file>

    典型流程(对应 example/):

    # 生成实体的 table() 实现 morm-gen example/entities.mbt -o example/entities.g.mbt # 生成 mapper 实现 morm-gen example/mapper.mbt -o example/mapper.g.mbt

    entities.g.mbt 会包含每个实体的 impl @morm.Entity 和 table(),mapper.g.mbt 会包含 mapper struct、::mapper 工厂函数以及基于 #morm.query 的方法实现。

    #Mapper 与查询

    在实体定义之外,你可以用 #morm.mapper 声明一个 mapper 接口(见 example/mapper.mbt):

    #morm.mapper(entity="Student")
    pub trait StudentMapper {
    #morm.query("SELECT * FROM students WHERE id = ?")
    pub fn find_student_by_id(Self, id : Int) -> Student?

    #morm.query("SELECT count(*) FROM students WHERE age = ?")
    pub fn count_students_by_age(Self, age : Int) -> Int?
    }

    生成器会为你生成:

    • pub struct StudentMapper_ { engine : &@engine.Engine }
    • pub fn Student::mapper(engine : &@engine.Engine) -> StudentMapper_
    • 对应的 impl StudentMapper for StudentMapper_,内部用参数化 SQL 调用 engine.exec / engine.query
    • 额外的通用方法:save 和 delete,直接基于实体的主键和唯一索引做 UPSERT / DELETE

    使用方式示例(简化):

    ///|
    let engine = MySQLEngine::open("mysql://...")

    ///|
    let mapper = Student::mapper(engine)

    ///|
    let stu = mapper.find_student_by_id(1)

    ///|
    let n = mapper.count_students_by_age(18)

    ///|
    let saved = mapper.save(stu)

    ///|
    let deleted = mapper.delete(stu)

    #SQL 构造器与方言占位符

    morm.mbt 内部提供一组构造器,统一返回 (sql : String, params : FixedArray[Json]):

    • select_from(table)
    • insert_into(table)
    • update(table)
    • delete_from(table)
    • upsert_into(table)

    占位符会根据方言自动切换:

    • PostgreSQL:$1, $2, ...
    • Oracle::1, :2, ...
    • MySQL / Sqlite / SQL Server:?

    例如:

    let q = @morm.select_from("students")
    .where_eq("id", 1)
    .where_eq("name", "Alice")
    .order_by(Desc("id"))
    .offset(5)
    .limit(5)
    let (sql, params) = q.to(PgSQL)

    sql 会是:

    SELECT * FROM students WHERE id = $1 AND name = $2 ORDER BY id DESC LIMIT 5 OFFSET 5

    params 则是对应顺序的 JSON 数组。

    UPSERT 使用 upsert_into(table),并且:

    • 在 PostgreSQL / Sqlite 下附带 RETURNING *,直接返回插入/更新后的行
    • 在不支持 RETURNING 的方言(如 MySQL)下,save 会用主键或唯一索引回读一行,保证你最终拿到一条完整记录

    #自动迁移(Auto-migrate)

    auto_migrate 可以根据实体的 table() 定义生成并执行建表 / 修改表 SQL。示例见 example/generator_test.mbt:

    let engine = MySQLEngine::open("mysql://root:123456@localhost:3306/test")
    @morm.auto_migrate(engine, [Student::table()])
    engine.close()

    当前版本偏保守,只覆盖基础场景;复杂 schema 变更建议显式写迁移。

    #运行测试与示例

    仓库内置了一些针对 SQL 生成和生成器的测试与示例:

    # 运行全部测试 moon test

    DbResult

    type DbResult[T] = Result[T, DbError]

    Driver

    pub(open) trait Driver {
    open(Self, dsn : String) -> Result[Self, DbError]
    close(Self) -> Result[Self, DbError]
    ping(Self) -> Result[Unit, DbError]
    exec(Self, sql : String, args : FixedArray[Json]) -> Result[Unit, DbError]
    query(Self, sql : String, args : FixedArray[Json]) -> Result[FixedArray[Json], DbError]
    }

    Entity

    pub(open) trait Entity : ToJson +
    FromJson
    {
    table(Self) -> Table
    }

    AlterOp

    pub enum AlterOp {
    AddColumn(Column)
    DropColumn(String)
    ModifyColumn(Column)
    AddIndex(Index)
    DropIndex(String)
    AddForeignKey(ForeignKey)
    DropForeignKey(String)
    RenameTable(String)
    TableComment(String)
    }

    impl Show for AlterOp
    impl ToJson for AlterOp

    AlterTable

    pub struct AlterTable {
    table : String
    ops : FixedArray[AlterOp]
    }

    impl Show for AlterTable

    AlterTable::add_column

    fn AlterTable::add_column(self : AlterTable, col : Column) -> AlterTable

    AlterTable::add_foreign_key

    fn AlterTable::add_foreign_key(self : AlterTable, fk : ForeignKey) -> AlterTable

    AlterTable::add_index

    fn AlterTable::add_index(self : AlterTable, idx : Index) -> AlterTable

    AlterTable::comment

    fn AlterTable::comment(self : AlterTable, text : String) -> AlterTable

    AlterTable::drop_column

    fn AlterTable::drop_column(self : AlterTable, name : String) -> AlterTable

    AlterTable::drop_foreign_key

    fn AlterTable::drop_foreign_key(self : AlterTable, name : String) -> AlterTable

    AlterTable::drop_index

    fn AlterTable::drop_index(self : AlterTable, name : String) -> AlterTable

    AlterTable::modify_column

    fn AlterTable::modify_column(self : AlterTable, col : Column) -> AlterTable

    AlterTable::rename

    fn AlterTable::rename(self : AlterTable, new_name : String) -> AlterTable

    AlterTable::to

    fn AlterTable::to(self : AlterTable, dialect :
    Dialect
    ) -> (String, FixedArray[Json])

    Column

    pub(all) struct Column {
    name : String
    column_type : ColumnType
    size : Int
    nullable : Bool
    default_value : String?
    auto_increment : Bool
    primary_key : Bool
    unique : Bool
    comment : String
    charset : String?
    collation : String?
    }

    impl Compare for Column
    impl Eq for Column
    impl Hash for Column
    impl Show for Column
    impl ToJson for Column

    Column::new

    fn Column::new(name : String, column_type : ColumnType) -> Column

    ColumnType

    pub(all) enum ColumnType {
    TinyInt
    SmallInt
    MediumInt
    Int
    BigInt
    Float
    Double
    Decimal(Int, Int)
    Char(Int)
    VarChar(Int)
    Text
    MediumText
    LongText
    Binary(Int)
    VarBinary(Int)
    Blob
    MediumBlob
    LongBlob
    Date
    Time
    DateTime
    Timestamp
    Year
    Json
    JsonB
    Boolean
    }

    impl Eq for ColumnType
    impl Hash for ColumnType
    impl Show for ColumnType

    DbError

    pub(all) struct DbError {
    message : String
    }

    impl Show for DbError
    impl ToJson for DbError

    DeleteQuery

    pub struct DeleteQuery {
    from : String
    where_ : FixedArray[Where]
    }

    impl Eq for DeleteQuery
    impl Show for DeleteQuery

    DeleteQuery::from

    DeleteQuery::to

    fn DeleteQuery::to(self : DeleteQuery, dialect :
    Dialect
    ) -> (String, FixedArray[Json])

    DeleteQuery::where_eq

    fn DeleteQuery::where_eq(self : DeleteQuery, col : String, value : Json) -> DeleteQuery

    DeleteQuery::where_gt

    fn DeleteQuery::where_gt(self : DeleteQuery, col : String, value : Json) -> DeleteQuery

    DeleteQuery::where_gte

    fn DeleteQuery::where_gte(self : DeleteQuery, col : String, value : Json) -> DeleteQuery

    DeleteQuery::where_lt

    fn DeleteQuery::where_lt(self : DeleteQuery, col : String, value : Json) -> DeleteQuery

    DeleteQuery::where_lte

    fn DeleteQuery::where_lte(self : DeleteQuery, col : String, value : Json) -> DeleteQuery

    ForeignKey

    pub(all) struct ForeignKey {
    name : String
    column : String
    referenced_table : String
    referenced_column : String
    on_delete : String
    on_update : String
    }

    impl Eq for ForeignKey
    impl Hash for ForeignKey
    impl Show for ForeignKey

    ForeignKey::new

    fn ForeignKey::new(name : String, column : String, referenced_table : String, referenced_column : String) -> ForeignKey

    Index

    pub(all) struct Index {
    name : String
    index_type : IndexType
    columns : FixedArray[String]
    comment : String
    }

    impl Compare for Index
    impl Eq for Index
    impl Hash for Index
    impl Show for Index
    impl ToJson for Index

    Index::new

    fn Index::new(name : String, columns : FixedArray[String]) -> Index

    Index::new_primary

    fn Index::new_primary(columns : FixedArray[String]) -> Index

    Index::new_unique

    fn Index::new_unique(name : String, columns : FixedArray[String]) -> Index

    IndexType

    pub(all) enum IndexType {
    Primary
    Unique
    Index
    FullText
    Spatial
    }

    impl Eq for IndexType
    impl Hash for IndexType
    impl Show for IndexType
    impl ToJson for IndexType

    InsertQuery

    pub struct InsertQuery {
    into : String
    columns : FixedArray[String]
    values : FixedArray[Json]
    }

    impl Eq for InsertQuery
    impl Show for InsertQuery

    InsertQuery::columns

    fn InsertQuery::columns(self : InsertQuery, columns : FixedArray[String]) -> InsertQuery

    InsertQuery::from

    InsertQuery::to

    fn InsertQuery::to(self : InsertQuery, dialect :
    Dialect
    ) -> (String, FixedArray[Json])

    InsertQuery::values

    fn InsertQuery::values(self : InsertQuery, values : FixedArray[Json]) -> InsertQuery

    OrderBy

    pub(all) enum OrderBy {
    Asc(String)
    Desc(String)
    }

    impl Eq for OrderBy
    impl Show for OrderBy
    impl ToJson for OrderBy

    Query

    pub struct Query {
    select : String
    from : String
    where_ : FixedArray[Where]
    order_by : FixedArray[OrderBy]
    limit : Int?
    offset : Int?
    }

    impl Eq for Query
    impl Show for Query
    impl ToJson for Query

    Query::limit

    fn Query::limit(self : Query, n : Int) -> Query

    Query::offset

    fn Query::offset(self : Query, n : Int) -> Query

    Query::order_by

    fn Query::order_by(self : Query, order_by : OrderBy) -> Query

    Query::page

    fn Query::page(self : Query, page : Int, size : Int) -> Query

    Query::to

    fn Query::to(self : Query, dialect :
    Dialect
    ) -> (String, FixedArray[Json])

    Query::where_eq

    fn Query::where_eq(self : Query, col : String, value : Json) -> Query

    Query::where_gt

    fn Query::where_gt(self : Query, col : String, value : Json) -> Query

    Query::where_gte

    fn Query::where_gte(self : Query, col : String, value : Json) -> Query

    Query::where_lt

    fn Query::where_lt(self : Query, col : String, value : Json) -> Query

    Query::where_lte

    fn Query::where_lte(self : Query, col : String, value : Json) -> Query

    Set

    pub struct Set {
    col : String
    value : Json
    }

    impl Eq for Set
    impl Show for Set
    impl ToJson for Set

    Table

    pub(all) struct Table {
    name : String
    columns : FixedArray[Column]
    indexes : FixedArray[Index]
    foreign_keys : FixedArray[ForeignKey]
    engine : String
    charset : String
    collation : String
    comment : String
    auto_increment : Int
    }

    impl Compare for Table
    impl Eq for Table
    impl Hash for Table
    impl Show for Table
    impl ToJson for Table

    Table::has_primary_key

    fn Table::has_primary_key(self : Table) -> Bool

    Table::new

    fn Table::new(name : String, columns? : FixedArray[Column]) -> Table

    Table::primary_key

    fn Table::primary_key(self : Table) -> String?

    Table::to_create_sql

    fn Table::to_create_sql(self : Table, dialect :
    Dialect
    ) -> String

    UpdateQuery

    pub struct UpdateQuery {
    table : String
    sets : FixedArray[Set]
    where_ : FixedArray[Where]
    }

    impl Eq for UpdateQuery
    impl Show for UpdateQuery

    UpdateQuery::from

    UpdateQuery::set

    fn UpdateQuery::set(self : UpdateQuery, col : String, value : Json) -> UpdateQuery

    UpdateQuery::to

    fn UpdateQuery::to(self : UpdateQuery, dialect :
    Dialect
    ) -> (String, FixedArray[Json])

    UpdateQuery::to_mysql_sql

    fn UpdateQuery::to_mysql_sql(self : UpdateQuery) -> String

    UpdateQuery::where_eq

    fn UpdateQuery::where_eq(self : UpdateQuery, col : String, value : Json) -> UpdateQuery

    UpdateQuery::where_gt

    fn UpdateQuery::where_gt(self : UpdateQuery, col : String, value : Json) -> UpdateQuery

    UpdateQuery::where_gte

    fn UpdateQuery::where_gte(self : UpdateQuery, col : String, value : Json) -> UpdateQuery

    UpdateQuery::where_lt

    fn UpdateQuery::where_lt(self : UpdateQuery, col : String, value : Json) -> UpdateQuery

    UpdateQuery::where_lte

    fn UpdateQuery::where_lte(self : UpdateQuery, col : String, value : Json) -> UpdateQuery

    UpsertQuery

    pub struct UpsertQuery {
    table : String
    sets : FixedArray[Set]
    conflict_target : FixedArray[String]
    update_sets : FixedArray[Set]
    }

    impl Eq for UpsertQuery
    impl Show for UpsertQuery

    UpsertQuery::do_update_set

    fn UpsertQuery::do_update_set(self : UpsertQuery, col : String, value : Json) -> UpsertQuery

    UpsertQuery::from

    UpsertQuery::on_conflict

    fn UpsertQuery::on_conflict(self : UpsertQuery, columns : FixedArray[String]) -> UpsertQuery

    UpsertQuery::set

    fn UpsertQuery::set(self : UpsertQuery, col : String, value : Json) -> UpsertQuery

    UpsertQuery::to

    fn UpsertQuery::to(self : UpsertQuery, dialect :
    Dialect
    ) -> (String, FixedArray[Json])

    Where

    pub struct Where {
    col : String
    value : Json
    ty : WhereType
    }

    impl Eq for Where
    impl Show for Where
    impl ToJson for Where

    WhereType

    pub enum WhereType {
    Eq
    Gt
    Lt
    Gte
    Lte
    }

    impl Eq for WhereType
    impl Show for WhereType
    impl ToJson for WhereType

    add_column

    fn add_column(table : Table, col : Column) -> Table

    add_foreign_key

    fn add_foreign_key(table : Table, fk : ForeignKey) -> Table

    add_index

    fn add_index(table : Table, idx : Index) -> Table

    alter_table

    fn alter_table(name : String) -> AlterTable

    auto_migrate

    fn[E :
    Engine
    ] auto_migrate(engine : E, tables : Array[Table]) -> Unit

    begin_tx

    fn begin_tx(dialect :
    Dialect
    ) -> String

    commit_tx

    fn commit_tx(dialect :
    Dialect
    ) -> String

    delete_from

    fn delete_from(table : String) -> DeleteQuery

    infer_column_type

    fn infer_column_type(value : Json) -> ColumnType

    insert_into

    fn insert_into(table : String) -> InsertQuery

    on_delete_cascade

    fn on_delete_cascade(fk : ForeignKey) -> ForeignKey

    on_update_cascade

    fn on_update_cascade(fk : ForeignKey) -> ForeignKey

    rollback_tx

    fn rollback_tx(dialect :
    Dialect
    ) -> String

    select_from

    fn select_from(table : String) -> Query

    select_raw

    fn select_raw(table : String, cols_expr : String) -> Query

    table_comment

    fn table_comment(table : Table, text : String) -> Table

    table_engine

    fn table_engine(table : Table, engine : String) -> Table

    update

    fn update(table : String) -> UpdateQuery

    upsert_into

    fn upsert_into(table : String) -> UpsertQuery