若依-代码自动生成解析
源码阅读技巧
- 可以从前端开始,前端的url请求暗含了业务流程,借助F12(打开浏览器控制台)后的network可以更快的找到对API的请求
- 根据url对后端项目进行搜索,使用ctrl+shift+f打开全局搜索,例如搜索“/login”,可以查询出项目包含/login的文本
代码自动生成功能
会生成 前端,后端,数据库sql,三个模块的代码
使用
- 在数据库建一张以sys_开头的表
- 在管理系统中选择系统工具—代码生成—导入—生成—下载代码
- 将下载得到的代码拷贝到自己的项目目录中
- 执行生成的sql文件,获得的代码包含三个模块,前端,后端,数据库sql,前面两个只要拷贝,sql文件需要手动执行
- 可以修改sys_menu这张表下的parent_id,使得menu处于你想要的位置,自动生成的id一般为3
sql解析
我这里生成的表为sys_goods,字段为goods_id,goods_name_goods_price,goods_status
自动生成的goodsMenu.sql由三个部分组成,具体如下
-- 菜单 SQL
insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
values('货物信息', '3', '1', 'goods', 'system/goods/index', 1, 0, 'C', '0', '0', 'system:goods:list', '#', 'admin', sysdate(), '', null, '货物信息菜单');
-
‘货物信息’ : 字段名字
-
‘3’ : 父id为3(父id后来被手动改成1,不然这条menu会在系统工具那里显示,而不是在系统管理那里显示)
-
‘1’ : 显示顺序为1
-
‘goods’ : 路由地址为“goods”
-
‘system/goods/index’ : 组件地址为’system/goods/index’(这里感觉有点问题,整个项目里并未使用过system/goods/index,也没拼接过该地址,但不影响界面显示和代码运行)
-
1 : 不是外链
-
0 : 内容进行缓存
-
‘C’ : 类型为菜单
-
‘0’ , ‘0’ : 显示菜单,且菜单状态正常
-
‘system:goods:list’,权限标志,在PermissionService类下的hasPermi(String permission)方法会使用到这个字段,从当前登录的用户中取出用户权限List,判断当前请求所需的permission是否在这个list中
-
‘#’ : 菜单icon,它回去找ruoyi-ui/src/assets/icons/svg/xxx.svg,将这个svg图标显示到menu的左边,没有改图标则为空
-
‘admin’ : 创建这条记录的人是’admin’
-
sysdate() : 返回现在时间的一个函数,返回格式为“yyyy-MM-dd HH:mm:ss”
-
‘’ : 修改人
-
null : 修改时间为空
-
‘货物信息菜单’ : 备注为’货物信息菜单’
-- 按钮父菜单ID
SELECT @parentId := LAST_INSERT_ID();
- 设置一个变量是 @parentId
- :=是赋值
- LAST_INSERT_ID() : 返回最后一个 INSERT 或 UPDATE 操作为 AUTO_INCREMENT 列设置的最新发生的值.
LAST_INSERT_ID是基于单个connection的, 不可能被其它的客户端连接改变,同时这个函数跟表无关联。
-- 按钮 SQL
insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
values('货物信息查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', 'system:goods:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
values('货物信息新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', 'system:goods:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
values('货物信息修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', 'system:goods:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
values('货物信息删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', 'system:goods:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
values('货物信息导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', 'system:goods:export', '#', 'admin', sysdate(), '', null, '');
整体思路和第一部分菜单 SQL差距不大,注意下@parentId是之前定义的变量,'F’是指按钮
前端文件
goods.js
import request from '@/utils/request'
导入ruoyi用于发送请求的axios实例
// 查询货物信息列表
export function listGoods(query) {
return request({
url: '/system/goods/list',
method: 'get',
params: query
})
}
发送请求,url为’/system/goods/list’,类型为get,参数由调用方传递
// 查询货物信息详细
export function getGoods(goodsId) {
return request({
url: '/system/goods/' + goodsId,
method: 'get'
})
}
// 新增货物信息
export function addGoods(data) {
return request({
url: '/system/goods',
method: 'post',
data: data
})
}
// 修改货物信息
export function updateGoods(data) {
return request({
url: '/system/goods',
method: 'put',
data: data
})
}
// 删除货物信息
export function delGoods(goodsId) {
return request({
url: '/system/goods/' + goodsId,
method: 'delete'
})
}
// 导出货物信息
export function exportGoods(query) {
return request({
url: '/system/goods/export',
method: 'get',
params: query
})
}
剩下的CRUD操作,和listGoods(query) ,没有本质区别
index.vue
主要由template和script两个部分组成
整体样式:
template 部分:
头部表单(el-form):
<el-form-item label="名称" prop="goodsName">
<el-input
v-model="queryParams.goodsName"
placeholder="请输入名称"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
- el-form-item : 表示这是element-ui下的一个标签,是一个item
- el-input : element-ui的输入框
- clearable:表示清空内容
- @keyup.enter.native : 表示对回车事件的处理,后面跟处理的方法名字
后面还跟着类似内容的价格和状态的item,解释如上
再之后则是一组button
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
- handleQuery:和前面处理回车时间的方法一样
- resetQuery:由两个部分组成,一个是resetForm,还有一个是handleQuery,第一个是为了清空内容,第二个用来刷新内容
四个功能按键:
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:goods:add']"
>新增</el-button>
</el-col>
...
</el-row>
-
gutter:gutter是指栅格间间隔,也可以使用offset,offset是指栅格左侧的间隔格数
-
v-hasPermi : ruoyi封装的一个指令权限,能简单快速的实现按钮级别的权限判断。v-permission
-
后面的修改、删除、导出原理类似,有个:disabled=“multiple”,是用来设置禁用情况的
el-table:
<el-table v-loading="loading" :data="goodsList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="编号" align="center" prop="goodsId" />
<el-table-column label="名称" align="center" prop="goodsName" />
<el-table-column label="价格" align="center" prop="goodsPrice" />
<el-table-column label="状态" align="center" prop="goodsStatus" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:goods:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:goods:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
script部分:
import { listGoods, getGoods, delGoods, addGoods, updateGoods, exportGoods } from "@/api/system/goods";
先从goods.js中导入方法
之后定义data()数据区,created()页面初始化前做的准备,methods方法区:
data区主要定义了一些常量
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 货物信息表格数据
goodsList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
goodsName: null,
goodsPrice: null,
goodsStatus: null
},
// 表单参数
form: {},
// 表单校验
rules: {
}
};
},
create(),调用了后面方法区methods中定义的方法
created() {
this.getList();
}
methods
methods: {
/** 查询货物信息列表 */
getList() {
this.loading = true;
listGoods(this.queryParams).then(response => {
this.goodsList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
goodsId: null,
goodsName: null,
goodsPrice: null,
goodsStatus: "0"
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.goodsId)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加货物信息";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const goodsId = row.goodsId || this.ids
getGoods(goodsId).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改货物信息";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.goodsId != null) {
updateGoods(this.form).then(response => {
this.msgSuccess("修改成功");
this.open = false;
this.getList();
});
} else {
addGoods(this.form).then(response => {
this.msgSuccess("新增成功");
this.open = false;
this.getList();
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const goodsIds = row.goodsId || this.ids;
this.$confirm('是否确认删除货物信息编号为"' + goodsIds + '"的数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return delGoods(goodsIds);
}).then(() => {
this.getList();
this.msgSuccess("删除成功");
})
},
/** 导出按钮操作 */
handleExport() {
const queryParams = this.queryParams;
this.$confirm('是否确认导出所有货物信息数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return exportGoods(queryParams);
}).then(response => {
this.download(response.msg);
})
}
}
后端文件
SysGoods 实体类
public class SysGoods extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 编号 */
private Long goodsId;
/** 名称 */
@Excel(name = "名称")
private String goodsName;
/** 价格 */
@Excel(name = "价格")
private Integer goodsPrice;
/** 状态(0正常 1不足) */
@Excel(name = "状态", readConverterExp = "0=正常,1=不足")
private String goodsStatus;
//。。。省略get和set方法
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("goodsId", getGoodsId())
.append("goodsName", getGoodsName())
.append("goodsPrice", getGoodsPrice())
.append("goodsStatus", getGoodsStatus())
.toString();
}
}
-
BaseEntity中主要是一些更新时间、创建者等公共而基础的属性
-
@Excel(name = “状态”, readConverterExp = “0=正常,1=不足”),导出到Excel的名字为状态,readConverterExp内容转换表达式,读入0,会转换为正常
-
ToStringBuilder,apache的commons-lang3的工具包里有一个ToStringBuilder类,使用IDEA自动生成的toString方法中,会使用“+”来拼接字符串,每次“+”都是在new一个新的对象,这比较消耗内存,使用ToStringBuilder可以减少内存消耗
SysGoodsController
@RestController//@Controller+@ResponseBody两个注解的结合,返回json数据
@RequestMapping("/system/goods")//映射路由
public class SysGoodsController extends BaseController
{
@Autowired
private ISysGoodsService sysGoodsService;
/**
* 查询货物信息列表
*/
@PreAuthorize("@ss.hasPermi('system:goods:list')")//检查当前用户是否拥有相关权限
@GetMapping("/list")
public TableDataInfo list(SysGoods sysGoods)
{
startPage();
List<SysGoods> list = sysGoodsService.selectSysGoodsList(sysGoods);
return getDataTable(list);
}
/**
* 导出货物信息列表
*/
@PreAuthorize("@ss.hasPermi('system:goods:export')")
@Log(title = "货物信息", businessType = BusinessType.EXPORT)
@GetMapping("/export")
public AjaxResult export(SysGoods sysGoods)
{
List<SysGoods> list = sysGoodsService.selectSysGoodsList(sysGoods);
ExcelUtil<SysGoods> util = new ExcelUtil<SysGoods>(SysGoods.class);
return util.exportExcel(list, "货物信息数据");//对list数据源将其里面的数据导入到excel表单
}
/**
* 获取货物信息详细信息
*/
@PreAuthorize("@ss.hasPermi('system:goods:query')")
@GetMapping(value = "/{goodsId}")
public AjaxResult getInfo(@PathVariable("goodsId") Long goodsId)//AjaxResult一个继承了HashMap的子类
{
return AjaxResult.success(sysGoodsService.selectSysGoodsById(goodsId));//实际上就返回了一个map,key为“success”,value为null
}
/**
* 新增货物信息
*/
@PreAuthorize("@ss.hasPermi('system:goods:add')")
@Log(title = "货物信息", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody SysGoods sysGoods)
{
return toAjax(sysGoodsService.insertSysGoods(sysGoods));//toAjax响应返回结果,影响行数大于0则返回success,否则为false
}
/**
* 修改货物信息
*/
@PreAuthorize("@ss.hasPermi('system:goods:edit')")
@Log(title = "货物信息", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody SysGoods sysGoods)
{
return toAjax(sysGoodsService.updateSysGoods(sysGoods));
}
/**
* 删除货物信息
*/
@PreAuthorize("@ss.hasPermi('system:goods:remove')")
@Log(title = "货物信息", businessType = BusinessType.DELETE)
@DeleteMapping("/{goodsIds}")
public AjaxResult remove(@PathVariable Long[] goodsIds)
{
return toAjax(sysGoodsService.deleteSysGoodsByIds(goodsIds));
}
}
SysGoodsService层只是简单的调用,dao层只是简单声明了方法,故不作详细解释
SysGoodsMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- dtd文件,Document Type Definition,用来规则定义文档规则的,用来验证下面的节点是否符合。mapper表示根节点 -->
<mapper namespace="com.ruoyi.system.mapper.SysGoodsMapper">
<!-- 声明dao层的那个接口的全类名 -->
<resultMap type="SysGoods" id="SysGoodsResult">
<!-- 声明JavaBean和数据库表字段的映射关系 -->
<result property="goodsId" column="goods_id" />
<result property="goodsName" column="goods_name" />
<result property="goodsPrice" column="goods_price" />
<result property="goodsStatus" column="goods_status" />
</resultMap>
<sql id="selectSysGoodsVo">
select goods_id, goods_name, goods_price, goods_status from sys_goods
</sql>
<select id="selectSysGoodsList" parameterType="SysGoods" resultMap="SysGoodsResult">
<!-- selectSysGoodsList 和方法名一样,parameterType,传入的参数的类型,resultMap封装规则 -->
<include refid="selectSysGoodsVo"/>
<!-- 复用sql -->
<where>
<if test="goodsName != null and goodsName != ''"> and goods_name like concat('%', #{goodsName}, '%')</if>
<!-- 动态拼接sql -->
<if test="goodsPrice != null "> and goods_price = #{goodsPrice}</if>
<if test="goodsStatus != null and goodsStatus != ''"> and goods_status = #{goodsStatus}</if>
</where>
</select>
<select id="selectSysGoodsById" parameterType="Long" resultMap="SysGoodsResult">
<include refid="selectSysGoodsVo"/>
where goods_id = #{goodsId}
</select>
<insert id="insertSysGoods" parameterType="SysGoods" useGeneratedKeys="true" keyProperty="goodsId">
insert into sys_goods
<trim prefix="(" suffix=")" suffixOverrides=",">
<!-- suffixOverrides去除后缀 -->
<if test="goodsName != null">goods_name,</if>
<if test="goodsPrice != null">goods_price,</if>
<if test="goodsStatus != null">goods_status,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="goodsName != null">#{goodsName},</if>
<if test="goodsPrice != null">#{goodsPrice},</if>
<if test="goodsStatus != null">#{goodsStatus},</if>
</trim>
</insert>
<update id="updateSysGoods" parameterType="SysGoods">
update sys_goods
<trim prefix="SET" suffixOverrides=",">
<if test="goodsName != null">goods_name = #{goodsName},</if>
<if test="goodsPrice != null">goods_price = #{goodsPrice},</if>
<if test="goodsStatus != null">goods_status = #{goodsStatus},</if>
</trim>
where goods_id = #{goodsId}
</update>
<delete id="deleteSysGoodsById" parameterType="Long">
delete from sys_goods where goods_id = #{goodsId}
</delete>
<delete id="deleteSysGoodsByIds" parameterType="String">
delete from sys_goods where goods_id in
<foreach item="goodsId" collection="array" open="(" separator="," close=")">
#{goodsId}
</foreach>
</delete>
</mapper>
生成原理解析
导入表
实际为简单的插入操作
前端请求
Request URL: http://localhost/dev-api/tool/gen/importTable?tables=test2
Referrer Policy: no-referrer-when-downgrade
后端接收
GenController.java下的importTableSave
/**
* 导入表结构(保存)
*/
@PreAuthorize("@ss.hasPermi('tool:gen:list')")//spring security中通过表达式控制方法权限
@Log(title = "代码生成", businessType = BusinessType.IMPORT)
@PostMapping("/importTable")
public AjaxResult importTableSave(String tables)
{
String[] tableNames = Convert.toStrArray(tables);//用逗号进行了split
// 查询表信息
List<GenTable> tableList = genTableService.selectDbTableListByNames(tableNames);//去数据库查这些表
genTableService.importGenTable(tableList);//将信息插入到gen_table和gen_column两个表中
return AjaxResult.success();
}
GenTableServiceImpl.java下的importGenTable
/**
* 导入表结构
*
* @param tableList 导入表列表
*/
@Override
@Transactional
public void importGenTable(List<GenTable> tableList)
{
String operName = SecurityUtils.getUsername();
try
{
for (GenTable table : tableList)
{
String tableName = table.getTableName();
GenUtils.initTable(table, operName);//这里是在初始化table的信息,将表转换为类,类所需的大部分信息,如包,前缀等信息都在generator.yml中配置,operName只是用来简单set一下是谁创建的而已
int row = genTableMapper.insertGenTable(table);//简单地调用mybatis的插入功能而已,动态sql需要判断是否为空,显得有些长
if (row > 0)
{
// 保存列信息
List<GenTableColumn> genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName);//这里根据表的名字查询该表的字段名字,用到了information_schema.columns,SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='数据库名'; 可以查询所有字段的name
for (GenTableColumn column : genTableColumns)
{
GenUtils.initColumnField(column, table);//将table的信息set到column中
genTableColumnMapper.insertGenTableColumn(column);//将column插入到GenTableColumn表中
}
}
}
}
catch (Exception e)
{
throw new CustomException("导入失败:" + e.getMessage());
}
}
生成代码
前端请求
在点击了生成按钮后,前端向后端发起了如下请求
Request URL: http://localhost/dev-api/tool/gen/batchGenCode?tables=sys_goods
Request Method: GET
Status Code: 204 Intercepted by the IDM Advanced Integration
Remote Address: 127.0.0.1:80
Referrer Policy: no-referrer-when-downgrade
后端接收
在GenController.java类下,有如下的方法用于接收请求
/**
* 批量生成代码
*/
@PreAuthorize("@ss.hasPermi('tool:gen:code')")
@Log(title = "代码生成", businessType = BusinessType.GENCODE)
@GetMapping("/batchGenCode")
public void batchGenCode(HttpServletResponse response, String tables) throws IOException
{
String[] tableNames = Convert.toStrArray(tables);
byte[] data = genTableService.downloadCode(tableNames);
genCode(response, data);//genCode:用来设置相应头,并将数据data传递给前端
}
- genTableService.downloadCode(tableNames),由于自动注入的原因,它会调用genTableServiceImpl下的downloadCode
@Override
public byte[] downloadCode(String[] tableNames)
{
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ZipOutputStream zip = new ZipOutputStream(outputStream);
for (String tableName : tableNames)//我只选了一张表来进行生成,所以tableNames的length为1
{
generatorCode(tableName, zip);
}
IOUtils.closeQuietly(zip);
return outputStream.toByteArray();
}
- generatorCode(tableName, zip),调用了当前类(genTableServiceImpl)下的方法
private void generatorCode(String tableName, ZipOutputStream zip)
{
// 查询表信息
GenTable table = genTableMapper.selectGenTableByName(tableName);//【1】
// 设置主子表信息
setSubTable(table);
// 设置主键列信息
setPkColumn(table);
VelocityInitializer.initVelocity();
VelocityContext context = VelocityUtils.prepareContext(table);//将table中的属性读出然后set到VelocityContext中,VelocityContex中有一个类型为Map的context属性
// 获取模板列表
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory());
for (String template : templates)
{
// 渲染模板
StringWriter sw = new StringWriter();
Template tpl = Velocity.getTemplate(template, Constants.UTF8);//tpl是模板文件,就是以vm结尾的那些文件
tpl.merge(context, sw);//这里是将context中的内容填充到tpl模板中留下的那些空中,最后将所有信息都填充到sw中,StringWriter是Writer和StringBuffer的结合体
try
{
// 添加到zip
zip.putNextEntry(new ZipEntry(VelocityUtils.getFileName(template, table)));
IOUtils.write(sw.toString(), zip, Constants.UTF8);
IOUtils.closeQuietly(sw);
zip.flush();//执行后浏览器基本就可以进行下载了
zip.closeEntry();
}
catch (IOException e)
{
log.error("渲染模板失败,表名:" + table.getTableName(), e);
}
}
}
- 【1】genTableMapper.selectGenTableByName(tableName);这里会调用dao接口,最后的sql xml文件如下,gen_table表的信息在导入的时候已经添加
<select id="selectGenTableByName" parameterType="String" resultMap="GenTableResult">
SELECT t.table_id, t.table_name, t.table_comment, t.sub_table_name, t.sub_table_fk_name, t.class_name, t.tpl_category, t.package_name, t.module_name, t.business_name, t.function_name, t.function_author, t.gen_type, t.gen_path, t.options, t.remark,
c.column_id, c.column_name, c.column_comment, c.column_type, c.java_type, c.java_field, c.is_pk, c.is_increment, c.is_required, c.is_insert, c.is_edit, c.is_list, c.is_query, c.query_type, c.html_type, c.dict_type, c.sort
FROM gen_table t
LEFT JOIN gen_table_column c ON t.table_id = c.table_id
where t.table_name = #{tableName} order by c.sort
</select>