后端低代码平台之动态建模和动态接口实现

764 阅读7分钟

前言

市面上关于前端低代码的平台或文章很多,后端低代码的文章比较少,这一篇文章我给大家分享一下后端低代码平台比较核心的两个东西,动态建模和动态接口,还有一个比较核心的逻辑编排后面再说。

框架选择

因为我是前端,有两个框架比较适合我,nest.js 和 midway.jsmidway.js 这个框架我用的多一点,所以这里我选择使用 midway 框架来讲解。

数据库 orm 库这里我选择的是 typeorm

使用 midway + typeorm 实现增删改查

创建midway项目

在合适的目录下执行下面命令,创建 midway 项目

npm init midway@latest -y

image.png

引入typeorm

安装依赖

在项目里安装 typeorm 依赖

pnpm i @midwayjs/typeorm@3 typeorm --save

引入组件

在 src/configuration.ts 引入 orm 组件

image.png

// configuration.ts
import { Configuration } from '@midwayjs/core';
import * as orm from '@midwayjs/typeorm';
import { join } from 'path';

@Configuration({
  imports: [
    // ...
    orm                                                         // 加载 typeorm 组件
  ],
  importConfigs: [
    join(__dirname, './config')
  ]
})
export class MainConfiguration {

}

安装mysql数据库驱动

pnpm i mysql2 --save

添加typeorm配置

修改 /src/config/config.default.ts 文件,添加数据库连接信息。

image.png

import { MidwayConfig } from '@midwayjs/core';

export default {
  // use for cookie sign key, should change to your own and keep security
  keys: '1731912689706_6032',
  koa: {
    port: 7001,
  },
  typeorm: {
    dataSource: {
      default: {
        /**
         * 单数据库实例
         */
        type: 'mysql',
        host: '127.0.01',
        port: 3306,
        username: 'root',
        password: '12345678',
        database: 'lowcode',
        synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
        logging: true,
        entities: [
          'entity',            
        ]
      }
    }
  },
} as MidwayConfig;

ps:需要启动数据库,我这里使用 docker 启动的mysql服务。

创建模型

在src文件夹下创建entity文件夹,在entity文件夹下创建user.ts 文件,创建一个 user 表,字段有 id,name,age,id 是主键并且自增。

// /src/entity/user.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('user')
export class UserEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  age: number;
}

@PrimaryGeneratedColumn 表示主键自增列

启动项目

npm run dev

启动项目之后,会根据模型自动在数据库中建表

image.png

实现server

在 service 文件夹下创建 user.service.ts 文件

import { Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { UserEntity } from '../entity/user';
import { Repository } from 'typeorm';

@Provide()
export class UserService {

  @InjectEntityModel(UserEntity)
  userModel: Repository<UserEntity>;

  /**
   * 查询所有用户
   * @returns all user
   */
  async list() {
    return await this.userModel.find();
  }

  /**
   * 分页查询用户
   * @returns page user
   */
  async page() {
    const [data, total] = await this.userModel.findAndCount();

    return {
      data,
      total
    }
  }

  /**
   * 新增用户
   * @param user
   * @returns
   */
  async add(user: UserEntity) {
    return await this.userModel.save(user);
  }

  /**
   * 根据id删除用户
   * @param id
   * @returns
   */

  async delete(id: number) {
    return await this.userModel.delete(id);
  }

  /**
   * 更新用户
   * @param user
   * @returns
   */
  async update(user: UserEntity) {
    return await this.userModel.save(user);
  }

  /**
   * 根据id查询用户
   * @param id
   * @returns
   */

  async findById(id: number) {
    return await this.userModel.findOne({ where: { id } });
  }
}

实现controller

在 controller 文件夹下创建 user.controller.ts 文件

import { Body, Controller, Del, Get, Inject, Param, Post, Put, Query } from '@midwayjs/core';
import { UserService } from '../service/user.service';

@Controller('/user')
export class ModelController {
  @Inject()
  userService: UserService;

  /**
   * 获取所有用户
   */
  @Get("/list")
  async list() {
    return await this.userService.list();
  }

  /**
   * 分页获取用户
   */
  @Get("/page")
  async page(@Query() query) {
    return await this.userService.page(query.page, query.size);
  }

  /**
   * 新增用户
   */
  @Post('/')
  async create(@Body() body) {
    return await this.userService.create(body);
  }

  /**
   * 更新用户
   */
  @Put('/')
  async update(@Body() body) {
    return await this.userService.update(body);
  }

  /**
   * 删除用户
   */
  @Del('/:id')
  async remove(@Param('id') id) {
    return await this.userService.delete(id);
  }
}

测试接口

使用postman测试接口

新增

image.png

查询所有用户

image.png

分页查询

image.png

更新用户信息

image.png

删除用户

image.png

image.png

动态建模

上面我们实现了对用户表增删改查,可以发现我们需要 手动创建user模型,还要实现 service 和 contoller,一个简单的功能要写那么多代码,如果这个功能使用低代码去做,应该怎么实现呢,下面给大家分享一下。

动态建表

typeorm 提供了方法可以直接建表,不用自己写 sql 语句建表。

import { Controller, Get } from '@midwayjs/core';
import { InjectDataSource } from '@midwayjs/typeorm';
import { DataSource, Table } from 'typeorm';

@Controller('/')
export class HomeController {
  @InjectDataSource()
  dataSource: DataSource;
  @Get('/')
  async home(): Promise<any> {
    const table = new Table({
      name: 'test',
      columns: [
        {
          name: 'id',
          type: 'int',
          isPrimary: true,
          isGenerated: true,
          generationStrategy: 'increment',
        },
        {
          name: 'name',
          type: 'varchar',
        },
      ],
    });

    await this.dataSource.createQueryRunner().createTable(table);
  }
}

启动项目,访问 http://localhost:7001, 发现表创建好了

image.png

构造schema

上面动态建了表,如果想对表数据进行增删改查,有两个方式

  1. 动态拼接 sql,然后使用 this.dataSource.createQueryRunner().query('select * from test') 动态执行。
  2. 使用 typeorm动态构建EntitySchema,不用写 sql。

这里我选择第 2 种方式,因为自己拼接 sql,复杂度太高了。

创建EntitySchema

   const schema = new EntitySchema({
      name: 'test',
      tableName: 'test',
      columns: {
        id: {
          name: 'id',
          primary: true,
          type: 'int',
        },
        name: {
          name: 'name',
          primary: false,
          type: 'varchar',
        },
      },
    });

把刚创建的 schema 放到 dataSource 里

    await this.dataSource
      .setOptions({
        entities: [schema],
      })
      .buildMetadatas();

这里调用 buildMetadatas方法会报错,因为这个是私有方法,不能访问,ts 语法会报错,这个方法还是我去 typeorm 源码里找到的。

自己封装一个 DataSource 类去继承 typeorm 里的 DataSource, 然后把 buildMetadatas 方法改为 public。

import { DataSource } from 'typeorm';

export class CustomDataSource extends DataSource {
  public async buildMetadatas(): Promise<void> {
    super.buildMetadatas();
  }
}

这里把类型改为CustomDataSource

image.png

然后可以这样插入数据和查询数据,不用写一行 sql

image.png

image.png

完整代码

import { Controller, Get } from '@midwayjs/core';
import { InjectDataSource } from '@midwayjs/typeorm';
import { EntitySchema } from 'typeorm';
import { CustomDataSource } from '../custom.data.source';

@Controller('/')
export class HomeController {
  @InjectDataSource()
  dataSource: CustomDataSource;
  @Get('/')
  async home(): Promise<any> {

    // const table = new Table({
    //   name: 'test',
    //   columns: [
    //     {
    //       name: 'id',
    //       type: 'int',
    //       isPrimary: true,
    //       isGenerated: true,
    //       generationStrategy: 'increment',
    //     },
    //     {
    //       name: 'name',
    //       type: 'varchar',
    //     },
    //   ],
    // });

    // await this.dataSource.createQueryRunner().createTable(table);

    const schema = new EntitySchema({
      name: 'test',
      tableName: 'test',
      columns: {
        id: {
          name: 'id',
          primary: true,
          type: 'int',
          generated: 'increment',
        },
        name: {
          name: 'name',
          primary: false,
          type: 'varchar',
        },
      },
    });


    await this.dataSource
      .setOptions({
        entities: [schema],
      })
      .buildMetadatas();
    
    // 插入数据
    await this.dataSource.getRepository(schema).save({ name: 'hello' })
    // 查询数据
    return await this.dataSource.getRepository(schema).find();
  }
}

对外暴露建模接口

上面创建表和列都是写死的数据,这里我们给改造成动态的,可以通过接口动态创建表和列。

创建table entity和column entity

创建table entity和column entity,用来存放我们动态创建的表和列。这里我只加了 name 字段,实际上表和列还可以配置其他很多属性,这里是 demo,我就简单处理了。

table entity

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('table')
export class TableEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  name: string;
}

column entity

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('column')
export class ColumnEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  modelId: number;
}

service实现


import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectDataSource, InjectEntityModel } from '@midwayjs/typeorm';
import { TableEntity } from '../entity/table';
import { EntitySchema, Repository, Table, TableColumn } from 'typeorm';
import { ColumnEntity } from '../entity/column';
import { CustomDataSource } from '../custom.data.source';

@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class TableService {
  @InjectEntityModel(TableEntity)
  tableModel: Repository<TableEntity>;
  @InjectEntityModel(ColumnEntity)
  columnModel: Repository<ColumnEntity>;
  @InjectDataSource()
  dataSource: CustomDataSource;

  /**
   * 创建表
   * @param data 
   * @returns 
   */
  async createModel(data) {
    await this.dataSource
      .createQueryRunner()
      .createTable(
        new Table({
          name: data.name,
          columns: [
            {
              name: 'id',
              type: 'int',
              isPrimary: true,
              isGenerated: true,
              generationStrategy: 'increment',
            },
          ],
        })
      );
    await this.tableModel.save(data);
    await this.buildMetadatas();
  }

  /**
   * 创建表字段
   * @param data
   */
  async createColumn(data) {

    const model = await this.tableModel.findOne({
      where: {
        id: data.modelId
      }
    });

    if (!model) return;

    await this.dataSource
      .createQueryRunner()
      .addColumn(
        model.name,
        new TableColumn(
          { name: data.name, type: data.type }
        )
      );

    await this.columnModel.save(data);
    await this.buildMetadatas();
  }

  /**
   * 获取所有模型和列
   */
  async getModelList() {
    return await this.tableModel
      .createQueryBuilder('t')
      .leftJoinAndMapMany('t.children', ColumnEntity, 'c', 't.id = c.modelId')
      .getMany();
  }

  /**
   * 动态创建模型
   */
  async buildMetadatas() {
    const data = await this.tableModel
      .createQueryBuilder('t')
      .innerJoinAndMapMany('t.columns', ColumnEntity, 'c', 't.id = c.modelId')
      .getMany();


    const schemas = data.map((cur: any) => {
      return new EntitySchema({
        name: cur.name,
        tableName: cur.name,
        columns: {
          id: {
            name: 'id',
            primary: true,
            type: 'int',
            generated: 'increment',
          },
          ...(cur.columns || []).reduce((p, c) => {
            return {
              ...p,
              [c.name]: {
                type: 'varchar',
                name: c.name
              }
            }
          }, {}),
        }
      })
    });

    await this.dataSource
      .setOptions({
        entities: [
          TableEntity,
          ColumnEntity,
          ...schemas
        ],
      })
      .buildMetadatas();
  }
}

contoller实现

import { Body, Controller, Get, Inject, Post } from '@midwayjs/core';
import { TableService } from '../service/table.service';

@Controller('/table')
export class TableController {
  @Inject()
  tableService: TableService;

  @Post('/')
  async createModel(@Body() data) {
    return await this.tableService.createModel(data);
  }

  @Post('/column')
  async createColumn(@Body() data) {
    return await this.tableService.createColumn(data);
  }

  @Get('/')
  async findModel() {
    return await this.tableService.getModelList();
  }
}

前端页面实现

使用 react 和 antd 快速实现模型的增删改查

import { Button, Form, Input, Modal, Space, Table } from 'antd'
import axios from 'axios';
import { useEffect, useState } from 'react'

function App() {
  const [open, setOpen] = useState(false);
  const [data, setData] = useState([]);
  const [modelId, setModelId] = useState<number | null>(null);

  const [form] = Form.useForm();

  useEffect(() => {
    getData();
  }, []);

  useEffect(() => {
    form.setFieldsValue({ name: '' })
  }, [open])

  function getData() {
    axios.get('/api/table').then(res => {
      setData(res.data);
    })
  }

  async function onFinish(values: any) {
    if (!modelId) {
      await axios.post('/api/table', {
        name: values.name
      });
    } else {
      await axios.post('/api/table/column', {
        modelId,
        name: values.name,
        type: 'varchar',
      });
    }
    setOpen(false);
    getData();
  }

  return (
    <Space
      direction='vertical'
      style={{ width: '100%' }}
    >
      <Button
        onClick={() => {
          setOpen(true);
          setModelId(null);
        }}
        type='primary'
      >
        创建模型
      </Button >
      <Table
        rowKey={'id'}
        dataSource={data}
        columns={[
          {
            dataIndex: 'name',
            title: '模型名称',
          },
          {
            dataIndex: 'id',
            title: '操作',
            render: (v) => (
              <Button
                type='link'
                onClick={
                  () => {
                    setOpen(true);
                    setModelId(v);
                  }
                }
              >
                添加列
              </Button>
            )
          }
        ]}
        size='small'
        pagination={false}
      />
      <Modal
        title="创建"
        open={open}
        onCancel={() => setOpen(false)}
        onOk={() => {
          form.submit();
        }}
      >
        <Form
          form={form}
          onFinish={onFinish}
        >
          <Form.Item
            name="name"
            label="名称"
          >
            <Input />
          </Form.Item>
        </Form>
      </Modal>
    </Space>
  )
}

export default App

动态接口

前面把模型和表都建好了,下面我们想办法让别人可以通过接口对表数据进行增删改查。

新建一个 ApiController

import { Controller, Param, Body, Post } from '@midwayjs/core';
import { InjectDataSource } from '@midwayjs/typeorm';
import { DataSource } from 'typeorm';

@Controller('/api')
export class APIController {
  @InjectDataSource()
  dataSource: DataSource;

  /**
   * 查询对应表数据
   */
  @Post('/model/:modelName/get')
  async find(@Param('modelName') modelName, @Body() body) {
    const data = await this.dataSource
      .getRepository(modelName)
      .find(body);
    return data;
  }

  /**
   * 分页查询对应表数据
   */
  @Post('/model/:modelName/page')
  async page(@Param('modelName') modelName, @Body() body) {
    const [data, total] = await this.dataSource
      .getRepository(modelName)
      .findAndCount({
        skip: (body.page || 0) * (body.size || 10),
        take: body.size || 10
      });
    return {
      data,
      total,
    };
  }

  /**
   * 创建数据
   */
  @Post('/model/:modelName/create')
  async create(@Param('modelName') modelName, @Body() body) {
    const data = await this.dataSource
      .getRepository(modelName)
      .save(body);
    return data;
  }

  /**
   * 更新数据
   */
  @Post('/model/:modelName/update')
  async update(@Param('modelName') modelName, @Body() body) {
    const data = await this.dataSource
      .getRepository(modelName)
      .save(body);
    return data;
  }

  /**
   * 删除数据
   */
  @Post('/model/:modelName/delete')
  async delete(@Param('modelName') modelName, @Body() body) {
    const data = await this.dataSource
      .getRepository(modelName)
      .delete(body.id);
    return data;
  }
}

在启动项目的生命周期里, 需要先调用TableService里的buildMetadatas方法。

configuration.ts

image.png

效果展示

在前端页面创建一个 book 表,有两个字段,name和author。

image.png

使用 postman 调用接口测试

创建数据

image.png

更新数据

image.png

查询数据

image.png

自定义条件查询

image.png

分页查询

image.png

删除数据

image.png

image.png

总结

这篇文章给大家分享了使用 midway + typeorm 动态建表以及通过接口操作表里数据一个非常简单的小 demo,后面会给大家分享复杂的东西,比如表与表关联查询,以及逻辑编排等功能。