Skip to content

operate MySQL/MariaDB schema column, dynamic with frontend

License

Notifications You must be signed in to change notification settings

liuanxin/table-column

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

说明

由前端在接口中定义查询参数及返回结果, 服务端无需生成任何 model dao service 等, 自动处理数据查询并组装数据, 可以极大的简化后端代码量

使用

添加引用

<dependency>
    <groupId>com.github.liuanxin</groupId>
    <artifactId>table-column</artifactId>
    <version>???</version>
</dependency>

添加配置

/** 可以不加此配置, 如果想要使用表关联则设置 tableRelationList, 如果想要使用别名则设置 queryAliasMap */
@Configuration
public class TableColumnConfig {

    /** 设置表关联关系 */
    @Bean
    public List<TableColumnRelation> tableRelationList() {
        return List.of(
                // 主表表名, 主表字段, 跟子表的关系(一对一, 一对多, 多对多), 子表表名, 子表字段
                new TableColumnRelation("t_order", "order_no", TableRelationType.ONE_TO_ONE, "t_order_address", "order_no"),
                new TableColumnRelation("t_order", "order_no", TableRelationType.ONE_TO_ONE, "t_order_item", "order_no"),
                new TableColumnRelation("t_order", "order_no", TableRelationType.ONE_TO_MANY, "t_order_log", "order_no")
        );
    }

    /** 设置查询别名 */
    @Bean
    public Map<String, ReqAliasTemplate> queryAliasMap() {
        return Map.of(
                "all-order", new ReqAliasTemplate("Order"),
                "order-address-item-log", orderAddressItemLogAlias()
        );
    }

    private ReqAliasTemplate orderAddressItemLogAlias() {
        /* 指定查询条件及其表达式 */
        ReqAliasTemplateQuery query = new ReqAliasTemplateQuery(OperateType.AND, List.of(
                Map.of("id", ConditionType.$GT), // "id": "$GT"  ->  id >
                Map.of("orderNo", ConditionType.$NN), // orderNo is not null
                Map.of(QueryConst.TEMPLATE_META_NAME, "startTime", "createTime", ConditionType.$GE), // "startTime": "createTime:$ge"  ->  createTime >
                Map.of(QueryConst.TEMPLATE_META_NAME, "endTime", "createTime", ConditionType.$LE), // "endTime": "createTime:$le"  ->  createTime <
                /* 这里的 xxx 表示分组, 参数的时候如果有分组会用到 */
                new ReqAliasTemplateQuery(OperateType.OR, "xxx", List.of(
                        Map.of("orderStatus", ConditionType.$EQ),
                        Map.of("amount", ConditionType.$BET)
                )), // "xxx": { "type": "OR", "cons": { "orderStatus": "$EQ", "amount": "$BET" } } ->  orderStatus =  or  amount between
                Map.of("orderStatus", ConditionType.$IN),
                Map.of("OrderItem.productName", ConditionType.$NE),
                Map.of("OrderAddress.contact", ConditionType.$NE)
        ));
        // 指定默认的排序
        Map<String, String> sort = Map.of("createTime", "desc");
        // 指定默认的分页
        List<String> page = List.of("1");
        // 指定查询时多张表之间的关联方式
        List<List<String>> relationList = List.of(List.of("Order", "inner", "OrderAddress"), List.of("Order", "left", "OrderItem"));
        // 指定返回字段: 订单表 + 订单地址表 订单项表 订单日志表, 查询 Order 时 distinct
        ReqResult result = new ReqResult(List.of(
                "orderNo", "orderStatus", "amount", "desc", "createTime",
                Map.of("address", Map.of("table", "OrderAddress", "columns", List.of("contact", "phone", "address"))),
                Map.of("items", Map.of("table", "OrderItem", "columns", List.of("productName", "price", "number"))),
                Map.of("logs", Map.of("table", "OrderLog", "columns", List.of("operator", "message", "time")))
        ), true);
        // return new ReqAliasTemplate("Order", query, relationList, result);
        return new ReqAliasTemplate("Order", query, sort, page, relationList, result);
    }
}

添加以下 mapping

import java.util.Collections;

/** 对外提供查询的接口 */
@RestController
@RequiredArgsConstructor
public class TableColumnController {

    private final TableColumnTemplate tableColumnTemplate;

    /**
     * 如果对实体类过份依赖, 没有总感觉没有安全感, 可以使用此接口来生成
     * 
     * @param tables 表名, 多个用英文逗号隔开, 空则表示全部表
     * @param targetPath 生成的目标地址
     * @param packagePath 包名
     * @param generateComment 是否生成注释, 默认是
     * @param modelPrefix 生成实体时的前缀
     * @param modelSuffix 生成实体时的后缀
     */
    @PostMapping("/generate-model")
    public Integer info(String tables, String targetPath, String packagePath,
                        Boolean generateComment, String modelPrefix, String modelSuffix) {
        tableColumnTemplate.generateModel(tables, targetPath, packagePath, generateComment, modelPrefix, modelSuffix);
        return 1;
    }

    /** 当表结构有调整, 可以使用此接口重刷相关信息, 如果在线上使用请限制只能本机或局域网访问 */
    @PostMapping("/table-column")
    public Integer info() {
        return tableColumnTemplate.refreshWithDatabase(tables) ? 1 : 0;
    }

    /** 查询各表及字段的相关说明 */
    @GetMapping("/table-column")
    public List<QueryInfo> info(String tables) {
        return tableColumnTemplate.info(tables);
    }

    /** 动态查询, 请求数据及返回结果在 ReqInfo 中定义(只使用 table param type result 四项) */
    @PostMapping("/query")
    public Object query(@RequestBody ReqInfo req) {
        return tableColumnTemplate.dynamicQuery(req);
    }

    /** 使用别名的动态查询, 请求及返回在上面的 queryAliasMap 中定义, 实际查询时在 ReqInfo 的 alias 和 ReqAlias 定义 */
    @PostMapping("/query-{alias}")
    public Object queryAlias(@PathVariable("alias") String alias, @RequestBody ReqAlias req) {
        return tableColumnTemplate.dynamicQuery(alias, req);
    }
}

相关的配置如下

query:
  # 存放关联表的类的包地址, 多个用英文逗号隔开.
  scan-packages:
  # 当关联表的类上没有标注解, 将类名转换成表名时, 表的前缀(比如 t_ 开头).
  table-prefix:
  # 当关联表的类上没有标注解, 将类名转换成表名时, 表的后缀(比如 _table 结尾).
  table-suffix:
  # 设置为 true 时查询表及字段信息将返回空, 比如只在生产环境设置成 true, 默认是 false.
  has-not-return-info:
  # 设置为 true 时, 查询只能基于别名, 默认是 false.
  required-alias:
  # 设置为 true 时, 查询可以不需要有条件或分页, 默认是 false.
  not-required-condition-or-page:
  # 表名 字段名 生成别名的规则(比如表名: t_user_info, 字段: user_name):
  #   Standard(表: UserInfo,  字段: userName, 不设置则默认是此值)
  #   Horizontal(表: User-Info,  字段: user-name)
  #   Under(表: User_Info,  字段: user_name)
  #   Letter(表: A-B...Z-AA...ZZ,  字段: a-b...z-aa...zz)
  #   Number(表: 100001-100002...,  字段: 1-2...)
  #   Same(一致)
  #   Lower(表: user_info,  字段: user_name)
  #   Upper(表: USER_INFO,  字段: USER_NAME)
  alias-generate-rule:
  # 当要请求的分页数据(page * limit)大于当前值时, sql 查询将会拆分成 2 条, 先只查 id 再用 id 查具体的数据, 默认是 10000.
  deep-max-page-size:
  # 查询条件是集合时的最大个数, 默认是 1000.
  max-list-count:
  # 当表与表之间是一对一关联, 但查询时数据却出现了多条时的处理: Exception(抛出异常, 此为默认), First(以前面的为准), Cover(后面覆盖前面).
  one-to-one-has-many-rule:
  # 用来做逻辑删除的字段名, 标了 @LogicInfo 注解则以注解所在字段为主.
  logic-delete-column:
  # 用来做逻辑删除的默认值, 比如设置成 0, 实体上的字段标了 @LogicDelete 则以注解为主.
  logic-value:
  # 用来做逻辑删除的字段类型是 tinyint(1) 时的删除值, 比如设置成 1, 实体上的字段标了 @LogicDelete 则以注解为主.
  logic-delete-boolean-value:
  # 用来做逻辑删除的字段类型是 int 时的删除值, 比如设置成 UNIX_TIMESTAMP(), 实体上的字段标了 @LogicDelete 则以注解为主.
  logic-delete-int-value:
  # 用来做逻辑删除的字段类型是 bigint 时的值, 比如设置成 id, 实体上的字段标了 @LogicDelete 则以注解为主.
  logic-delete-long-value:

比如有如下表

create table `t_order` (
  `id` bigint unsigned not null auto_increment,
  `order_no` varchar(32) not null comment '订单号',
  `order_status` int unsigned not null default '0' comment '订单状态(0.用户已创建待支付, 1.用户已支付待商户发货, 2.商户已发货待用户签收, 3.用户已签收待确认完结, 4.已完结)',
  `amount` decimal(20,2) unsigned not null default '0.00' comment '订单金额',
  `desc` varchar(32) not null default '' comment '备注',
  `create_time` datetime(6) not null default current_timestamp(6) comment '创建时间',
  `update_time` datetime(6) not null default current_timestamp(6) on update current_timestamp(6) comment '更新时间',
  `deleted` int unsigned not null default '0' comment '0.未删除, 非 0.已删除',
  primary key (`id`),
  unique key `uk_order_no` (`order_no`, `deleted`)  /* 逻辑删除标识做为唯一索引的一部分 */
) engine=innodb default charset=utf8mb4 collate=utf8mb4_unicode_ci comment='订单';

create table `t_order_address` (
  `id` bigint unsigned not null auto_increment,
  `order_no` varchar(32) not null default '' comment '订单号',
  `contact` varchar(16) not null default '' comment '联系人',
  `phone` varchar(16) not null default '' comment '联系电话',
  `address` varchar(128) not null default '' comment '联系人地址',
  `create_time` datetime(6) not null default current_timestamp(6) comment '创建时间',
  `update_time` datetime(6) not null default current_timestamp(6) on update current_timestamp(6) comment '更新时间',
  `deleted` bigint unsigned not null default '0' comment '0.未删除, 非 0.已删除',
  primary key (`id`),
  unique key `uk_order_no` (`order_no`, `deleted`)
) engine=innodb default charset=utf8mb4 collate=utf8mb4_unicode_ci comment='订单地址';

create table `t_order_item` (
  `id` bigint unsigned not null auto_increment,
  `order_no` varchar(32) not null default '' comment '订单号',
  `product_name` varchar(32) not null default '' comment '商品名',
  `price` decimal(20,2) unsigned not null default '0.00' comment '商品价格',
  `number` int unsigned not null default '0' comment '商品数量',
  primary key (`id`),
  key `idx_order_no` (`order_no`)
) engine=innodb default charset=utf8mb4 collate=utf8mb4_unicode_ci comment='订单项(商品)';

create table `t_order_log` (
  `id` bigint unsigned not null auto_increment,
  `order_no` varchar(32) not null default '' comment '订单号',
  `operator` varchar(32) not null default '' comment '操作人',
  `message` text comment '操作内容',
  `time` datetime(6) not null default current_timestamp(6) comment '创建时间',
  `deleted` tinyint(1) not null default '0' comment '0.未删除, 1.已删除',
  primary key (`id`),
  key `idx_order_no` (`order_no`)  /* 有逻辑删除标识, 但无需唯一索引 */
) engine=innodb default charset=utf8mb4 collate=utf8mb4_unicode_ci comment='订单日志';

请求 GET /table-column 将会返回表及字段的结构数据(如果想要此接口不返回数据, 配置 query.has-not-return-info = true 即可),

[
  {
    "name": "Order",
    "desc": "订单",
    "columnList": [
      {
        "name": "id",
        "type": "int"
      },
      {
        "name": "orderNo",
        "desc": "订单号",
        "type": "string",
        "writeRequired": true,
        "maxLength": 32
      },
      {
        "name": "orderStatus",
        "desc": "订单状态(0.用户已创建待支付, 1.用户已支付待商户发货, 2.商户已发货待用户签收, 3.用户已签收待确认完结, 4.已完结)",
        "type": "int"
      },
      {
        "name": "amount",
        "desc": "订单金额",
        "type": "number"
      },
      {
        "name": "desc",
        "desc": "备注",
        "type": "string",
        "maxLength": 32
      },
      {
        "name": "createTime",
        "desc": "创建时间",
        "type": "date-time"
      },
      {
        "name": "updateTime",
        "desc": "更新时间",
        "type": "date-time"
      }
    ]
  },
  {
    "name": "OrderAddress",
    "desc": "订单地址",
    "columnList": [
      {
        "name": "id",
        "type": "int"
      },
      {
        "name": "orderNo",
        "desc": "订单号",
        "type": "string",
        "maxLength": 32,
        "relationTable": "Order",
        "relationColumn": "orderNo"
      },
      {
        "name": "contact",
        "desc": "联系人",
        "type": "string",
        "maxLength": 16
      },
      {
        "name": "phone",
        "desc": "联系电话",
        "type": "string",
        "maxLength": 16
      },
      {
        "name": "address",
        "desc": "联系人地址",
        "type": "string",
        "maxLength": 128
      },
      {
        "name": "createTime",
        "desc": "创建时间",
        "type": "date-time"
      },
      {
        "name": "updateTime",
        "desc": "更新时间",
        "type": "date-time"
      }
    ]
  },
  {
    "name": "OrderItem",
    "desc": "订单项(商品)",
    "columnList": [
      {
        "name": "id",
        "type": "int"
      },
      {
        "name": "orderNo",
        "desc": "订单号",
        "type": "string",
        "maxLength": 32,
        "relationTable": "Order",
        "relationColumn": "orderNo"
      },
      {
        "name": "productName",
        "desc": "商品名",
        "type": "string",
        "maxLength": 32
      },
      {
        "name": "price",
        "desc": "商品价格",
        "type": "number"
      },
      {
        "name": "number",
        "desc": "商品数量",
        "type": "int"
      }
    ]
  },
  {
    "name": "OrderLog",
    "desc": "订单日志",
    "columnList": [
      {
        "name": "id",
        "type": "int"
      },
      {
        "name": "orderNo",
        "desc": "订单号",
        "type": "string",
        "maxLength": 32,
        "relationTable": "Order",
        "relationColumn": "orderNo"
      },
      {
        "name": "operator",
        "desc": "操作人",
        "type": "string",
        "maxLength": 32
      },
      {
        "name": "message",
        "desc": "操作内容",
        "type": "string",
        "maxLength": 65535
      },
      {
        "name": "time",
        "desc": "创建时间",
        "type": "date-time"
      }
    ]
  }
]

请求 POST /query 时, 将会自动处理数据查询并组装数据, 其入参(条件和响应都在下面定义)示例如下

{
  "table": "Order", /* 表名 */
  "param": {
    "query": {      /* 查询条件 */
      /* "operate": "下面的条件拼接时的表达式, 并且(and) 和 或者(or) 两种, 不设置则默认是 and.", */
      "conditions": [
        [ "id", "$nn(表达式, 见下面的说明)" ],           /* 无值 */
        [ "orderNo", "$eq", "x" ],                    /* 单值(长度不能超过上面的 maxLength 值) */
        [ "orderStatus", "$in", [ "0", "1", "2" ] ],  /* 多值(长度不能超过 query.max-list-count 设置的值) */
        [ "amount", "$bet", [ "10", "1000.5" ] ],
        [ "OrderItem.productName", "$fuzzy", "xx" ],  /* 子表 */
        [ "OrderLog.operator", "$start", "xxx" ],     /* 子表 */
        {
          "operate": "or", /* 嵌套条件( orderStatus = 3 OR OrderLog.time >= "2020-01-01" ) */
          "conditions": [
            [ "orderStatus", "$eq", "3" ],
            [ "OrderLog.time", "$bet", [ "2020-01-01" ] ]
          ]
        },
      ]
    },
    "sort": { "createTime": "desc",  "OrderLog.operator": "asc" },
    "page": [ 2, 20 ],  /* 分页查询, 如果省略第 2 个参数如 [ 2 ] 则等同于 [ 2, 10 ] */
    "relation": [ [ "Order(主表)", "inner(连接类型, 有 left inner right 三种)", "OrderItem(子表)" ],  [ "Order", "inner", "OrderLog" ] ] # 当上面的 conditions 有多个表时需要
  },
  "result": {
    "columns": [
      "id", "orderNo", "orderStatus", "amount", "desc",
      { "createTime" : [ "yyyy-MM-dd", "GMT+8" ] },  /* 格式化: [ "pattern", "timeZone" ], 默认是 yyyy-MM-dd HH:mm:ss */
      {
        "address(子表数据返回时的自定义属性名)": {
          "table": "OrderAddress",
          "columns": [ "contact", "address" ]
        }
      },
      {
        "items": {
          "table": "OrderItem",
          "columns": [ "productName", "price", "number" ]
        }
      },
      {
        "logs": {
          "table": "OrderLog",
          "columns": [ "operator", "message", "time" ]
        }
      }
    ],
    "distinct": true /* true 表示将查询数据去重, 不设置则默认是 false */
  }
}

请求 POST /query-order-address-item-log 将使用别名中配置的规则, 前端只关注条件参数即可

{
  "query" : {
    "id" : 1,
    "orderNo": null, /* 任意值(null "" 1 0 均可), 只要有这个项就行. 对应上面的 $nn 条件 */
    "createTime": [ "2020-01-01", "2020-02-01" ],
    "xxx": { "orderStatus": "3", "amount": [ "100.50", "200" ] } /* xxx 跟别名模板中的分组名对应 */
    "orderStatus": [ 1, 2, 3 ],
    "OrderItem.productName": "xx",
    "OrderAddress.contact": "xxx"
  },
  "sort": { "id": "desc" }, /* 排序, 忽略则使用别名中设置的值 */
  "page": [ 2, 10 ] /* 分页, 忽略则使用别名中设置的值 */
}

最终会生成如下 sql

/* 如果没有分页查询入参(page)则不会生成此 sql */
SELECT COUNT(DISTINCT `Order`.id)
FROM t_order `Order` INNER JOIN t_order_item OrderItem ON ... INNER JOIN t_order_log OrderLog ON ...
WHERE `Order`.orderNo IS NOT NULL AND `Order`.orderNo = 'x'
AND `Order`.order_status IN ( 0, 1, 2 ) AND `Order`.amount BETWEEN 10 AND 1000.5
AND OrderItem.product_name LIKE '%xx%' AND OrderLog.operator LIKE 'xx%'
AND ( `Order`.order_status = 3 OR OrderLog.time >= '2020-01-01' )


/* 如果没有分页查询入参(page)则不会有 LIMIT */
SELECT DISTINCT `Order`.id, `Order`.order_no, `Order`.order_status, `Order`.amount, `Order`.`desc`
FROM t_order `Order` INNER JOIN t_order_item OrderItem ON ... INNER JOIN t_order_log OrderLog ON ...
WHERE `Order`.orderNo IS NOT NULL AND `Order`.orderNo = 'x'
AND `Order`.order_status IN ( 0, 1, 2 ) AND `Order`.amount BETWEEN 10 AND 1000.5
AND OrderItem.product_name LIKE '%xx%' AND OrderLog.operator LIKE 'xx%'
AND ( `Order`.order_status = 3 OR OrderLog.time >= '2020-01-01' )
ORDER BY `Order`.create_time DESC, OrderLog.operator
LIMIT 10, 20


/* 下面的 xxx 和 yyy 由上面的查询而来, 如果量很大会分批查询, 单次查询的个数由 query.max-list-count 控制 */
SELECT order_no, contact, address
FROM t_order_address
WHERE order_no IN ( 'xxx', 'yyy' )


SELECT order_no, productName, price, number
FROM t_order_item
WHERE order_no IN ( 'xxx', 'yyy' )


SELECT order_no, operator, message, time
FROM t_order_log
WHERE order_no IN ( 'xxx', 'yyy' )

返回数据如下

{
  "count": 123,
  "list": [ /* 如果没有分页查询入参(page)则返回的是此数组 */
    {
      "id": 1234,
      "orderNo": "xx",
      "amount": "xxx",
      "desc": "xxxxxx",
      "createTime": "yyyy-MM-dd",
      "address": {
        "contact": "y",
        "address": "yy"
      },
      "items": [
        {
          "productName": "z",
          "price": "10.5",
          "number": 2
        }
      ],
      "logs": [
        {
          "operator": "z",
          "message": "zz",
          "time": "yyyy-MM-dd HH:mm:ss"
        },
        {
          "operator": "zzz",
          "message": "zzzz",
          "time": "yyyy-MM-dd HH:mm:ss"
        }
      ]
    },
    { ... }
  ]
}

表达式说明

表达式(忽略大小写) 说明 对应 sql
$nu 为空 IS NULL
$nn 不为空 IS NOT NULL
$eq 等于 =
$ne 不等于 <>
$in 包含 IN
$ni 不包含 NOT IN
$bet 区间 BETWEEN
$nbe 不在区间 NOT BETWEEN
$gt 大于 >
$ge 大于等于 >=
$lt 小于 <
$le 小于等于 <=
$fuzzy 模糊 LIKE '%x%'
$nfuzzy 不模糊 NOT LIKE '%x%'
$start 开头 LIKE 'x%'
$nstart 不开头 NOT LIKE 'x%'
$end 结尾 LIKE '%x'
$nend 不结尾 NOT LIKE '%x'

About

operate MySQL/MariaDB schema column, dynamic with frontend

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages