【JSqlParser】Java使用JSqlParser解析SQL语句总结

打印 上一主题 下一主题

主题 1853|帖子 1853|积分 5559

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
简述

Java解析SQL语句有很多工具都可以做到,好比Mybatis、Druid、目前用来用去最全面的仍然是Jsqlparser,它是一个Github上的开源项目,JSqlParser是一个用于解析SQL语句的Java库,它可以帮助开发者分析和操作SQL语句的结构。无论是从事数据库开发、SQL性能优化,照旧需要解析SQL语句以进行其他操作,JSqlParser都能提供强大的支持
特点


  • 支持多种SQL方言:JSqlParser支持多种数据库的SQL方言,包罗MySQL、Oracle、PostgreSQL等,这使得在不同数据库之间进行SQL语句解析变得更加方便。
  • 灵活的操作:JSqlParser以多种方式操作SQL语句,例如修改查询条件、提取表名、列名等,乃至整个SQL语句中使用到的函数,从而满意各种需求。
  • 易于集成:JSqlParser可以轻松集成到您的Java项目中,只需将其作为依靠项添加到项目中即可。
  • 社区支持:JSqlParser拥有一个活跃的社区,许多开发者为其提供贡献,使得这个工具不断得到改进和优化,它的紧张操刀人manticore-projects (github.com) 也非常负责并乐意解答各种标题和参与讨论
环境准备

将Jsqlparser直接添加到项目中
Maven:
  1. <dependency>
  2.     <groupId>com.github.jsqlparser</groupId>
  3.     <artifactId>jsqlparser</artifactId>
  4.     <version>4.9</version>
  5. </dependency>
复制代码
Gradle:
  1. implementation("com.github.jsqlparser:jsqlparser:4.9")
复制代码
快速使用

使用原则

假设现在有一条简朴的SQL语句需要拿来解析,起首应该保证这个SQL在结构上是没有标题的,最好是放在数据库中可以直接运行的,不夹杂不应该的标点符号,那样解析起来才不会出错
使用案例:
以下是一个简朴的SQL语句,并且这句SQL没有违反原则,是一条根本可以正常运行的SQL语句
  1. SELECT id,name,nickname,age,job,department FROM staff_member WHERE nickname= "刘"
复制代码
解析SQL语句

接下来使用Jsqlparser去解析语句,此中第二行则是最根本的,将SQL语句字符串拿来解析,如果这句SQL语句违反了原则,例如存在特别标点符号或者不符合SQL语句,那么在第二行就会产生异常
  1. String sql = "SELECT id,name,nickname,age,job,department FROM staff_member WHERE nickname= '刘'";
  2. // Parse SQL
  3. Statement statement = CCJSqlParserUtil.parse(sql);
  4. Select selectStatement = (Select) statement;
  5. log.info("==> JsqlParser SQL: {}", selectStatement.toString());
复制代码
正常情况下,将得到一个包含各种属性的statement,这意味着一条SQL乐成被解析,并被赋予到一个对象的各个属性中

认识Statement

熟悉JDBC的程序员一样寻常都知道Statement,其实就是语句的意思,不过在Jsqlparser中Statement已经面向对象,被设计成了一个interface,之以是设计成interface大概都可以猜到,因为Jsqlparser既然要去解析SQL,那必然要对SQL语句做区分,到底是Select、照旧Insert、照旧Delete、乃至是Create,而Jsqlparser对每种语句都做了一个封装,它们都继承了Statement

以是一条SQL语句,根据不怜悯况,都有适配的对象,例如Select语句对应着 net.sf.jsqlparser.statement.select.Select对象,而Insert也有本身的对象,以是我们都可以通过将Statement强转为它所对应的对象来获取或改变此中的属性,这也是解析SQL的一大目的
其实在Jsqlparser乐成解析SQL语句之后,statement就已经有了它的类型
  1. String sql = "SELECT id,name,nickname,age,job,department FROM staff_member WHERE nickname= '刘'";
  2.         // Parse SQL
  3. Statement statement = CCJSqlParserUtil.parse(sql);
  4. if(statement instanceof Select){
  5.        Select selectStatement = (Select) statement;
  6.       log.info("==> JsqlParser SQL: {}", selectStatement.toString());
  7. }
  8. if(statement instanceof Insert){
  9.        Insert insertStatement = (Insert) statement;
  10.        log.info("==> JsqlParser SQL: {}", insertStatement.toString());
  11. }
  12. if(statement instanceof Update){
  13.       Update updateStatement = (Update) statement;
  14.       log.info("==> JsqlParser SQL: {}", updateStatement.toString());
  15. }
  16. if (statement instanceof Delete) {
  17.       Delete deleteStatement = (Delete) statement;
  18.       log.info("==> JsqlParser SQL: {}", statement.toString());
  19. }
复制代码
分析语句

查询语句

在statement乐成解析SQL语句之后,通过PlainSelect就可以拿到SQL语句中的各个元素
  1. String sql = "SELECT id,name,nickname,age,job,department FROM staff_member WHERE nickname= '刘'";
  2. // Parse SQL
  3. Statement statement = CCJSqlParserUtil.parse(sql);
  4. if(statement instanceof Select){
  5.     Select selectStatement = (Select) statement;
  6.     log.info("==> JsqlParser SQL: {}", selectStatement.toString());
  7.     PlainSelect plainSelect = selectStatement.getPlainSelect();
  8.     log.info("==> FromItem: {}", plainSelect.getFromItem());
  9.     log.info("==> SelectItem: {}",plainSelect.getSelectItems());
  10.     log.info("==> Where: {}",plainSelect.getWhere());
  11. }
复制代码
运行效果:
  1. ==> JsqlParser SQL: SELECT id, name, nickname, age, job, department FROM staff_member WHERE nickname = '刘'
  2. ==> FromItem: staff_member
  3. ==> SelectItem: [id, name, nickname, age, job, department]
  4. ==> Where: nickname = '刘'
复制代码
PlainSelect常用方法:



  • 获取和设置表(From子句):

    • FromItem getFromItem(): 获取FROM子句中的表或子查询。
    • void setFromItem(FromItem fromItem): 设置FROM子句中的表或子查询。

  • 获取和设置选择项(SelectItems):

    • List<SelectItem> getSelectItems(): 获取SELECT子句中的选择项列表。
    • void setSelectItems(List<SelectItem> selectItems): 设置SELECT子句中的选择项列表。

  • 获取和设置WHERE子句:

    • Expression getWhere(): 获取WHERE子句的条件表达式。
    • void setWhere(Expression where): 设置WHERE子句的条件表达式。

  • 获取和设置GROUP BY子句:

    • List<Expression> getGroupByColumnReferences(): 获取GROUP BY子句中的列引用列表。
    • void setGroupByColumnReferences(List<Expression> groupByColumnReferences): 设置GROUP BY子句中的列引用列表。

  • 获取和设置ORDER BY子句:

    • List<OrderByElement> getOrderByElements(): 获取ORDER BY子句中的排序元素列表。
    • void setOrderByElements(List<OrderByElement> orderByElements): 设置ORDER BY子句中的排序元素列表。

  • 获取和设置LIMIT子句:

    • Limit getLimit(): 获取LIMIT子句。
    • void setLimit(Limit limit): 设置LIMIT子句。

  • 获取和设置DISTINCT关键字:

    • boolean isDistinct(): 检查SELECT语句是否使用了DISTINCT关键字。
    • void setDistinct(boolean distinct): 设置SELECT语句是否使用DISTINCT关键字。

  • 获取和设置INTO子句(用于SELECT INTO语句):

    • SubSelect getIntoTables(): 获取INTO子句中的表。
    • void setIntoTables(SubSelect intoTables): 设置INTO子句中的表。

  • 获取和设置HAVING子句:

    • Expression getHaving(): 获取HAVING子句的条件表达式。
    • void setHaving(Expression having): 设置HAVING子句的条件表达式。

  • 获取和设置别名:

    • String getAlias(): 获取SELECT语句的别名。
    • void setAlias(String alias): 设置SELECT语句的别名。

  • 获取和设置子查询(SubSelect):

    • SubSelect getSubSelect(): 获取子查询。
    • void setSubSelect(SubSelect subSelect): 设置子查询。

  • 获取和设置团结查询(Union):

    • List<lainSelect> getUnion(): 获取团结查询的SELECT语句列表。
    • void setUnion(List<lainSelect> union): 设置团结查询的SELECT语句列表。

新增语句

新增语句和查询语句一样,只不过由于Insert没有Select语句那么复杂,以是Jsqlparsert并没有专门设计一个类似PlainSelect extend Select如许一个类,而是直接通过Insert对象就可以获取和操作,Insert语句中的内容
  1. String sql = "INSERT INTO employees (employee_id, employee_name, department) VALUES (1, 'John Doe', 'Human Resources')";
  2. // Parse SQL
  3. Statement statement = CCJSqlParserUtil.parse(sql);
  4. if (statement instanceof Insert) {
  5.     Insert insertStatement = (Insert) statement;
  6.     log.info("==> JsqlParser SQL: {}", insertStatement.toString());
  7.     log.info("==> Table: {}", insertStatement.getTable());
  8.     log.info("==> Columns: {}", insertStatement.getColumns());
  9.     log.info("==> ItemsList: {}", insertStatement.getValues());
  10. }
复制代码
运行效果:
  1. ==> JsqlParser SQL: INSERT INTO employees (employee_id, employee_name, department) VALUES (1, 'John Doe', 'Human Resources')
  2. ==> Table: employees
  3. ==> Columns: employee_id, employee_name, department
  4. ==> ItemsList: VALUES (1, 'John Doe', 'Human Resources')
复制代码
Insert常用方法



  • Table getTable(): 获取插入语句中的目标表。
  • List<Column> getColumns(): 获取插入语句中要插入的列的列表。
  • ItemsList getValues(): 获取插入语句中的值列表,可以是单个值或者子查询。
  • String getPrefix(): 获取INSERT关键字前的前缀,如INSERT INTO或者INSERT IGNORE。
  • void setTable(Table table): 设置插入语句中的目标表。
  • void setColumns(List<Column> columns): 设置插入语句中要插入的列的列表。
  • void setValues(ItemsList values): 设置插入语句中的值列表。
  • void setPrefix(String prefix): 设置INSERT关键字前的前缀。
更新语句

Update和Insert是一样的,内容相对于Select较为简朴,通过Update对象即可获得相干内容
  1. String sql = "UPDATE employees SET department = 'Human Resources' WHERE employee_id = 1";
  2. // Parse SQL
  3. Statement statement = CCJSqlParserUtil.parse(sql);
  4. if (statement instanceof Update) {
  5.     Update updateStatement = (Update) statement;
  6.     log.info("==> JsqlParser SQL: {}", updateStatement.toString());
  7.     Table table = updateStatement.getTable();
  8.     log.info("Table Name: {}", table.getName());
  9.     log.info("==> Columns: {}", updateStatement.getColumns());
  10.     // 获取更新项
  11.     List<UpdateSet> updateSets = updateStatement.getUpdateSets();
  12.     for (UpdateSet updateSet : updateSets) {
  13.         for (Expression expression : updateSet.getColumns()) {
  14.             log.info("==> Expression: {}", expression.toString());
  15.         }
  16.     }
  17.     log.info("==> ItemsList: {}", updateStatement.getExpressions());
  18.     Expression where = updateStatement.getWhere();
  19.     log.info("==> Where: {}", where.toString());
  20. }
复制代码
运行效果
  1. ==> JsqlParser SQL: UPDATE employees SET department = 'Human Resources' WHERE employee_id = 1
  2. Table Name: employees
  3. ==> Columns: department
  4. ==> Expression: department
  5. ==> ItemsList: 'Human Resources'
  6. ==> Where: employee_id = 1
复制代码
删除语句

  1.         String sql = "DELETE FROM table_name WHERE condition";
  2.         Statement statement = CCJSqlParserUtil.parse(sql);
  3.         if (statement instanceof Delete) {
  4.             Delete deleteStatement = (Delete) statement;
  5.             // 获取要删除的表
  6.             Table table = deleteStatement.getTable();
  7.             System.out.println("Table Name: " + table.getName());
  8.             // 获取WHERE条件
  9.             Expression where = deleteStatement.getWhere();
  10.             System.out.println("Where Condition: " + where.toString());
  11.         }
复制代码
运行效果:
  1. Table Name: table_name
  2. Where Condition: condition
复制代码
从SQL语句中提取表名

  1. Statement statement = CCJSqlParserUtil.parse("SELECT * FROM MY_TABLE1");
  2. Select selectStatement = (Select) statement;
  3. TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
  4. List<String> tableList = tablesNamesFinder.getTableList(selectStatement);
复制代码
最终tableList里将存入所有给出的SQL语句中的表名,以上案例只有一个表名
为SQL语句各个字段表达式添加别名

  1. String sql = "SELECT id,name,nickname,age,job,department FROM staff_member WHERE nickname= '刘'";
  2. // Parse SQL
  3. Statement statement = CCJSqlParserUtil.parse(sql);
  4. if(statement instanceof Select ){
  5.     Select selectStatement = (Select) statement;
  6.     final AddAliasesVisitor instance = new AddAliasesVisitor();
  7.     instance.setPrefix("t");
  8.     selectStatement.accept(instance);
  9.     log.info("==> JSqlParser finalSQL: {}", selectStatement);
  10. }
复制代码
动态加字段加表达式加条件

使用SelectUtils,为一个Select语句,增长查询的字段

  1. Select select = (Select) CCJSqlParserUtil.parse("select mydate from mytable");
  2. SelectUtils.addExpression(select, new Column("mylocation"));
复制代码
增长一个表达式

  1. Select select = (Select) CCJSqlParserUtil.parse("select a from mytable");
  2. SelectUtils.addExpression(select, new Column("b"));
  3. assertEquals("SELECT a, b FROM mytable", select.toString());
  4. Addition add = new Addition();
  5. add.setLeftExpression(new LongValue(5));
  6. add.setRightExpression(new LongValue(6));
  7. SelectUtils.addExpression(select, add);
  8. assertEquals("SELECT a, b, 5 + 6 FROM mytable", select.toString());
复制代码
增长一个Join

动态添加Join,可以为Join增长表达式,以及设置Join的表,并且通过setLeft()、setRight()、setInner()可以设置join的方向,最终它会生成对应的SQL语句
  1. Select select = (Select) CCJSqlParserUtil.parse("select a from mytable");
  2. final EqualsTo equalsTo = new EqualsTo();
  3. equalsTo.setLeftExpression(new Column("a"));
  4. equalsTo.setRightExpression(new Column("b"));
  5. Join addJoin = SelectUtils.addJoin(select, new Table("mytable2"), equalsTo);
  6. addJoin.setLeft(true);
  7. assertEquals("SELECT a FROM mytable LEFT JOIN mytable2 ON a = b", select.toString());
复制代码
用SelectUtils构建一个SQL语句

下面是SelectUtils里面的一些方法,可以看到不光是为查询语句增长表达式、Join和分组,其次还可以使用build等方法去构建一个SQL语句

这里是一个案例,构建了一个查询语句,此中也使用到了addGroupBy
  1. Select select = SelectUtils.buildSelectFromTableAndExpressions(new Table("mytable"),
  2.                 new Column("a"), new Column("b"));
  3. SelectUtils.addExpression(select, new Column("c"));
  4. final EqualsTo equalsTo = new EqualsTo();
  5. equalsTo.setLeftExpression(new Column("id"));
  6. equalsTo.setRightExpression(new Column("1"));
  7. SelectUtils.addGroupBy(select, new Column("d"));
  8. log.info("==> JsqlParser Build SQL: {}", select.toString());
复制代码
输出效果:
  1. ==> JsqlParser Build SQL: SELECT a, b, c FROM mytable GROUP BY d
复制代码
简短的总结

上面的代码虽然不少,但实际上真正需要熟悉的只有一个,就是直接调用CCJSqlParserUtil.parse(sql);去获得Statement,然后通过Statement去操作和获取解析后的SQL中的内容,非常简朴方便

实际应用场景

说了那么多JSQLPARSER的使用,大概很多朋友并不能联想到有哪些详细可以用到它的地方,实际上想要开发一个优秀的软件产物,那么细节是少不了的,SQL是BS软件的本质之一,那么针对SQL,我们能做的另有很多,以下列举几个常见的场景
   

  • SQL审计和分析:

    • 审计SQL语句,检查是否包含埋伏的安全漏洞,如SQL注入。
    • 分析SQL语句的性能,检查是否存在可以优化的查询条件。

  • 数据库迁徙和同步:

    • 在迁徙数据库时,使用JSqlParser解析源数据库的SQL语句,并生成目标数据库的相应语句。
    • 数据库同步工具可以使用JSqlParser来解析和生成SQL语句,以实现数据的同步。

  • 动态SQL生成:

    • 应用程序需要生成动态SQL语句以实行不同的操作,JSqlParser可以用来解析这些动态生成的SQL语句。

  • SQL测试和验证:

    • 开发过程中,使用JSqlParser来验证SQL语句的精确性。
    • 单位测试中,使用JSqlParser来解析和实行测试用例中的SQL语句。

  • SQL注入防护:

    • 在应用程序中,使用JSqlParser来解析和分析用户输入的SQL查询,以防止SQL注入攻击。

  • 数据库管理工具:

    • 数据库管理工具可以使用JSqlParser来解析和显示SQL语句的结构,帮助开发者理解查询的逻辑。

  • 代码生成:

    • 在生成数据库访问层代码时,使用JSqlParser来解析SQL语句,并生成相应的数据访问对象(DAO)或查询对象(DTO)。

  • SQL格式化:

    • 使用JSqlParser来格式化SQL语句,使其更易于阅读和理解。

  • SQL优化:

    • 通太过析SQL语句的结构,可以提出性能优化建议。

  • 数据处理工具:

    • 在数据处理和转换工具中,使用JSqlParser来解析和生成SQL语句,以实现数据的导入和导出。

  在Springboot+Mybaits中使用

如果使用纯原生Mybatis那么我们需要手动在maven中参加Jsqlparser的支持,但如果使用Mybatis plus,那么就无需本身再引用,Mybaits plus自带Jsqlparser
上面举的很多例子都很简朴,拿一个SQL语句解析而已,这种情况是手动化的,通常见于单位测试等情况,但如果在项目中想要通过被动的方式,让项目本身去解析SQL语句,就需要分析项目的详细情况,例如在Mybatis中通过Interceptor就可以获得到项目中真正去实行的SQL语句,详见:Mybatis 的 Interceptor(拦截器) 与 JSqlparser 结合解析SQL 使SpringBoot项目多数据库兼容的尝试_mybatis设置jsqlparser-CSDN博客
通过Mybatis的拦截器,我们拿到了项目实行的SQL语句,再通过Jsqlparser去解析,并做一定的处理,例如以上提到的那些实际应用场景
高级特性(很实用)

Jsqlparser在解析SQL语句的过程中,每一个节点都会被解析成一个叫SimpleNode的对象,它包含着各个节点的属性,这仿佛就像Dom4j解析XML的时候所有的元素都视为Node一样,解析之后的内容都是节点,而循环这些节点,Jsqlparser给出了相应的方法,提供了用于遍历节点的接口CCJSqlParserVisitor,而它的默认实现则是CCJSqlParserDefaultVisitor,在这里创建一个本身的类,并通过继承 CCJSqlParserDefaultVisitor 重写它的 visit 方法,便可以实现本身的计谋,更加方便的去操作解析内容
  1. public class SQLModifier extends CCJSqlParserDefaultVisitor {
  2.     @Override
  3.     public Object visit(SimpleNode node, Object data) {
  4.         Object value = node.jjtGetValue();
  5.         switch (node.getId()) {
  6.             case CCJSqlParserTreeConstants.JJTTABLENAME:
  7.                 break;
  8.             case CCJSqlParserTreeConstants.JJTCOLUMN:
  9.                 break;
  10.             case CCJSqlParserTreeConstants.JJTFUNCTION:
  11.                 break;
  12.             default:
  13.                 break;
  14.         }
  15.         return super.visit(node, data);
  16.     }
  17. }
复制代码
调用自定义的Visitor

  1. String originalSql = "select * from user where id = 1";
  2. CCJSqlParser parser = CCJSqlParserUtil.newParser(originalSql);
  3. Statement statement = parser.Statement();
  4. parser.getASTRoot().jjtAccept(sqlTestModifier, null);
复制代码
以上代码做了一个自定义的visitor,重写的visit方法中可以看到形参SimpleNode,而调用这个自定义的Visitor之后,语句则会被拆解,依次进入到visit方法中,通过node.jjtGetValue可以获得节点信息,而node.getId()实则是获取节点的类型,而Switch-case中的常量分别代表了在解析SQL语句时,生成的抽象语法树AST (abstract syntax tree)中不同类型的节点,每个节点对应一个特定的SQL构造,如SELECT、FROM、WHERE等。下面是对这些常量代表的SQL构造的简要分析:


  • JJTSTATEMENT: 代表一个SQL语句。
  • JJTVOID: 可能代表一个空语句或者不返回效果的语句。
  • JJTBLOCK: 代表一个语句块,可能包含多个语句。
  • JJTSTATEMENTS: 代表一个包含多个语句的列表。
  • JJTCOLUMN: 代表一个列名。
  • JJTTABLENAME: 代表一个表名。
  • JJTSELECT: 代表一个SELECT查询。
  • JJTPARENTHESEDSELECT: 代表被括号困绕的SELECT查询。
  • JJTLATERALVIEW: 代表LATERAL VIEW子句,常用于Hive SQL。
  • JJTFORCLAUSE: 代表FOR子句。
  • JJTLATERALSUBSELECT: 代表LATERAL子查询。
  • JJTPLAINSELECT: 代表一个简朴的SELECT查询(不包含UNION等)。
  • JJTSETOPERATIONLIST: 代表一个集合操作列表,好比UNION, EXCEPT, INTERSECT。
  • JJTWITHITEM: 代表WITH子句中的单个项。
  • JJTSELECTITEM: 代表SELECT子句中的一个项,可能是列名、表达式等。
  • JJTJOINEREXPRESSION: 代表JOIN操作的表达式。
  • JJTLIMITWITHOFFSET: 代表LIMIT和OFFSET子句。
  • JJTPLAINLIMIT: 代表一个简朴的LIMIT子句。
  • JJTEXPRESSION: 代表一个表达式。
  • JJTREGULARCONDITION: 代表一个常规条件(如WHERE子句中的条件)。
  • JJTINEXPRESSION: 代表IN表达式。
  • JJTLIKEEXPRESSION: 代表LIKE表达式。
  • JJTSIMILARTOEXPRESSION: 代表SIMILAR TO表达式。
  • JJTISDISTINCTEXPRESSION: 代表IS DISTINCT FROM表达式。
  • JJTEXPRESSIONLIST: 代表一个表达式列表。
  • JJTPRIMARYEXPRESSION: 代表一个紧张表达式。
  • JJTCONNECTBYROOTOPERATOR: 代表CONNECT BY ROOT操作符。
  • JJTCASEWHENEXPRESSION: 代表CASE WHEN表达式。
  • JJTFUNCTION: 代表一个函数调用。
  • JJTSEQUENCE: 代表一个序列。
  • JJTSYNONYM: 代表一个同义词。
Visit常见应用场景

目前我们知道,通过Mybatis 的 interceptor可以拦截到所有实行的SQL语句,而在 自定义的interceptor中调用自定义的visit,就可以对项目中所有运行的SQL做一个拦截并处理,那么详细可以做哪些骚操作呢

  • SQL语句重写: 在某些数据库系统中,为了优化性能或满意特定的需求,可能需要重写SQL语句。通过自定义访问者,可以在AST(abstract syntax tree)层面进行这些操作
  • 元数据提取: 自定义访问者可以用来提取SQL语句中的元数据,好比查询涉及的所有表名、列名、函数等,这些信息可以用于构建数据库的概要图或进行数据治理。
  • 数据屏蔽: 在需要对敏感数据进行屏蔽的应用中,可以通过自定义访问者来识别并修改涉及敏感数据的查询,以确保在查询效果中不会袒露敏感信息。
  • 动态查询构建: 在需要动态构建SQL查询的应用中,可以通过自定义访问者来解析模板SQL语句,并根据实际参数动态替换模板中的占位符,从而构建出完整的SQL语句。
  • 缓存计谋决策: 根据SQL查询的特征,可以通过自定义访问者来判断查询效果是否得当缓存,以及应该使用什么样的缓存计谋。
总结

Jsqlparser非常轻易上手使用,而它也办理相识析SQL语句的标题,结合Springboot 和 mybatis,可以设计自定义插件,就像Mybatis plus的分页插件那样,可以开发本身系统需求的业务处理功能,方便项目业务的时间,乃至可以拿来提高效率,毕竟总有一些时候,对SQL的解析是绕不开的。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

河曲智叟

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表