commit 83e9a89c6dbe49b53c7da76e010a3c2ff7bc99d9 Author: 开源海哥 Date: Wed Mar 1 13:10:17 2023 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9cc7d853 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +target +logs +.idea +*.iml +*.log +.logs +*.iws +*.report +*.classpath +.DS_Store +.ossutil_checkpoint +*.patch +~$* +node_modules +yarn.lock +dist +package-lock.json +remarks.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/changes.txt b/changes.txt new file mode 100644 index 00000000..97f7cae6 --- /dev/null +++ b/changes.txt @@ -0,0 +1,2 @@ +mybatis-flex v1.0.0-beta.1: +init mybatis-flex \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 00000000..e69de29b diff --git a/mybatis-flex-annotation/pom.xml b/mybatis-flex-annotation/pom.xml new file mode 100644 index 00000000..a15d5012 --- /dev/null +++ b/mybatis-flex-annotation/pom.xml @@ -0,0 +1,48 @@ + + + + parent + com.mybatis-flex + 1.0.0-beta.1 + + 4.0.0 + + mybatis-flex-annotation + + + + 8 + 8 + + + + + + src/main/resources + + **/* + + + + *.properties + + + + + + + + maven-compiler-plugin + 3.8.1 + + none + 1.8 + 1.8 + + + + + + \ No newline at end of file diff --git a/mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/Column.java b/mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/Column.java new file mode 100644 index 00000000..9916c62b --- /dev/null +++ b/mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/Column.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface Column { + + /** + * 字段名称 + */ + String value() default ""; + + /** + * update 的时候自动赋值,这个值会直接被拼接到 sql 而不通过参数设置 + */ + String update() default ""; + + /** + * insert 的时候默认值,这个值会直接被拼接到 sql 而不通过参数设置 + */ + String insert() default ""; + + /** + * 是否忽略该字段,可能只是业务字段,而非数据库对应字段 + */ + boolean ignore() default false; + + /** + * 是否是大字段,大字默认不会对齐进行查询,除非指定查询 + */ + boolean isLarge() default false; + + /** + * 是否是逻辑删除字段,一张表中只能存在 1 一个逻辑删除字段 + */ + boolean isLogicDelete() default false; + + /** + * 是否为乐观锁字段,如果是的话更新的时候会去检测当前版本号,更新成功的话会设置当前版本号 +1 + * 只能用于数值的字段 + */ + boolean version() default false; + +} \ No newline at end of file diff --git a/mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/Id.java b/mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/Id.java new file mode 100644 index 00000000..1e55749b --- /dev/null +++ b/mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/Id.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.annotation; + +import com.mybatisflex.core.enums.KeyType; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface Id { + + /** + * ID 生成策略,默认为 auto + * + * @return 生成策略 + */ + KeyType keyType() default KeyType.Auto; + + /** + * 若 keyType 类型是 sequence, value 则代表的是 + * sequence 序列的 sql 内容 + * 例如:select SEQ_USER_ID.nextval as id from dual + * + * 若 keyType 是 Generator,value 则代表的是使用的那个 keyGenerator 的名称 + * + */ + String value() default ""; + + + /** + * sequence 序列执行顺序 + * 是在 entity 数据插入之前执行,还是之后执行,之后执行的一般是数据主动生成的 id + * + * @return 执行之前还是之后 + */ + boolean before() default true; +} \ No newline at end of file diff --git a/mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/Table.java b/mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/Table.java new file mode 100644 index 00000000..2adb1e2e --- /dev/null +++ b/mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/Table.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface Table { + + /** + * 显式指定表名称 + */ + String value(); + + /** + * 数据库的 schema + */ + String schema() default ""; + + /** + * 是否使用 mybatis 缓存 + */ + boolean useCached() default false; + + /** + * 默认为 驼峰属性 转换为 下划线字段 + */ + boolean camelToUnderline() default true; +} \ No newline at end of file diff --git a/mybatis-flex-annotation/src/main/java/com/mybatisflex/core/enums/KeyType.java b/mybatis-flex-annotation/src/main/java/com/mybatisflex/core/enums/KeyType.java new file mode 100644 index 00000000..09ab71b4 --- /dev/null +++ b/mybatis-flex-annotation/src/main/java/com/mybatisflex/core/enums/KeyType.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.enums; + +/** + * ID 生成策略 + */ +public enum KeyType { + + /** + * 自增的方式 + */ + Auto, + + /** + * 通过执行数据库 sql 生成 + * 例如:select SEQ_USER_ID.nextval as id from dual + */ + Sequence, + + /** + * 通过 IKeyGenerator 生成器生成 + */ + Generator, + + /** + * 其他方式,比如说在代码层用户手动设置 + */ + None, +} diff --git a/mybatis-flex-annotation/src/main/java/com/mybatisflex/processer/MyBatisFlexProps.java b/mybatis-flex-annotation/src/main/java/com/mybatisflex/processer/MyBatisFlexProps.java new file mode 100644 index 00000000..7d430339 --- /dev/null +++ b/mybatis-flex-annotation/src/main/java/com/mybatisflex/processer/MyBatisFlexProps.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.processer; + + +import java.io.*; +import java.net.URL; +import java.util.Properties; + + +class MyBatisFlexProps { + protected Properties properties = null; + private static final String DEFAULT_ENCODING = "UTF-8"; + + public MyBatisFlexProps(String fileName) { + this(fileName, DEFAULT_ENCODING); + } + + public MyBatisFlexProps(String fileName, String encoding) { + properties = new Properties(); + InputStream inputStream = null; + try { + inputStream = getResourceAsStreamByCurrentThread(fileName); + + if (inputStream == null) { + inputStream = getResourceAsStreamByClassloader(fileName); + } + + // 当系统未编译的时候,开发环境下的 resources 目录下的 properties 文件不会自动被 copy 到 target/classes 目录下 + // 此时,需要主动去探测 resources 目录的文件 + if (inputStream == null) { + URL resourceURL = MyBatisFlexProps.class.getResource("/"); + if (resourceURL != null) { + String classPath = resourceURL.toURI().getPath(); + + if (removeSlashEnd(classPath).endsWith("classes")) { + + //from classes path + File propFile = new File(classPath, fileName); + if (propFile.exists() && propFile.isFile()) { + inputStream = new FileInputStream(propFile); + } + + //from resources path + else { + File resourcesDir = new File(classPath, "../../src/main/resources"); + propFile = new File(resourcesDir, fileName); + if (propFile.exists() && propFile.isFile()) { + inputStream = new FileInputStream(propFile); + } + } + } + } + } + + if (inputStream != null) { + properties.load(new InputStreamReader(inputStream, encoding)); + } else if (!fileName.contains("-")) { + } + } catch (Exception e) { + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + } + } + } + } + + private InputStream getResourceAsStreamByCurrentThread(String fileName) { + ClassLoader ret = Thread.currentThread().getContextClassLoader(); + return ret != null ? ret.getResourceAsStream(fileName) : null; + } + + + private InputStream getResourceAsStreamByClassloader(String fileName) { + ClassLoader ret = MyBatisFlexProps.class.getClassLoader(); + return ret != null ? ret.getResourceAsStream(fileName) : null; + } + + + private static String removeSlashEnd(String path) { + if (path != null && (path.endsWith("/") || path.endsWith("\\"))) { + return path.substring(0, path.length() - 1); + } else { + return path; + } + } + + + public Properties getProperties() { + return properties; + } +} diff --git a/mybatis-flex-annotation/src/main/java/com/mybatisflex/processer/QueryEntityProcesser.java b/mybatis-flex-annotation/src/main/java/com/mybatisflex/processer/QueryEntityProcesser.java new file mode 100644 index 00000000..2a9c7d85 --- /dev/null +++ b/mybatis-flex-annotation/src/main/java/com/mybatisflex/processer/QueryEntityProcesser.java @@ -0,0 +1,360 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.processer; + + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Table; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; +import java.io.*; +import java.util.*; +import java.util.function.Consumer; + +public class QueryEntityProcesser extends AbstractProcessor { + + private static final String classTableTemplate = "package @package;\n" + + "\n" + + "import com.mybatisflex.core.querywrapper.QueryColumn;\n" + + "import com.mybatisflex.core.table.TableDef;\n" + + "\n" + + "// Auto generate by mybatis-flex, do not modify it.\n" + + "public class Tables {\n" + + "@classesInfo" + + "}\n"; + + private static final String tableDefTemplate = "\n\n public static final @entityClassTableDef @tableField = new @entityClassTableDef(\"@tableName\");\n"; + + + private static final String classTemplate = "\n" + + " public static class @entityClassTableDef extends TableDef {\n" + + "\n" + + "@queryColumns" + + "@allColumns" + + "\n" + + " public @entityClassTableDef(String tableName) {\n" + + " super(tableName);\n" + + " }\n" + + " }\n"; + + + private static final String columnsTemplate = " public QueryColumn @property = new QueryColumn(this, \"@columnName\");\n"; + private static final String allColumnsTemplate = "\n public QueryColumn[] ALL_COLUMNS = new QueryColumn[]{@allColumns};\n\n"; + + protected Filer filer; + + @Override + public synchronized void init(ProcessingEnvironment processingEnvironment) { + super.init(processingEnvironment); + this.filer = processingEnvironment.getFiler(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + + if (!roundEnv.processingOver()) { + + MyBatisFlexProps props = new MyBatisFlexProps("mybatis-flex.properties"); + + String enable = props.getProperties().getProperty("processer.enable", ""); + if ("false".equalsIgnoreCase(enable)) { + return true; + } + String genPath = props.getProperties().getProperty("processer.genPath", ""); + final String genPackage = props.getProperties().getProperty("processer.package"); + String className = props.getProperties().getProperty("processer.className", "Tables"); + + StringBuilder guessPackage = new StringBuilder(); + + + StringBuilder tablesContent = new StringBuilder(); + roundEnv.getElementsAnnotatedWith(Table.class).forEach((Consumer) entityClassElement -> { + + Table table = entityClassElement.getAnnotation(Table.class); + + //init genPackage + if ((genPackage == null || genPackage.trim().length() == 0) + && guessPackage.length() == 0) { + String entityClassName = entityClassElement.toString(); + if (!entityClassName.contains(".")) { + guessPackage.append("table");// = "table"; + } else { + guessPackage.append(entityClassName.substring(0, entityClassName.lastIndexOf(".")) + ".table"); + } + } + + String tableName = table != null && table.value().trim().length() != 0 + ? table.value() + : firstCharToLowerCase(entityClassElement.getSimpleName().toString()); + + + Map propertyAndColumns = new LinkedHashMap<>(); + + TypeElement classElement = (TypeElement) entityClassElement; + for (Element fieldElement : classElement.getEnclosedElements()) { + + //all fields + if (ElementKind.FIELD == fieldElement.getKind()) { + Column column = fieldElement.getAnnotation(Column.class); + if (column != null && column.ignore()) { + continue; + } + String columnName = column != null && column.value().trim().length() > 0 ? column.value() : camelToUnderline(fieldElement.toString()); + propertyAndColumns.put(fieldElement.toString(), columnName); + } + } + + String entityClassName = entityClassElement.getSimpleName().toString(); + tablesContent.append(buildClass(entityClassName, tableName, propertyAndColumns)); + }); + + if (tablesContent.length() > 0) { + String realGenPackage = genPackage == null || genPackage.trim().length() == 0 ? guessPackage.toString() : genPackage; + genClass(genPath, realGenPackage, className, tablesContent.toString()); + } + + } + + + return false; + } + + private String buildClass(String entityClass, String tableName, Map propertyAndColumns) { + + // tableDefTemplate = " public static final @entityClassTableDef @tableField = new @entityClassTableDef(\"@tableName\");\n"; + + String tableDef = tableDefTemplate.replace("@entityClass", entityClass) + .replace("@tableField", entityClass.toUpperCase()) + .replace("@tableName", tableName); + + + //columnsTemplate = " public QueryColumn @property = new QueryColumn(this, \"@columnName\");\n"; + StringBuilder queryColumns = new StringBuilder(); + propertyAndColumns.forEach((property, column) -> + queryColumns.append(columnsTemplate + .replace("@property", camelToUnderline(property).toUpperCase()) + .replace("@columnName", column) + )); + + +// public QueryColumn[] ALL_COLUMNS = new QueryColumn[]{@allColumns}; + StringJoiner allColumns = new StringJoiner(", "); + propertyAndColumns.forEach((property, column) -> allColumns.add(camelToUnderline(property).toUpperCase())); + String allColumnsString = allColumnsTemplate.replace("@allColumns", allColumns.toString()); + + +// classTemplate = "\n" + +// " public static class @entityClassTableDef extends TableDef {\n" + +// "\n" + +// "@queryColumns" + +// "@allColumns" + +// "\n" + +// " public @entityClassTableDef(String tableName) {\n" + +// " super(tableName);\n" + +// " }\n" + +// " }\n"; + + String tableClass = classTemplate.replace("@entityClass", entityClass) + .replace("@queryColumns", queryColumns) + .replace("@allColumns", allColumnsString); + + return tableDef + tableClass; + } + + + private void genClass(String genBasePath, String genPackageName, String className, String classContent) { + String genContent = classTableTemplate.replace("@package", genPackageName) + .replace("@classesInfo", classContent); + + Writer writer = null; + try { + JavaFileObject sourceFile = filer.createSourceFile(genPackageName + "." + className); + if (genBasePath == null || genBasePath.trim().length() == 0) { + writer = sourceFile.openWriter(); + writer.write(genContent); + writer.flush(); + + printMessage(">>>>> mybatis-flex success generate tables class: \n" + sourceFile.toUri()); + return; + } + + + String defaultGenPath = sourceFile.toUri().getPath(); + + //真实的生成代码的目录 + String realPath; + + //用户配置的路径为绝对路径 + if (isAbsolutePath(genBasePath)) { + realPath = genBasePath; + } + //配置的是相对路径,那么则以项目根目录为相对路径 + else { + String projectRootPath = getProjectRootPath(defaultGenPath); + realPath = new File(projectRootPath, genBasePath).getAbsolutePath(); + } + + //通过在 test/java 目录下执行编译生成的 + boolean fromTestSource = isFromTestSource(defaultGenPath); + if (fromTestSource) { + realPath = new File(realPath, "src/test/java").getAbsolutePath(); + } else { + realPath = new File(realPath, "src/main/java").getAbsolutePath(); + } + + File genJavaFile = new File(realPath, (genPackageName + "." + className).replace(".", "/") + ".java"); + if (!genJavaFile.getParentFile().exists() && !genJavaFile.getParentFile().mkdirs()) { + System.out.println(">>>>>ERROR: can not mkdirs by mybatis-flex processer for: " + genJavaFile.getParentFile()); + return; + } + + writer = new PrintWriter(new FileOutputStream(genJavaFile)); + writer.write(classContent); + writer.flush(); + + printMessage(">>>>> mybatis-flex success generate tables class: \n" + genJavaFile.toURI()); + + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + } + } + } + } + + + private void printMessage(String message) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, message); + System.out.println(message); + } + + public static String firstCharToLowerCase(String str) { + char firstChar = str.charAt(0); + if (firstChar >= 'A' && firstChar <= 'Z') { + char[] arr = str.toCharArray(); + arr[0] += ('a' - 'A'); + return new String(arr); + } + return str; + } + + + @Override + public Set getSupportedAnnotationTypes() { + Set supportedAnnotationTypes = new HashSet<>(); + supportedAnnotationTypes.add(Table.class.getCanonicalName()); + return supportedAnnotationTypes; + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + + private boolean isFromTestSource(String path) { + return path.contains("test-sources") || path.contains("test-annotations"); + } + + + public static boolean isAbsolutePath(String path) { + return path != null && (path.startsWith("/") || path.indexOf(":") > 0); + } + + public static String camelToUnderline(String string) { + if (string == null || string.trim().length() == 0) { + return ""; + } + int len = string.length(); + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { + char c = string.charAt(i); + if (Character.isUpperCase(c) && i > 0) { + sb.append('_'); + } + sb.append(Character.toLowerCase(c)); + } + return sb.toString(); + } + + + /** + * 获取项目的根目录,也就是根节点 pom.xml 所在的目录 + * + * @return + */ + private String getProjectRootPath(String genFilePath) { + File file = new File(genFilePath); + int count = 20; + return getProjectRootPath(file, count); + } + + + private String getProjectRootPath(File file, int count) { + if (count <= 0) { + return null; + } + if (file.isFile()) { + return getProjectRootPath(file.getParentFile(), --count); + } else { + if (new File(file, "pom.xml").exists() && !new File(file.getParentFile(), "pom.xml").exists()) { + return file.getAbsolutePath(); + } else { + return getProjectRootPath(file.getParentFile(), --count); + } + } + } + + /** + * 当前 Maven 模块所在所在的目录 + * + * @return + */ + private String getModuleRootPath(String genFilePath) { + File file = new File(genFilePath); + int count = 20; + return getModuleRootPath(file, count); + } + + + private String getModuleRootPath(File file, int count) { + if (count <= 0) { + return null; + } + if (file.isFile()) { + return getModuleRootPath(file.getParentFile(), --count); + } else { + if (new File(file, "pom.xml").exists()) { + return file.getAbsolutePath(); + } else { + return getModuleRootPath(file.getParentFile(), --count); + } + } + } +} diff --git a/mybatis-flex-annotation/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/mybatis-flex-annotation/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 00000000..27352fa0 --- /dev/null +++ b/mybatis-flex-annotation/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +com.mybatisflex.processer.QueryEntityProcesser \ No newline at end of file diff --git a/mybatis-flex-codegen/pom.xml b/mybatis-flex-codegen/pom.xml new file mode 100644 index 00000000..91cbb241 --- /dev/null +++ b/mybatis-flex-codegen/pom.xml @@ -0,0 +1,19 @@ + + + + parent + com.mybatis-flex + 1.0.0-beta.1 + + 4.0.0 + + mybatis-flex-codegen + + + 8 + 8 + + + \ No newline at end of file diff --git a/mybatis-flex-core/pom.xml b/mybatis-flex-core/pom.xml new file mode 100644 index 00000000..38e7118c --- /dev/null +++ b/mybatis-flex-core/pom.xml @@ -0,0 +1,53 @@ + + + + parent + com.mybatis-flex + 1.0.0-beta.1 + + 4.0.0 + + mybatis-flex-core + + + + 8 + 8 + + + + + com.mybatis-flex + mybatis-flex-annotation + 1.0.0-beta.1 + + + + org.mybatis + mybatis + + + + mysql + mysql-connector-java + test + + + + com.zaxxer + HikariCP + test + + + + junit + junit + test + + + + + + \ No newline at end of file diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/BaseMapper.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/BaseMapper.java new file mode 100644 index 00000000..38413176 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/BaseMapper.java @@ -0,0 +1,284 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core; + +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.provider.EntitySqlProvider; +import com.mybatisflex.core.querywrapper.QueryColumn; +import com.mybatisflex.core.querywrapper.QueryWrapper; +import com.mybatisflex.core.querywrapper.CPI; +import org.apache.ibatis.annotations.*; +import org.apache.ibatis.builder.annotation.ProviderContext; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public interface BaseMapper { + + /** + * 插入 entity 数据 + * + * @param entity + * @return 返回影响的行数 + * @see com.mybatisflex.core.provider.EntitySqlProvider#insert(Map, ProviderContext) + */ + @InsertProvider(type = EntitySqlProvider.class, method = "insert") + int insert(@Param(FlexConsts.ENTITY) T entity); + + + /** + * 批量插入 entity 数据,只会根据第一条数据来构建插入的字段内容 + * + * @param entities 插入的数据列表 + * @return 返回影响的行数 + * @see com.mybatisflex.core.provider.EntitySqlProvider#insertBatchWithFirstEntityColumns(Map, ProviderContext) + */ + @InsertProvider(type = EntitySqlProvider.class, method = "insertBatchWithFirstEntityColumns") + int insertBatchWithFirstEntityColumns(@Param(FlexConsts.ENTITIES) List entities); + + /** + * 根据 id 删除数据 + * 如果是多个主键的情况下,需要传入数组 new Object[]{100,101} + * + * @param id 主键数据 + * @return 返回影响的行数 + * @see com.mybatisflex.core.provider.EntitySqlProvider#deleteById(Map, ProviderContext) + */ + @DeleteProvider(type = EntitySqlProvider.class, method = "deleteById") + int deleteById(@Param(FlexConsts.PRIMARY_VALUE) Serializable id); + + + /** + * 根据多个 id 批量删除数据 + * + * @param ids ids 列表 + * @return 返回影响的行数 + * @see com.mybatisflex.core.provider.EntitySqlProvider#deleteBatchByIds(Map, ProviderContext) + */ + @DeleteProvider(type = EntitySqlProvider.class, method = "deleteBatchByIds") + int deleteBatchByIds(@Param(FlexConsts.PRIMARY_VALUE) Collection ids); + + + /** + * 根据 map 构建的条件来删除数据 + * + * @param whereConditions + * @return 返回影响的行数 + */ + default int deleteByMap(Map whereConditions) { + return deleteByQuery(QueryWrapper.create().where(whereConditions)); + } + + /** + * 根据 query 构建的条件来数据吗 + * + * @param queryWrapper query 条件 + * @return 返回影响的行数 + * @see com.mybatisflex.core.provider.EntitySqlProvider#deleteByQuery(Map, ProviderContext) + */ + @DeleteProvider(type = EntitySqlProvider.class, method = "deleteByQuery") + int deleteByQuery(@Param(FlexConsts.QUERY) QueryWrapper queryWrapper); + + + /** + * 根据主键来更新数据,若数据为 null,也会更新到数据库 + * + * @param entity 数据内容,必须包含有主键 + * @return 返回影响的行数 + */ + default int update(T entity) { + return update(entity, true); + } + + /** + * 根据主键来更新数据到数据库 + * + * @param entity 数据内容 + * @param ignoreNulls 是否忽略空内容字段 + * @return 返回影响的行数 + * @see com.mybatisflex.core.provider.EntitySqlProvider#update(Map, ProviderContext) + */ + @UpdateProvider(type = EntitySqlProvider.class, method = "update") + int update(@Param(FlexConsts.ENTITY) T entity, @Param(FlexConsts.IGNORE_NULLS) boolean ignoreNulls); + + + /** + * 根据 map 构建的条件来更新数据 + * + * @param entity 数据 + * @param map where 条件内容 + * @return 返回影响的行数 + */ + default int updateByMap(T entity, Map map) { + return updateByQuery(entity, QueryWrapper.create().where(map)); + } + + + /** + * 根据 query 构建的条件来更新数据 + * + * @param entity 数据内容 + * @param queryWrapper query 条件 + * @return 返回影响的行数 + */ + + default int updateByQuery(@Param(FlexConsts.ENTITY) T entity, @Param(FlexConsts.QUERY) QueryWrapper queryWrapper) { + return updateByQuery(entity, true, queryWrapper); + } + + /** + * 根据 query 构建的条件来更新数据 + * + * @param entity 数据内容 + * @param ignoreNulls 是否忽略空值 + * @param queryWrapper query 条件 + * @return + * @see com.mybatisflex.core.provider.EntitySqlProvider#updateByQuery(Map, ProviderContext) + */ + @UpdateProvider(type = EntitySqlProvider.class, method = "updateByQuery") + int updateByQuery(@Param(FlexConsts.ENTITY) T entity, @Param("ignoreNulls") boolean ignoreNulls, @Param(FlexConsts.QUERY) QueryWrapper queryWrapper); + + + /** + * 根据主键来选择数据 + * + * @param id 多个主键 + * @return entity + * @see com.mybatisflex.core.provider.EntitySqlProvider#selectOneById(Map, ProviderContext) + */ + @SelectProvider(type = EntitySqlProvider.class, method = "selectOneById") + T selectOneById(@Param(FlexConsts.PRIMARY_VALUE) Serializable id); + + + /** + * 根据 map 构建的条件来查询数据 + * + * @param whereConditions where 条件 + * @return entity 数据 + */ + default T selectOneByMap(Map whereConditions) { + return selectOneByQuery(QueryWrapper.create().where(whereConditions)); + } + + + /** + * 根据 queryWrapper 构建的条件来查询 1 条数据 + * + * @param queryWrapper query 条件 + * @return entity 数据 + */ + default T selectOneByQuery(@Param(FlexConsts.QUERY) QueryWrapper queryWrapper) { + List entities = selectListByQuery(queryWrapper.limit(1)); + if (entities == null || entities.isEmpty()) { + return null; + } else { + return entities.get(0); + } + } + + /** + * 根据多个主键来查询多条数据 + * + * @param ids 主键列表 + * @return 数据列表 + * @see com.mybatisflex.core.provider.EntitySqlProvider#selectListByIds(Map, ProviderContext) + */ + @SelectProvider(type = EntitySqlProvider.class, method = "selectListByIds") + List selectListByIds(@Param(FlexConsts.PRIMARY_VALUE) Collection ids); + + + /** + * 根据 map 来构建查询条件,查询多条数据 + * + * @param whereConditions 条件列表 + * @return 数据列表 + */ + default List selectListByMap(Map whereConditions) { + return selectListByQuery(QueryWrapper.create().where(whereConditions)); + } + + + /** + * 根据 query 来构建条件查询数据列表 + * + * @param queryWrapper 查询条件 + * @return 数据列表 + * @see com.mybatisflex.core.provider.EntitySqlProvider#selectListByQuery(Map, ProviderContext) + */ + @SelectProvider(type = EntitySqlProvider.class, method = "selectListByQuery") + List selectListByQuery(@Param(FlexConsts.QUERY) QueryWrapper queryWrapper); + + + /** + * 根据 queryWrapper 来查询数据量 + * + * @param queryWrapper + * @return 数据量 + * @see EntitySqlProvider#selectCountByQuery(Map, ProviderContext) + */ + @SelectProvider(type = EntitySqlProvider.class, method = "selectCountByQuery") + long selectCountByQuery(@Param(FlexConsts.QUERY) QueryWrapper queryWrapper); + + + /** + * 分页查询 + * + * @param pageNumber 当前页码 + * @param pageSize 每页的数据量 + * @param queryWrapper 查询条件 + * @return 返回 Page 数据 + */ + default Page paginate(int pageNumber, int pageSize, QueryWrapper queryWrapper) { + Page page = new Page<>(pageNumber, pageSize); + return paginate(page, queryWrapper); + } + + + /** + * 分页查询 + * + * @param page page,其包含了页码、每页的数据量,可能包含数据总量 + * @param queryWrapper 查询条件 + * @return page 数据 + */ + default Page paginate(@Param("page") Page page, @Param("query") QueryWrapper queryWrapper) { + List groupByColumns = CPI.getGroupByColumns(queryWrapper); + + // 只有 totalRow 小于 0 的时候才会去查询总量 + // 这样方便用户做总数缓存,而非每次都要去查询总量 + // 一般的分页场景中,只有第一页的时候有必要去查询总量,第二页以后是不需要的 + if (page.getTotalRow() < 0) { + //清除group by 去查询数据 + CPI.setGroupByColumns(queryWrapper, null); + long count = selectCountByQuery(queryWrapper); + page.setTotalRow(count); + } + + if (page.getTotalRow() == 0 || page.getPageNumber() > page.getTotalPage()) { + return page; + } + + //恢复数量查询清除的 groupBy + CPI.setGroupByColumns(queryWrapper, groupByColumns); + int offset = page.getPageSize() * (page.getPageNumber() - 1); + queryWrapper.limit(offset, page.getPageSize()); + List rows = selectListByQuery(queryWrapper); + page.setList(rows); + return page; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/FlexConsts.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/FlexConsts.java new file mode 100644 index 00000000..bf7ee433 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/FlexConsts.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core; + +/** + * Mybatis-Flex 可能用到的静态常量 + */ +public class FlexConsts { + public static final String VERSION = "1.0.0-beta.1"; + + public static final String DEFAULT_PRIMARY_FIELD = "id"; + + public static final String SQL = "$$sql"; + public static final String SQL_ARGS = "$$sql_args"; + public static final String TABLE_NAME = "$$tableName"; + public static final String PRIMARY_KEY = "$$primaryKey"; + public static final String PRIMARY_VALUE = "$$primaryValue"; + + public static final String QUERY = "$$query"; + public static final String ROW = "$$row"; + public static final String ROWS = "$$rows"; + + public static final String ENTITY = "$$entity"; + public static final String ENTITIES = "$$entities"; + public static final String IGNORE_NULLS = "$$ignoreNulls"; +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/FlexGlobalConfig.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/FlexGlobalConfig.java new file mode 100644 index 00000000..a1801d09 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/FlexGlobalConfig.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core; + +import com.mybatisflex.core.dialect.DbType; +import org.apache.ibatis.session.SqlSessionFactory; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * 全局配置文件 + */ +public class FlexGlobalConfig { + + /** + * 默认使用 Mysql 数据库类型 + */ + private DbType dbType = DbType.MYSQL; + + /** + * 创建好的 sqlSessionFactory + */ + private SqlSessionFactory sqlSessionFactory; + + + + public DbType getDbType() { + return dbType; + } + + public void setDbType(DbType dbType) { + this.dbType = dbType; + } + + public SqlSessionFactory getSqlSessionFactory() { + return sqlSessionFactory; + } + + public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { + this.sqlSessionFactory = sqlSessionFactory; + } + + + /////static factory methods///// + private static ConcurrentHashMap globalConfigs = new ConcurrentHashMap(); + private static FlexGlobalConfig defaultConfig; + + public static FlexGlobalConfig getDefaultConfig() { + return defaultConfig; + } + + public static void setDefaultConfig(FlexGlobalConfig config) { + defaultConfig = config; + } + + public static FlexGlobalConfig getConfig(String environmentId) { + return globalConfigs.get(environmentId); + } + + public static void setConfig(String id, FlexGlobalConfig config) { + if (getDefaultConfig() == null) { + setDefaultConfig(config); + } + globalConfigs.put(id, config); + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/MybatisFlexBootstrap.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/MybatisFlexBootstrap.java new file mode 100644 index 00000000..e473b64c --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/MybatisFlexBootstrap.java @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core; + +import com.mybatisflex.core.dialect.DbType; +import com.mybatisflex.core.dialect.DialectFactory; +import com.mybatisflex.core.mybatis.FlexConfiguration; +import com.mybatisflex.core.mybatis.FlexSqlSessionFactoryBuilder; +import org.apache.ibatis.logging.LogFactory; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.transaction.TransactionFactory; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; + +import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; + +/** + * MybatisFlex 的启动类 + * + * + * MybatisFlexBootstrap.getInstance() + * .setDatasource(...) + * .addMapper(...) + * .start(); + *

+ *

+ * MybatisFlexBootstrap.getInstance() + * .execute(...) + * + */ +public class MybatisFlexBootstrap { + + private final AtomicBoolean started = new AtomicBoolean(false); + + private String environmentId = "mybatis-flex"; + private TransactionFactory transactionFactory; + + private DataSource dataSource; + private Configuration configuration; + private List> mappers; + + private DbType dbType; + private SqlSessionFactory sqlSessionFactory; + + + private static volatile MybatisFlexBootstrap instance; + + public static MybatisFlexBootstrap getInstance() { + if (instance == null) { + synchronized (MybatisFlexBootstrap.class) { + if (instance == null) { + instance = new MybatisFlexBootstrap(); + } + } + } + return instance; + } + + + public MybatisFlexBootstrap setDatasource(DataSource datasource) { + this.dataSource = datasource; + return this; + } + + public MybatisFlexBootstrap setConfiguration(Configuration configuration) { + this.configuration = configuration; + return this; + } + + public MybatisFlexBootstrap setTransactionFactory(TransactionFactory transactionFactory) { + this.transactionFactory = transactionFactory; + return this; + } + + + public MybatisFlexBootstrap setEnvironmentId(String environmentId) { + this.environmentId = environmentId; + return this; + } + + + public MybatisFlexBootstrap addMapper(Class type) { + if (this.mappers == null) { + mappers = new ArrayList<>(); + } + mappers.add(type); + return this; + } + + + public MybatisFlexBootstrap start() { + if (started.compareAndSet(false, true)) { + if (dataSource == null) { + throw new IllegalStateException("dataSource can not be null."); + } + + //init configuration + if (configuration == null) { + + if (transactionFactory == null) { + transactionFactory = new JdbcTransactionFactory(); + } + + Environment environment = new Environment(environmentId, transactionFactory, dataSource); + configuration = new FlexConfiguration(environment); + } + + //init mappers + if (mappers != null) { + mappers.forEach(configuration::addMapper); + } + + //init sqlSessionFactory + this.sqlSessionFactory = new FlexSqlSessionFactoryBuilder().build(configuration); + + //init dbType + this.dbType = FlexGlobalConfig.getConfig(environmentId).getDbType(); + + LogFactory.getLog(MybatisFlexBootstrap.class).debug("Mybatis-Flex has started."); + } + + return this; + } + + + public R execute(Class mapperClass, Function function) { + try (SqlSession sqlSession = openSession()) { + DialectFactory.setHintDbType(dbType); + T mapper = sqlSession.getMapper(mapperClass); + return function.apply(mapper); + } finally { + DialectFactory.clearHintDbType(); + } + } + + + private SqlSession openSession() { + return sqlSessionFactory.openSession(configuration.getDefaultExecutorType()); + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/CommonsDialectImpl.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/CommonsDialectImpl.java new file mode 100644 index 00000000..b73f8863 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/CommonsDialectImpl.java @@ -0,0 +1,576 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.dialect; + +import com.mybatisflex.core.querywrapper.*; +import com.mybatisflex.core.row.Row; +import com.mybatisflex.core.table.TableInfo; +import com.mybatisflex.core.util.ArrayUtil; +import com.mybatisflex.core.util.CollectionUtil; +import com.mybatisflex.core.util.StringUtil; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 通用的方言设计,其他方言可以继承于当前 CommonsDialectImpl + * 创建或获取方言请参考 {@link com.mybatisflex.core.dialect.DialectFactory} + */ +public class CommonsDialectImpl implements IDialect { + + protected KeywordWrap keywordWrap = KeywordWrap.NONE; + private LimitOffsetProcesser limitOffsetProcesser = LimitOffsetProcesser.MYSQL; + + public CommonsDialectImpl() { + } + + public CommonsDialectImpl(LimitOffsetProcesser limitOffsetProcesser) { + this.limitOffsetProcesser = limitOffsetProcesser; + } + + public CommonsDialectImpl(KeywordWrap keywordWrap, LimitOffsetProcesser limitOffsetProcesser) { + this.keywordWrap = keywordWrap; + this.limitOffsetProcesser = limitOffsetProcesser; + } + + @Override + public String wrap(String keyword) { + return keywordWrap.wrap(keyword); + } + + @Override + public String forInsertRow(String tableName, Row row) { + StringBuilder fields = new StringBuilder(); + StringBuilder questions = new StringBuilder(); + + Set attrs = row.obtainModifyAttrs(); + int index = 0; + for (String attr : attrs) { + fields.append(wrap(attr)); + questions.append("?"); + if (index != attrs.size() - 1) { + fields.append(", "); + questions.append(", "); + } + index++; + } + + String sql = "INSERT INTO " + wrap(tableName) + + "(" + fields + ") VALUES " + + "(" + questions + ")"; + return sql; + } + + + @Override + public String forInsertBatchWithFirstRowColumns(String tableName, List rows) { + StringBuilder fields = new StringBuilder(); + StringBuilder questions = new StringBuilder(); + + Row firstRow = rows.get(0); + Set attrs = firstRow.obtainModifyAttrs(); + int index = 0; + for (String attr : attrs) { + fields.append(wrap(attr)); + if (index != attrs.size() - 1) { + fields.append(", "); + } + index++; + } + + for (int i = 0; i < rows.size(); i++) { + questions.append(buildQuestion(attrs.size(), true)); + if (i != rows.size() - 1) { + questions.append(","); + } + } + + String sql = "INSERT INTO " + wrap(tableName) + + "(" + fields + ") VALUES " + questions; + return sql; + } + + + @Override + public String forDeleteById(String tableName, String[] primaryKeys) { + StringBuilder sql = new StringBuilder(); + sql.append("DELETE FROM "); + sql.append(wrap(tableName)); + sql.append(" WHERE "); + for (int i = 0; i < primaryKeys.length; i++) { + if (i > 0) { + sql.append(" AND "); + } + sql.append(wrap(primaryKeys[i])).append(" = ?"); + } + return sql.toString(); + } + + + @Override + public String forDeleteBatchByIds(String tableName, String[] primaryKeys, Object[] ids) { + StringBuilder sql = new StringBuilder(); + sql.append("DELETE FROM "); + sql.append(wrap(tableName)); + sql.append(" WHERE "); + + //多主键的场景 + if (primaryKeys.length > 1) { + for (int i = 0; i < ids.length / primaryKeys.length; i++) { + if (i > 0) { + sql.append(" OR "); + } + sql.append("("); + for (int j = 0; j < primaryKeys.length; j++) { + if (j > 0) { + sql.append(" AND "); + } + sql.append(wrap(primaryKeys[j])).append(" = ?"); + } + sql.append(")"); + } + } + // 单主键 + else { + for (int i = 0; i < ids.length; i++) { + if (i > 0) { + sql.append(" OR "); + } + sql.append(wrap(primaryKeys[0])).append(" = ?"); + } + } + return sql.toString(); + } + + @Override + public String forDeleteByQuery(QueryWrapper queryWrapper) { + return buildDeleteSql(queryWrapper); + } + + @Override + public String forUpdateById(String tableName, Row row) { + StringBuilder sql = new StringBuilder(); + + Set modifyAttrs = row.obtainModifyAttrs(); + String[] primaryKeys = row.obtainsPrimaryKeyStrings(); + + sql.append("UPDATE ").append(wrap(tableName)).append(" SET "); + int index = 0; + for (Map.Entry e : row.entrySet()) { + String colName = e.getKey(); + if (modifyAttrs.contains(colName) && !ArrayUtil.contains(primaryKeys, colName)) { + if (index > 0) { + sql.append(", "); + } + sql.append(wrap(colName)).append(" = ? "); + index++; + } + } + sql.append(" WHERE "); + for (int i = 0; i < primaryKeys.length; i++) { + if (i > 0) { + sql.append(" AND "); + } + sql.append(wrap(primaryKeys[i])).append(" = ?"); + } + + return sql.toString(); + } + + @Override + public String forUpdateByQuery(String tableName, Row row, QueryWrapper queryWrapper) { + StringBuilder sql = new StringBuilder(); + + Set modifyAttrs = row.obtainModifyAttrs(); + + sql.append("UPDATE ").append(wrap(tableName)).append(" SET "); + int index = 0; + for (String modifyAttr : modifyAttrs) { + if (index > 0) { + sql.append(", "); + } + sql.append(wrap(modifyAttr)).append(" = ? "); + index++; + } + + String whereConditionSql = buildWhereConditionSql(queryWrapper); + if (StringUtil.isNotBlank(whereConditionSql)) { + sql.append(" WHERE ").append(whereConditionSql); + } + + return sql.toString(); + } + + @Override + public String forUpdateBatchById(String tableName, List rows) { + if (rows.size() == 1) { + return forUpdateById(tableName, rows.get(0)); + } + StringBuilder sql = new StringBuilder(); + for (Row row : rows) { + sql.append(forUpdateById(tableName, row)).append("; "); + } + return sql.toString(); + } + + + @Override + public String forSelectOneById(String tableName, String[] primaryKeys, Object[] primaryValues) { + StringBuilder sql = new StringBuilder("SELECT * FROM "); + sql.append(wrap(tableName)).append(" WHERE "); + for (int i = 0; i < primaryKeys.length; i++) { + if (i > 0) { + sql.append(" AND "); + } + sql.append('`').append(primaryKeys[i]).append("` = ?"); + } + return sql.toString(); + } + + @Override + public String forSelectListByQuery(QueryWrapper queryWrapper) { + return buildSelectSql(queryWrapper); + } + + + @Override + public String forSelectCountByQuery(QueryWrapper queryWrapper) { + return buildSelectCountSql(queryWrapper); + } + + + ////////////build query sql/////// + @Override + public String buildSelectSql(QueryWrapper queryWrapper) { + List queryTables = CPI.getQueryTables(queryWrapper); + List joinTables = CPI.getJoinTables(queryWrapper); + List allTables = CollectionUtil.merge(queryTables, joinTables); + + List selectColumns = CPI.getSelectColumns(queryWrapper); + + StringBuilder sqlBuilder = new StringBuilder("SELECT "); + if (selectColumns == null || selectColumns.isEmpty()) { + sqlBuilder.append("*"); + } else { + int index = 0; + + for (QueryColumn selectColumn : selectColumns) { + String selectColumnSql = CPI.toSelectSql(selectColumn, allTables, this); + sqlBuilder.append(selectColumnSql); + if (index != selectColumns.size() - 1) { + sqlBuilder.append(", "); + } + index++; + } + } + sqlBuilder.append(" FROM ").append(StringUtil.join(",", queryTables, queryTable -> queryTable.toSql(this))); + + buildJoinSql(sqlBuilder, queryWrapper, allTables); + buildWhereSql(sqlBuilder, queryWrapper, allTables); + buildGroupBySql(sqlBuilder, queryWrapper, allTables); + buildHavingSql(sqlBuilder, queryWrapper, allTables); + buildOrderBySql(sqlBuilder, queryWrapper, allTables); + + Integer limitRows = CPI.getLimitRows(queryWrapper); + Integer limitOffset = CPI.getLimitOffset(queryWrapper); + if (limitRows != null || limitOffset != null) { + sqlBuilder = buildLimitOffsetSql(sqlBuilder, queryWrapper, limitRows, limitOffset); + } + + return sqlBuilder.toString(); + } + + + @Override + public String buildSelectCountSql(QueryWrapper queryWrapper) { + List queryTables = CPI.getQueryTables(queryWrapper); + List joinTables = CPI.getJoinTables(queryWrapper); + List allTables = CollectionUtil.merge(queryTables, joinTables); + + //ignore selectColumns + StringBuilder sqlBuilder = new StringBuilder("SELECT COUNT(*) FROM "); + sqlBuilder.append(StringUtil.join(",", queryTables, queryTable -> queryTable.toSql(this))); + + + buildJoinSql(sqlBuilder, queryWrapper, allTables); + buildWhereSql(sqlBuilder, queryWrapper, allTables); + buildGroupBySql(sqlBuilder, queryWrapper, allTables); + buildHavingSql(sqlBuilder, queryWrapper, allTables); + + // ignore orderBy and limit + // buildOrderBySql(sqlBuilder, queryWrapper); + // buildLimitSql(sqlBuilder, queryWrapper); + + return sqlBuilder.toString(); + } + + @Override + public String buildDeleteSql(QueryWrapper queryWrapper) { + List queryTables = CPI.getQueryTables(queryWrapper); + List joinTables = CPI.getJoinTables(queryWrapper); + List allTables = CollectionUtil.merge(queryTables, joinTables); + + //ignore selectColumns + StringBuilder sqlBuilder = new StringBuilder("DELETE FROM "); + sqlBuilder.append(StringUtil.join(",", queryTables, queryTable -> queryTable.toSql(this))); + + buildJoinSql(sqlBuilder, queryWrapper, allTables); + buildWhereSql(sqlBuilder, queryWrapper, allTables); + buildGroupBySql(sqlBuilder, queryWrapper, allTables); + buildHavingSql(sqlBuilder, queryWrapper, allTables); + + //ignore orderBy and limit + //buildOrderBySql(sqlBuilder, queryWrapper); + //buildLimitSql(sqlBuilder, queryWrapper); + + return sqlBuilder.toString(); + } + + @Override + public String buildWhereConditionSql(QueryWrapper queryWrapper) { + QueryCondition whereQueryCondition = CPI.getWhereQueryCondition(queryWrapper); + return whereQueryCondition != null ? whereQueryCondition.toSql(CPI.getQueryTables(queryWrapper), this) : ""; + } + + @Override + public String forInsertEntity(TableInfo tableInfo, Object entity) { + StringBuilder sql = new StringBuilder(); + sql.append("INSERT INTO ").append(wrap(tableInfo.getTableName())); + String[] insertColumns = tableInfo.obtainInsertColumns(); + sql.append("(").append(StringUtil.join(",", insertColumns)).append(")"); + sql.append(" VALUES "); + sql.append(buildQuestion(insertColumns.length, true)); + return sql.toString(); + } + + @Override + public String forInsertBatchWithFirstEntityColumns(TableInfo tableInfo, List entities) { + StringBuilder sql = new StringBuilder(); + sql.append("INSERT INTO ").append(wrap(tableInfo.getTableName())); + String[] insertColumns = tableInfo.obtainInsertColumns(); + sql.append("(").append(StringUtil.join(",", insertColumns)).append(")"); + sql.append(" VALUES "); + + for (int i = 0; i < entities.size(); i++) { + sql.append(buildQuestion(insertColumns.length, true)); + if (i != entities.size() - 1) { + sql.append(", "); + } + } + return sql.toString(); + } + + @Override + public String forDeleteEntityById(TableInfo tableInfo) { + return forDeleteById(tableInfo.getTableName(), tableInfo.getPrimaryKeys()); + } + + @Override + public String forDeleteEntityBatchById(TableInfo tableInfo, Object[] primaryValues) { + return forDeleteBatchByIds(tableInfo.getTableName(), tableInfo.getPrimaryKeys(), primaryValues); + } + + + @Override + public String forUpdateEntity(TableInfo tableInfo, Object entity, boolean ignoreNulls) { + StringBuilder sql = new StringBuilder(); + + Set modifyAttrs = tableInfo.obtainUpdateColumns(entity, ignoreNulls, false); + String[] primaryKeys = tableInfo.getPrimaryKeys(); + + sql.append("UPDATE ").append(wrap(tableInfo.getTableName())).append(" SET "); + int index = 0; + for (String modifyAttr : modifyAttrs) { + if (index > 0) { + sql.append(", "); + } + sql.append(wrap(modifyAttr)).append(" = ? "); + index++; + } + sql.append(" WHERE "); + for (int i = 0; i < primaryKeys.length; i++) { + if (i > 0) { + sql.append(" AND "); + } + sql.append(wrap(primaryKeys[i])).append(" = ?"); + } + + return sql.toString(); + } + + @Override + public String forUpdateEntityByQuery(TableInfo tableInfo, Object entity, boolean ignoreNulls, QueryWrapper queryWrapper) { + StringBuilder sql = new StringBuilder(); + + Set modifyAttrs = tableInfo.obtainUpdateColumns(entity, ignoreNulls, true); + + sql.append("UPDATE ").append(wrap(tableInfo.getTableName())).append(" SET "); + int index = 0; + for (String modifyAttr : modifyAttrs) { + if (index > 0) { + sql.append(", "); + } + sql.append(wrap(modifyAttr)).append(" = ? "); + index++; + } + sql.append(" WHERE "); + sql.append(buildWhereConditionSql(queryWrapper)); + + return sql.toString(); + } + + @Override + public String forSelectOneEntityById(TableInfo tableInfo) { + StringBuilder sql = new StringBuilder("SELECT * FROM "); + sql.append(wrap(tableInfo.getTableName())); + sql.append(" WHERE "); + String[] pKeys = tableInfo.getPrimaryKeys(); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" AND "); + } + sql.append(wrap(pKeys[i])).append(" = ?"); + } + return sql.toString(); + } + + + @Override + public String forSelectEntityListByIds(TableInfo tableInfo, Object[] primaryValues) { + StringBuilder sql = new StringBuilder("SELECT * FROM "); + sql.append(wrap(tableInfo.getTableName())); + sql.append(" WHERE "); + String[] primaryKeys = tableInfo.getPrimaryKeys(); + + //多主键的场景 + if (primaryKeys.length > 1) { + for (int i = 0; i < primaryValues.length / primaryKeys.length; i++) { + if (i > 0) { + sql.append(" OR "); + } + sql.append("("); + for (int j = 0; j < primaryKeys.length; j++) { + if (j > 0) { + sql.append(" AND "); + } + sql.append(wrap(primaryKeys[j])).append(" = ?"); + } + sql.append(")"); + } + } + // 单主键 + else { + for (int i = 0; i < primaryValues.length; i++) { + if (i > 0) { + sql.append(" OR "); + } + sql.append(wrap(primaryKeys[0])).append(" = ?"); + } + } + return sql.toString(); + } + + + protected void buildJoinSql(StringBuilder sqlBuilder, QueryWrapper queryWrapper, List queryTables) { + List joins = CPI.getJoins(queryWrapper); + if (joins != null && !joins.isEmpty()) { + for (Join join : joins) { + if (!join.checkEffective()) { + continue; + } + sqlBuilder.append(join.toSql(queryTables, this)); + } + } + } + + + protected void buildWhereSql(StringBuilder sqlBuilder, QueryWrapper queryWrapper, List queryTables) { + QueryCondition whereQueryCondition = CPI.getWhereQueryCondition(queryWrapper); + if (whereQueryCondition != null) { + String whereSql = whereQueryCondition.toSql(queryTables, this); + if (StringUtil.isNotBlank(whereSql)) { + sqlBuilder.append(" WHERE ").append(whereSql); + } + } + } + + + protected void buildGroupBySql(StringBuilder sqlBuilder, QueryWrapper queryWrapper, List queryTables) { + List groupByColumns = CPI.getGroupByColumns(queryWrapper); + if (groupByColumns != null && !groupByColumns.isEmpty()) { + sqlBuilder.append(" GROUP BY "); + int index = 0; + for (QueryColumn groupByColumn : groupByColumns) { + String groupBy = CPI.toConditionSql(groupByColumn, queryTables, this); + sqlBuilder.append(groupBy); + if (index != groupByColumns.size() - 1) { + sqlBuilder.append(", "); + } + index++; + } + } + } + + + protected void buildHavingSql(StringBuilder sqlBuilder, QueryWrapper queryWrapper, List queryTables) { + QueryCondition havingQueryCondition = CPI.getHavingQueryCondition(queryWrapper); + if (havingQueryCondition != null) { + String havingSql = havingQueryCondition.toSql(queryTables, this); + if (StringUtil.isNotBlank(havingSql)) { + sqlBuilder.append(" HAVING ").append(havingSql); + } + } + } + + + protected void buildOrderBySql(StringBuilder sqlBuilder, QueryWrapper queryWrapper, List queryTables) { + List orderBys = CPI.getOrderBys(queryWrapper); + if (orderBys != null && !orderBys.isEmpty()) { + sqlBuilder.append(" ORDER BY "); + int index = 0; + for (QueryOrderBy orderBy : orderBys) { + sqlBuilder.append(orderBy.toSql(queryTables, this)); + if (index != orderBys.size() - 1) { + sqlBuilder.append(", "); + } + index++; + } + } + } + + + /** + * 构建 limit 和 offset 的参数 + */ + protected StringBuilder buildLimitOffsetSql(StringBuilder sqlBuilder, QueryWrapper queryWrapper, Integer limitRows, Integer limitOffset) { + return limitOffsetProcesser.process(sqlBuilder, queryWrapper, limitRows, limitOffset); + } + + + protected String buildQuestion(int count, boolean withBrackets) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.append("?"); + if (i != count - 1) { + sb.append(", "); + } + } + return withBrackets ? "(" + sb + ")" : sb.toString(); + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/DbType.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/DbType.java new file mode 100644 index 00000000..fa5da594 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/DbType.java @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.dialect; + + +public enum DbType { + + /** + * MYSQL + */ + MYSQL("mysql", "MySql数据库"), + /** + * MARIADB + */ + MARIADB("mariadb", "MariaDB数据库"), + /** + * ORACLE + */ + ORACLE("oracle", "Oracle11g及以下数据库"), + /** + * oracle12c + */ + ORACLE_12C("oracle12c", "Oracle12c及以上数据库"), + /** + * DB2 + */ + DB2("db2", "DB2数据库"), + /** + * H2 + */ + H2("h2", "H2数据库"), + /** + * HSQL + */ + HSQL("hsql", "HSQL数据库"), + /** + * SQLITE + */ + SQLITE("sqlite", "SQLite数据库"), + /** + * POSTGRE + */ + POSTGRE_SQL("postgresql", "Postgre数据库"), + /** + * SQLSERVER2005 + */ + SQL_SERVER2005("sqlserver2005", "SQLServer2005数据库"), + /** + * SQLSERVER + */ + SQL_SERVER("sqlserver", "SQLServer数据库"), + /** + * DM + */ + DM("dm", "达梦数据库"), + /** + * xugu + */ + XU_GU("xugu", "虚谷数据库"), + /** + * Kingbase + */ + KINGBASE_ES("kingbasees", "人大金仓数据库"), + /** + * Phoenix + */ + PHOENIX("phoenix", "Phoenix HBase数据库"), + /** + * Gauss + */ + GAUSS("gauss", "Gauss 数据库"), + /** + * ClickHouse + */ + CLICK_HOUSE("clickhouse", "clickhouse 数据库"), + /** + * GBase + */ + GBASE("gbase", "南大通用(华库)数据库"), + /** + * GBase-8s + */ + GBASE_8S("gbase-8s", "南大通用数据库 GBase 8s"), + /** + * Oscar + */ + OSCAR("oscar", "神通数据库"), + /** + * Sybase + */ + SYBASE("sybase", "Sybase ASE 数据库"), + /** + * OceanBase + */ + OCEAN_BASE("oceanbase", "OceanBase 数据库"), + /** + * Firebird + */ + FIREBIRD("Firebird", "Firebird 数据库"), + /** + * derby + */ + DERBY("derby", "derby 数据库"), + /** + * HighGo + */ + HIGH_GO("highgo", "瀚高数据库"), + /** + * CUBRID + */ + CUBRID("cubrid", "CUBRID数据库"), + + /** + * GOLDILOCKS + */ + GOLDILOCKS("goldilocks", "GOLDILOCKS数据库"), + /** + * CSIIDB + */ + CSIIDB("csiidb", "CSIIDB数据库"), + /** + * CSIIDB + */ + SAP_HANA("hana", "SAP_HANA数据库"), + /** + * Impala + */ + IMPALA("impala", "impala数据库"), + /** + * Vertica + */ + VERTICA("vertica", "vertica数据库"), + /** + * 东方国信 xcloud + */ + XCloud("xcloud", "行云数据库"), + /** + * redshift + */ + REDSHIFT("redshift", "亚马逊redshift数据库"), + /** + * openGauss + */ + OPENGAUSS("openGauss", "华为 opengauss 数据库"), + /** + * TDengine + */ + TDENGINE("TDengine", "TDengine数据库"), + /** + * Informix + */ + INFORMIX("informix", "Informix数据库"), + /** + * uxdb + */ + UXDB("uxdb", "优炫数据库"), + /** + * greenplum + */ + GREENPLUM("greenplum", "greenplum数据库"), + /** + * UNKNOWN DB + */ + OTHER("other", "其他数据库"); + + /** + * 数据库名称 + */ + private final String name; + + /** + * 描述 + */ + private final String remarks; + + + DbType(String name, String remarks) { + this.name = name; + this.remarks = remarks; + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/DbTypeUtil.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/DbTypeUtil.java new file mode 100644 index 00000000..6dc8e753 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/DbTypeUtil.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.dialect; + + +import java.util.regex.Pattern; + +/** + * DbType 解析 工具类 + */ +public class DbTypeUtil { + + + /** + * 参考 druid 和 MyBatis-plus 的 JdbcUtils + * {@link com.alibaba.druid.util.JdbcUtils#getDbType(String, String)} + * {@link com.baomidou.mybatisplus.extension.toolkit.JdbcUtils#getDbType(String)} + * + * @param jdbcUrl jdbcURL + * @return 返回数据库类型 + */ + public static DbType parseDbType(String jdbcUrl) { + jdbcUrl = jdbcUrl.toLowerCase(); + if (jdbcUrl.contains(":mysql:") || jdbcUrl.contains(":cobar:")) { + return DbType.MYSQL; + } else if (jdbcUrl.contains(":mariadb:")) { + return DbType.MARIADB; + } else if (jdbcUrl.contains(":oracle:")) { + return DbType.ORACLE; + } else if (jdbcUrl.contains(":sqlserver:") || jdbcUrl.contains(":microsoft:")) { + return DbType.SQL_SERVER2005; + } else if (jdbcUrl.contains(":sqlserver2012:")) { + return DbType.SQL_SERVER; + } else if (jdbcUrl.contains(":postgresql:")) { + return DbType.POSTGRE_SQL; + } else if (jdbcUrl.contains(":hsqldb:")) { + return DbType.HSQL; + } else if (jdbcUrl.contains(":db2:")) { + return DbType.DB2; + } else if (jdbcUrl.contains(":sqlite:")) { + return DbType.SQLITE; + } else if (jdbcUrl.contains(":h2:")) { + return DbType.H2; + } else if (isMatchedRegex(":dm\\d*:", jdbcUrl)) { + return DbType.DM; + } else if (jdbcUrl.contains(":xugu:")) { + return DbType.XU_GU; + } else if (isMatchedRegex(":kingbase\\d*:", jdbcUrl)) { + return DbType.KINGBASE_ES; + } else if (jdbcUrl.contains(":phoenix:")) { + return DbType.PHOENIX; + } else if (jdbcUrl.contains(":zenith:")) { + return DbType.GAUSS; + } else if (jdbcUrl.contains(":gbase:")) { + return DbType.GBASE; + } else if (jdbcUrl.contains(":gbasedbt-sqli:") || jdbcUrl.contains(":informix-sqli:")) { + return DbType.GBASE_8S; + } else if (jdbcUrl.contains(":ch:") || jdbcUrl.contains(":clickhouse:")) { + return DbType.CLICK_HOUSE; + } else if (jdbcUrl.contains(":oscar:")) { + return DbType.OSCAR; + } else if (jdbcUrl.contains(":sybase:")) { + return DbType.SYBASE; + } else if (jdbcUrl.contains(":oceanbase:")) { + return DbType.OCEAN_BASE; + } else if (jdbcUrl.contains(":highgo:")) { + return DbType.HIGH_GO; + } else if (jdbcUrl.contains(":cubrid:")) { + return DbType.CUBRID; + } else if (jdbcUrl.contains(":goldilocks:")) { + return DbType.GOLDILOCKS; + } else if (jdbcUrl.contains(":csiidb:")) { + return DbType.CSIIDB; + } else if (jdbcUrl.contains(":sap:")) { + return DbType.SAP_HANA; + } else if (jdbcUrl.contains(":impala:")) { + return DbType.IMPALA; + } else if (jdbcUrl.contains(":vertica:")) { + return DbType.VERTICA; + } else if (jdbcUrl.contains(":xcloud:")) { + return DbType.XCloud; + } else if (jdbcUrl.contains(":firebirdsql:")) { + return DbType.FIREBIRD; + } else if (jdbcUrl.contains(":redshift:")) { + return DbType.REDSHIFT; + } else if (jdbcUrl.contains(":opengauss:")) { + return DbType.OPENGAUSS; + } else if (jdbcUrl.contains(":taos:") || jdbcUrl.contains(":taos-rs:")) { + return DbType.TDENGINE; + } else if (jdbcUrl.contains(":informix")) { + return DbType.INFORMIX; + } else if (jdbcUrl.contains(":uxdb:")) { + return DbType.UXDB; + } else if (jdbcUrl.contains(":greenplum:")) { + return DbType.GREENPLUM; + } else { + return DbType.OTHER; + } + } + + /** + * 正则匹配,验证成功返回 true,验证失败返回 false + */ + public static boolean isMatchedRegex(String regex, String jdbcUrl) { + if (null == jdbcUrl) { + return false; + } + return Pattern.compile(regex).matcher(jdbcUrl).find(); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/DialectFactory.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/DialectFactory.java new file mode 100644 index 00000000..3d50e3e7 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/DialectFactory.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.dialect; + + +import com.mybatisflex.core.FlexGlobalConfig; +import com.mybatisflex.core.util.ObjectUtil; +import org.apache.ibatis.util.MapUtil; + +import java.util.EnumMap; +import java.util.Map; + +/** + * 方言工厂类,用于创建方言 + */ +public class DialectFactory { + + /** + * 数据库类型和方言的映射关系,可以通过其读取指定的方言,亦可能通过其扩展其他方言 + * 比如,在 mybatis-flex 实现的方言中有 bug 或者 有自己的独立实现,可以添加自己的方言实现到 + * 此 map 中,用于覆盖系统的方言实现 + */ + private static Map dialectMap = new EnumMap<>(DbType.class); + + /** + * 通过设置当前线程的数据库类型,以达到在代码执行时随时切换方言的功能 + */ + private static ThreadLocal dbTypeThreadLocal = new ThreadLocal<>(); + + + /** + * 获取方言 + * + * @return IDialect + */ + public static IDialect getDialect() { + DbType dbType = ObjectUtil.requireNonNullElse(dbTypeThreadLocal.get(), FlexGlobalConfig.getDefaultConfig().getDbType()); + return MapUtil.computeIfAbsent(dialectMap, dbType, DialectFactory::createDialectByDbType); + } + + /** + * 设置当前线程的 dbType + * + * @param dbType + */ + public static void setHintDbType(DbType dbType) { + dbTypeThreadLocal.set(dbType); + } + + /** + * 获取当前线程的 dbType + * + * @return dbType + */ + public static DbType getHintDbType() { + return dbTypeThreadLocal.get(); + } + + + /** + * 清除当前线程的 dbType + */ + public static void clearHintDbType() { + dbTypeThreadLocal.remove(); + } + + + /** + * 可以为某个 dbType 注册(新增或覆盖)自己的方言 + * + * @param dbType 数据库类型 + * @param dialect 方言的实现 + */ + public static void registerDialect(DbType dbType, IDialect dialect) { + dialectMap.put(dbType, dialect); + } + + + private static IDialect createDialectByDbType(DbType dbType) { + switch (dbType) { + case MYSQL: + case H2: + case MARIADB: + case GBASE: + case OSCAR: + case XU_GU: + case CLICK_HOUSE: + case OCEAN_BASE: + case CUBRID: + case GOLDILOCKS: + case CSIIDB: + return new CommonsDialectImpl(KeywordWrap.BACKQUOTE, LimitOffsetProcesser.MYSQL); + case ORACLE: + case DM: + case GAUSS: + return new CommonsDialectImpl(KeywordWrap.DOUBLE_QUOTATION, LimitOffsetProcesser.ORACLE); + case POSTGRE_SQL: + case SQLITE: + case HSQL: + case KINGBASE_ES: + case PHOENIX: + case SAP_HANA: + case IMPALA: + case HIGH_GO: + case VERTICA: + case REDSHIFT: + case OPENGAUSS: + case TDENGINE: + case UXDB: + return new CommonsDialectImpl(KeywordWrap.BACKQUOTE, LimitOffsetProcesser.POSTGRESQL); + case ORACLE_12C: + case FIREBIRD: + case SQL_SERVER: + return new CommonsDialectImpl(KeywordWrap.DOUBLE_QUOTATION, LimitOffsetProcesser.DERBY); + case SQL_SERVER2005: + return new CommonsDialectImpl(KeywordWrap.SQUARE_BRACKETS, LimitOffsetProcesser.DB2); + case INFORMIX: + return new CommonsDialectImpl(KeywordWrap.DOUBLE_QUOTATION, LimitOffsetProcesser.INFORMIX); + case DB2: + return new CommonsDialectImpl(KeywordWrap.DOUBLE_QUOTATION, LimitOffsetProcesser.DB2); + default: + return new CommonsDialectImpl(); + } + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/IDialect.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/IDialect.java new file mode 100644 index 00000000..43d8a9b0 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/IDialect.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.dialect; + +import com.mybatisflex.core.querywrapper.QueryWrapper; +import com.mybatisflex.core.row.Row; +import com.mybatisflex.core.table.TableInfo; + +import java.util.List; + +public interface IDialect { + + String wrap(String keyword); + + String forInsertRow(String tableName, Row row); + + String forInsertBatchWithFirstRowColumns(String tableName, List rows); + + String forDeleteById(String tableName, String[] primaryKeys); + + String forDeleteBatchByIds(String tableName, String[] primaryKeys, Object[] ids); + + String forDeleteByQuery(QueryWrapper queryWrapper); + + String forUpdateById(String tableName, Row row); + + String forUpdateByQuery(String tableName, Row data, QueryWrapper queryWrapper); + + String forUpdateBatchById(String tableName, List rows); + + String forSelectOneById(String tableName, String[] primaryKeys, Object[] primaryValues); + + String forSelectListByQuery(QueryWrapper queryWrapper); + + String forSelectCountByQuery(QueryWrapper queryWrapper); + + + String buildSelectSql(QueryWrapper queryWrapper); + + String buildSelectCountSql(QueryWrapper queryWrapper); + + String buildDeleteSql(QueryWrapper queryWrapper); + + String buildWhereConditionSql(QueryWrapper queryWrapper); + + + + //////for entity ///// + String forInsertEntity(TableInfo tableInfo, Object entity); + + String forInsertBatchWithFirstEntityColumns(TableInfo tableInfo, List entities); + + String forDeleteEntityById(TableInfo tableInfo); + + String forDeleteEntityBatchById(TableInfo tableInfo, Object[] primaryValues); + + String forUpdateEntity(TableInfo tableInfo, Object entity, boolean ignoreNulls); + + String forUpdateEntityByQuery(TableInfo tableInfo, Object entity, boolean ignoreNulls, QueryWrapper queryWrapper); + + String forSelectOneEntityById(TableInfo tableInfo); + + String forSelectEntityListByIds(TableInfo tableInfo, Object[] primaryValues); + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/KeywordWrap.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/KeywordWrap.java new file mode 100644 index 00000000..7a061755 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/KeywordWrap.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.dialect; + +import com.mybatisflex.core.util.StringUtil; + +/** + * 用于对数据库的关键字包装 + */ +public class KeywordWrap { + + /** + * 无反义处理, 适用于 db2, informix, clickhouse 等 + */ + public final static KeywordWrap NONE = new KeywordWrap("", "") { + @Override + public String wrap(String keyword) { + return keyword; + } + }; + + /** + * 反引号反义处理, 适用于 mysql, h2 等 + */ + public final static KeywordWrap BACKQUOTE = new KeywordWrap("`", "`"); + + /** + * 双引号反义处理, 适用于 postgresql, sqlite, derby, oracle 等 + */ + public final static KeywordWrap DOUBLE_QUOTATION = new KeywordWrap("\"", "\""); + + /** + * 方括号反义处理, 适用于 sqlserver + */ + public final static KeywordWrap SQUARE_BRACKETS = new KeywordWrap("[", "]"); + + /** + * 前缀 + */ + private final String prefix; + + /** + * 后缀 + */ + private final String suffix; + + + public KeywordWrap(String prefix, String suffix) { + this.prefix = prefix; + this.suffix = suffix; + } + + public String wrap(String keyword) { + return StringUtil.isBlank(keyword) ? "" : prefix + keyword + suffix; + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/LimitOffsetProcesser.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/LimitOffsetProcesser.java new file mode 100644 index 00000000..a4fd3031 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/dialect/LimitOffsetProcesser.java @@ -0,0 +1,138 @@ +package com.mybatisflex.core.dialect; + +import com.mybatisflex.core.querywrapper.QueryWrapper; + +/** + * limit 和 offset 参数的处理器 + */ +public interface LimitOffsetProcesser { + + /** + * MySql 的处理器 + * 适合 {@link DbType#MYSQL,DbType#MARIADB,DbType#H2,DbType#CLICK_HOUSE,DbType#XCloud} + */ + LimitOffsetProcesser MYSQL = (sql, queryWrapper, limitRows, limitOffset) -> { + if (limitRows != null && limitOffset != null) { + sql.append(" LIMIT ").append(limitOffset).append(", ").append(limitRows); + } else if (limitRows != null) { + sql.append(" LIMIT ").append(limitRows); + } + return sql; + }; + + /** + * Postgresql 的处理器 + * 适合 {@link DbType#POSTGRE_SQL,DbType#SQLITE,DbType#H2,DbType#HSQL,DbType#KINGBASE_ES,DbType#PHOENIX} + * 适合 {@link DbType#SAP_HANA,DbType#IMPALA,DbType#HIGH_GO,DbType#VERTICA,DbType#REDSHIFT} + * 适合 {@link DbType#OPENGAUSS,DbType#TDENGINE,DbType#UXDB} + */ + LimitOffsetProcesser POSTGRESQL = (sql, queryWrapper, limitRows, limitOffset) -> { + if (limitRows != null && limitOffset != null) { + sql.append(" LIMIT ").append(limitOffset).append(" OFFSET ").append(limitRows); + } else if (limitRows != null) { + sql.append(" LIMIT ").append(limitRows); + } + return sql; + }; + + /** + * derby 的处理器 + * 适合 {@link DbType#DERBY,DbType#ORACLE_12C,DbType#SQL_SERVER,DbType#POSTGRE_SQL} + */ + LimitOffsetProcesser DERBY = (sql, queryWrapper, limitRows, limitOffset) -> { + if (limitRows != null && limitOffset != null) { + // OFFSET ** ROWS FETCH NEXT ** ROWS ONLY") + sql.append(" OFFSET ").append(limitOffset).append(" ROWS FETCH NEXT ").append(limitRows).append(" ROWS ONLY"); + } else if (limitRows != null) { + // FETCH FIRST 20 ROWS ONLY + sql.append(" FETCH FIRST ").append(limitRows).append(" ROWS ONLY"); + } + return sql; + }; + + /** + * db2 的处理器 + * 适合 {@link DbType#DB2,DbType#SQL_SERVER2005} + */ + LimitOffsetProcesser DB2 = (sql, queryWrapper, limitRows, limitOffset) -> { + if (limitRows != null && limitOffset != null) { + // OFFSET ** ROWS FETCH NEXT ** ROWS ONLY") + sql.append(" OFFSET ").append(limitOffset).append(" ROWS FETCH NEXT ").append(limitRows).append(" ROWS ONLY"); + } else if (limitRows != null) { + // FETCH FIRST 20 ROWS ONLY + sql.append(" FETCH FIRST ").append(limitRows).append(" ROWS ONLY"); + } + return sql; + }; + + /** + * Informix 的处理器 + * 适合 {@link DbType#INFORMIX} + * 文档 {@link https://www.ibm.com/docs/en/informix-servers/14.10?topic=clause-restricting-return-values-skip-limit-first-options} + */ + LimitOffsetProcesser INFORMIX = (sql, queryWrapper, limitRows, limitOffset) -> { + if (limitRows != null && limitOffset != null) { + // SELECT SKIP 2 FIRST 1 * FROM + sql.insert(6, " SKIP " + limitOffset + " FIRST " + limitRows); + } else if (limitRows != null) { + sql.insert(6, " FIRST " + limitRows); + } + return sql; + }; + + /** + * Firebird 的处理器 + * 适合 {@link DbType#FIREBIRD} + */ + LimitOffsetProcesser FIREBIRD = (sql, queryWrapper, limitRows, limitOffset) -> { + if (limitRows != null && limitOffset != null) { + // ROWS 2 TO 3 + sql.append(" ROWS ").append(limitOffset).append(" TO ").append(limitOffset + limitRows); + } else if (limitRows != null) { + sql.insert(6, " FIRST " + limitRows); + } + return sql; + }; + + /** + * Oracle11g及以下数据库的处理器 + * 适合 {@link DbType#ORACLE,DbType#DM,DbType#GAUSS} + */ + LimitOffsetProcesser ORACLE = (sql, queryWrapper, limitRows, limitOffset) -> { + if (limitRows != null) { + if (limitOffset == null) { + limitOffset = 0; + } + StringBuilder newSql = new StringBuilder("SELECT * FROM (SELECT TEMP_DATAS.*, ROWNUM RN FROM ("); + newSql.append(sql); + newSql.append(") TEMP_DATAS WHERE ROWNUM <=").append(limitOffset + limitRows).append(") WHERE RN >").append(limitOffset); + return newSql; + } + return sql; + }; + + /** + * Sybase 处理器 + * 适合 {@link DbType#SYBASE} + */ + LimitOffsetProcesser SYBASE = (sql, queryWrapper, limitRows, limitOffset) -> { + if (limitRows != null && limitOffset != null) { + //SELECT TOP 1 START AT 3 * FROM + sql.insert(6, " TOP " + limitRows + " START AT " + (limitRows + limitOffset)); + } else if (limitRows != null) { + sql.insert(6, " TOP " + limitRows); + } + return sql; + }; + + + /** + * 处理构建 limit 和 offset + * + * @param sql 已经构建的 sql + * @param queryWrapper 参数内容 + * @param limitRows 用户传入的 limit 参数 可能为 null + * @param limitOffset 用户传入的 offset 参数,可能为 null + */ + StringBuilder process(StringBuilder sql, QueryWrapper queryWrapper, Integer limitRows, Integer limitOffset); +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/exception/FlexExceptions.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/exception/FlexExceptions.java new file mode 100644 index 00000000..c509ecce --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/exception/FlexExceptions.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.exception; + +/** + * MybatisFlexException 异常封装类 + */ +public final class FlexExceptions { + + private FlexExceptions() { + } + + + /** + * 封装 MybatisFlexException 异常 + * + * @param throwable 异常 + * @return MybatisFlexException + */ + public static MybatisFlexException wrap(Throwable throwable) { + if (throwable instanceof MybatisFlexException){ + return (MybatisFlexException) throwable; + } + return new MybatisFlexException(throwable); + } + + + /** + * 封装 MybatisFlexException 异常 + * + * @param throwable 异常 + * @param msg 消息 + * @param params 消息参数 + * @return MybatisFlexException + */ + public static MybatisFlexException wrap(Throwable throwable, String msg, Object... params) { + return new MybatisFlexException(String.format(msg, params), throwable); + } + + /** + * 封装 MybatisFlexException 异常 + * + * @param msg 消息 + * @param params 消息参数 + * @return MybatisFlexException + */ + public static MybatisFlexException wrap(String msg, Object... params) { + return new MybatisFlexException(String.format(msg, params)); + } + + + /** + * 抛出一个新的 MybatisFlexException 异常 + * + * @param condition 抛出条件 + * @param msg 消息 + * @param params 消息参数 + */ + public static void throwIf(boolean condition, String msg, Object... params) { + if (condition) { + throw wrap(msg, params); + } + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/exception/MybatisFlexException.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/exception/MybatisFlexException.java new file mode 100644 index 00000000..278897b6 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/exception/MybatisFlexException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.exception; + +public class MybatisFlexException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public MybatisFlexException(String message) { + super(message); + } + + public MybatisFlexException(String message, Throwable cause) { + super(message, cause); + } + + public MybatisFlexException(Throwable cause) { + super(cause); + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/javassist/ModifyAttrsRecord.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/javassist/ModifyAttrsRecord.java new file mode 100644 index 00000000..c70e2e8a --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/javassist/ModifyAttrsRecord.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.javassist; + +import java.io.Serializable; +import java.util.LinkedHashSet; +import java.util.Set; + +public interface ModifyAttrsRecord extends Serializable { + + /** + * 注意: + * 对于 entity 来说,这里存放的只是 属性的名称,而非字段 + * 对于 row 来说,存放的则是 字段 名称 + */ + Set modifyAttrs = new LinkedHashSet<>(); + + default void addModifyAttr(String attr) { + modifyAttrs.add(attr); + } + + default void removeModifyAttr(String attr) { + modifyAttrs.remove(attr); + } + + default Set obtainModifyAttrs() { + return new LinkedHashSet<>(modifyAttrs); + } + + default void clearModifyFlag() { + modifyAttrs.clear(); + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/javassist/ModifyAttrsRecordHandler.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/javassist/ModifyAttrsRecordHandler.java new file mode 100644 index 00000000..a1bde974 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/javassist/ModifyAttrsRecordHandler.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.javassist; + + +import com.mybatisflex.core.util.StringUtil; +import org.apache.ibatis.javassist.util.proxy.MethodHandler; + +import java.lang.reflect.Method; + + +public class ModifyAttrsRecordHandler implements MethodHandler { + + + @Override + public Object invoke(Object self, Method originalMethod, Method proxyMethod, Object[] args) throws Throwable { + + if (originalMethod.getName().startsWith("set")){ + String property = StringUtil.firstCharToLowerCase(originalMethod.getName().substring(3)); + ((ModifyAttrsRecord) self).addModifyAttr(property); + } + + return proxyMethod.invoke(self, args); + } + + + + +} + + diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/javassist/ModifyAttrsRecordProxyFactory.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/javassist/ModifyAttrsRecordProxyFactory.java new file mode 100644 index 00000000..54030bd3 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/javassist/ModifyAttrsRecordProxyFactory.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.javassist; + +import org.apache.ibatis.javassist.util.proxy.ProxyFactory; +import org.apache.ibatis.javassist.util.proxy.ProxyObject; +import org.apache.ibatis.logging.LogFactory; + +import java.util.Arrays; + + +public class ModifyAttrsRecordProxyFactory { + + private static ModifyAttrsRecordProxyFactory instance = new ModifyAttrsRecordProxyFactory(); + + public static ModifyAttrsRecordProxyFactory getInstance(){ + return instance; + } + + private ModifyAttrsRecordProxyFactory(){} + + public T get(Class target) { + ProxyFactory factory = new ProxyFactory(); + factory.setSuperclass(target); + + + Class[] interfaces = Arrays.copyOf(target.getInterfaces(), target.getInterfaces().length + 1); + interfaces[interfaces.length - 1] = ModifyAttrsRecord.class; + factory.setInterfaces(interfaces); + + final Class proxyClass = factory.createClass(); + + T proxyObject = null; + try { + proxyObject = (T) proxyClass.newInstance(); + ((ProxyObject) proxyObject).setHandler(new ModifyAttrsRecordHandler()); + } catch (Throwable e) { + LogFactory.getLog(ModifyAttrsRecordProxyFactory.class).error(e.toString(),e); + } + + return proxyObject; + } + + +} + + + diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/CustomKeyGenerator.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/CustomKeyGenerator.java new file mode 100644 index 00000000..f7d352aa --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/CustomKeyGenerator.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.key; + +import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.exception.FlexExceptions; +import com.mybatisflex.core.table.IdInfo; +import com.mybatisflex.core.table.TableInfo; +import com.mybatisflex.core.util.StringUtil; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.reflection.invoker.Invoker; +import org.apache.ibatis.session.Configuration; + +import java.sql.Statement; +import java.util.Map; + +/** + * 通过 java 编码的方式生成主键 + * 当主键类型配置为 KeyType#Generator 时,使用此生成器生成 + * {@link com.mybatisflex.core.enums.KeyType#Generator} + */ +public class CustomKeyGenerator implements KeyGenerator { + + protected Configuration configuration; + protected IKeyGenerator keyGenerator; + protected TableInfo tableInfo; + protected IdInfo idInfo; + + + public CustomKeyGenerator(Configuration configuration, TableInfo tableInfo, IdInfo idInfo) { + this.configuration = configuration; + this.keyGenerator = KeyGeneratorFactory.getKeyGenerator(idInfo.getValue()); + this.tableInfo = tableInfo; + this.idInfo = idInfo; + + ensuresKeyGeneratorNotNull(); + } + + private void ensuresKeyGeneratorNotNull() { + if (keyGenerator == null) { + throw FlexExceptions.wrap("The name of \"%s\" key generator not exist.\n" + + "please check annotation @Id(value=\"%s\") at field: %s#%s" + , idInfo.getValue(), idInfo.getValue(), tableInfo.getEntityClass().getName(), idInfo.getProperty()); + } + } + + + @Override + public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + Object entity = ((Map) parameter).get(FlexConsts.ENTITY); + Object generateId = keyGenerator.generate(entity, idInfo.getColumn()); + try { + Invoker setInvoker = tableInfo.getReflector().getSetInvoker(idInfo.getProperty()); + Object id = convert(generateId, setInvoker.getType()); + setInvoker.invoke(entity, new Object[]{id}); + } catch (Exception e) { + throw FlexExceptions.wrap(e); + } + } + + + public Object convert(Object value, Class targetClass) { + if (value == null || (value.getClass() == String.class && StringUtil.isBlank((String) value) + && targetClass != String.class)) { + return null; + } + if (value.getClass().isAssignableFrom(targetClass)) { + return value; + } + if (targetClass == String.class) { + return value.toString(); + } else if (targetClass == Long.class || targetClass == long.class) { + if (value instanceof Number) { + return ((Number) value).longValue(); + } + return Long.parseLong(value.toString()); + } else if (targetClass == java.math.BigInteger.class) { + return new java.math.BigInteger(value.toString()); + } else if (targetClass == java.math.BigDecimal.class) { + return new java.math.BigDecimal(value.toString()); + } else if (targetClass == Integer.class || targetClass == int.class) { + if (value instanceof Number) { + return ((Number) value).intValue(); + } + return Integer.parseInt(value.toString()); + } else if (targetClass == Double.class || targetClass == double.class) { + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + return Double.parseDouble(value.toString()); + } else if (targetClass == Float.class || targetClass == float.class) { + if (value instanceof Number) { + return ((Number) value).floatValue(); + } + return Float.parseFloat(value.toString()); + } else if (targetClass == Short.class || targetClass == short.class) { + if (value instanceof Number) { + return ((Number) value).shortValue(); + } + return Short.parseShort(value.toString()); + } + + throw FlexExceptions.wrap("\"%s\" can not be parsed for primary key.", value); + } + + + @Override + public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + //do nothing + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/IKeyGenerator.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/IKeyGenerator.java new file mode 100644 index 00000000..2078553d --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/IKeyGenerator.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.key; + +public interface IKeyGenerator { + + Object generate(Object entity,String keyColumn); + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/IMultiKeyGenerator.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/IMultiKeyGenerator.java new file mode 100644 index 00000000..a7f2d611 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/IMultiKeyGenerator.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.key; + +public interface IMultiKeyGenerator { + + /** + * 是否需要数据库生成主键 + * + * @return true 需要生成主键 + */ + boolean isNeedGeneratedKeys(); + + /** + * 数据库主键的列名 + * + * @return 列名数组 + */ + String[] getKeyColumnNames(); +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/KeyGeneratorFactory.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/KeyGeneratorFactory.java new file mode 100644 index 00000000..5d60d352 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/KeyGeneratorFactory.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.key; + +import com.mybatisflex.core.key.impl.UUIDKeyGenerator; + +import java.util.HashMap; +import java.util.Map; + +public class KeyGeneratorFactory { + + private static final Map KEY_GENERATOR_MAP = new HashMap<>(); + static { + /** 内置了 uuid 的生成器,因此主键配置的时候可以直接配置为 @Id(keyType = KeyType.Generator, value = "uuid") + * {@link com.mybatisflex.annotation.Id} + */ + register("uuid", new UUIDKeyGenerator()); + } + + + /** + * 获取 主键生成器 + * + * @param name + * @return + */ + public static IKeyGenerator getKeyGenerator(String name) { + return KEY_GENERATOR_MAP.get(name.trim()); + } + + + /** + * 注册一个主键生成器 + * + * @param key + * @param keyGenerator + */ + public static void register(String key, IKeyGenerator keyGenerator) { + KEY_GENERATOR_MAP.put(key.trim(), keyGenerator); + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/MultiEntityKeyGenerator.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/MultiEntityKeyGenerator.java new file mode 100644 index 00000000..19226327 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/MultiEntityKeyGenerator.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.key; + +import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.util.CollectionUtil; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; + +import java.sql.Statement; +import java.util.List; +import java.util.Map; + +/** + * 多实体主键生成器,用于批量插入的场景 + */ +public class MultiEntityKeyGenerator implements KeyGenerator { + + private KeyGenerator keyGenerator; + + public MultiEntityKeyGenerator(KeyGenerator keyGenerator) { + this.keyGenerator = keyGenerator; + } + + @Override + public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + List entities = (List) ((Map) parameter).get(FlexConsts.ENTITIES); + if (CollectionUtil.isNotEmpty(entities)) { + for (Object entity : entities) { + ((Map) parameter).put(FlexConsts.ENTITY, entity); + keyGenerator.processBefore(executor, ms, stmt, parameter); + } + } + } + + + @Override + public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + // do nothing + // 多条数据批量插入的场景下,不支持后设置主键 + // 比如 INSERT INTO `tb_account`(uuid,name,sex) VALUES (?, ?, ?), (?, ?, ?) + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/MultiPrimaryKeyGenerator.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/MultiPrimaryKeyGenerator.java new file mode 100644 index 00000000..41abd118 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/MultiPrimaryKeyGenerator.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.key; + +import com.mybatisflex.core.table.IdInfo; +import com.mybatisflex.core.table.TableInfo; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; + +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +/** + * 多主键、复合主键 id 生成器 + */ +public class MultiPrimaryKeyGenerator implements KeyGenerator, IMultiKeyGenerator { + + private List keyGenerators; + + //所有自增字段的名称 + private String[] autoGenKeyColumnNames; + + public MultiPrimaryKeyGenerator(MappedStatement mappedStatement, TableInfo tableInfo, List primaryKeyList) { + this.keyGenerators = new ArrayList<>(); + + List autoGenKeyColumnNameList = new ArrayList<>(); + for (IdInfo idInfo : primaryKeyList) { + KeyGenerator idKeyGenerator = MybatisKeyGeneratorUtil.createIdKeyGenerator(tableInfo, mappedStatement, idInfo); + keyGenerators.add(idKeyGenerator); + if (idKeyGenerator == Jdbc3KeyGenerator.INSTANCE) { + autoGenKeyColumnNameList.add(idInfo.getColumn()); + } + } + + this.autoGenKeyColumnNames = autoGenKeyColumnNameList.toArray(new String[0]); + } + + @Override + public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + for (KeyGenerator keyGenerator : keyGenerators) { + keyGenerator.processBefore(executor, ms, stmt, parameter); + } + } + + + @Override + public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + for (KeyGenerator keyGenerator : keyGenerators) { + keyGenerator.processAfter(executor, ms, stmt, parameter); + } + } + + /** + * 是否需要数据库 自动生成主键 + * + * @return true: need generated keys + */ + @Override + public boolean isNeedGeneratedKeys() { + for (KeyGenerator keyGenerator : keyGenerators) { + if (keyGenerator == Jdbc3KeyGenerator.INSTANCE) { + return true; + } + } + return false; + } + + /** + * 自动生成主键的 columns 字段 + * + * @return keyColumnNames + */ + @Override + public String[] getKeyColumnNames() { + return autoGenKeyColumnNames; + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/MultiRowKeyGenerator.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/MultiRowKeyGenerator.java new file mode 100644 index 00000000..dba5f5d0 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/MultiRowKeyGenerator.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.key; + +import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.row.Row; +import com.mybatisflex.core.util.CollectionUtil; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; + +import java.sql.Statement; +import java.util.List; +import java.util.Map; + +/** + * 用于批量插入的场景 + */ +public class MultiRowKeyGenerator implements KeyGenerator { + + private KeyGenerator keyGenerator; + + public MultiRowKeyGenerator(KeyGenerator keyGenerator) { + this.keyGenerator = keyGenerator; + } + + @Override + public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + List rows = (List) ((Map) parameter).get(FlexConsts.ROWS); + if (CollectionUtil.isNotEmpty(rows)) { + for (Row entity : rows) { + ((Map) parameter).put(FlexConsts.ROW, entity); + keyGenerator.processBefore(executor, ms, stmt, parameter); + } + } + } + + + @Override + public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + // do nothing + // 多条数据批量插入的场景下,不支持后设置主键 + // 比如 INSERT INTO `tb_account`(uuid,name,sex) VALUES (?, ?, ?), (?, ?, ?) + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/MybatisKeyGeneratorUtil.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/MybatisKeyGeneratorUtil.java new file mode 100644 index 00000000..bbe86df2 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/MybatisKeyGeneratorUtil.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.key; + +import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.enums.KeyType; +import com.mybatisflex.core.exception.FlexExceptions; +import com.mybatisflex.core.table.IdInfo; +import com.mybatisflex.core.table.TableInfo; +import com.mybatisflex.core.util.StringUtil; +import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.executor.keygen.NoKeyGenerator; +import org.apache.ibatis.executor.keygen.SelectKeyGenerator; +import org.apache.ibatis.mapping.*; +import org.apache.ibatis.session.Configuration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class MybatisKeyGeneratorUtil { + + + public static KeyGenerator createTableKeyGenerator(TableInfo tableInfo, MappedStatement ms) { + List primaryKeyList = tableInfo.getPrimaryKeyList(); + + //无主键 + if (primaryKeyList == null || primaryKeyList.isEmpty()) { + return NoKeyGenerator.INSTANCE; + } + + //多主键的 + if (primaryKeyList.size() > 1) { + return new MultiPrimaryKeyGenerator(ms, tableInfo, primaryKeyList); + } + + return createIdKeyGenerator(tableInfo, ms, primaryKeyList.get(0)); + } + + + public static KeyGenerator createIdKeyGenerator(TableInfo tableInfo, MappedStatement ms, IdInfo idInfo) { + if (idInfo.getKeyType() == KeyType.None) { + return NoKeyGenerator.INSTANCE; + } + + //自增主键 + if (idInfo.getKeyType() == KeyType.Auto) { + return Jdbc3KeyGenerator.INSTANCE; + } + + //通过 java 生成的主键 + if (idInfo.getKeyType() == KeyType.Generator) { + return new CustomKeyGenerator(ms.getConfiguration(), tableInfo, idInfo); + } + + //通过序列生成的注解 + String sequence = idInfo.getValue(); + if (StringUtil.isBlank(sequence)) { + throw FlexExceptions.wrap("please config @Id(value=\"...\") for field: %s in class: %s" + , idInfo.getProperty() + , tableInfo.getEntityClass().getName()); + } + + + String selectId = ms.getId() + SelectKeyGenerator.SELECT_KEY_SUFFIX; + SqlSource sqlSource = ms.getLang().createSqlSource(ms.getConfiguration(), sequence.trim(), idInfo.getPropertyType()); + MappedStatement.Builder msBuilder = new MappedStatement.Builder(ms.getConfiguration(), selectId, sqlSource, SqlCommandType.SELECT) + .resource(ms.getResource()) + .fetchSize(null) + .timeout(null) + .statementType(StatementType.PREPARED) + .keyGenerator(NoKeyGenerator.INSTANCE) + .keyProperty(FlexConsts.ENTITY + "." + idInfo.getProperty()) + .keyColumn(idInfo.getColumn()) + .databaseId(ms.getDatabaseId()) + .lang(ms.getLang()) + .resultOrdered(false) + .resultSets(null) + .resultMaps(createIdResultMaps(ms.getConfiguration(), selectId + "-Inline", idInfo.getPropertyType(), new ArrayList<>())) + .resultSetType(null) + .flushCacheRequired(false) + .useCache(false) + .cache(ms.getCache()); + + MappedStatement keyMappedStatement = msBuilder.build(); + ms.getConfiguration().addMappedStatement(keyMappedStatement); + + //看到有的框架把 keyGenerator 添加到 mybatis 的当前配置里去,其实是完全没必要的 + //因为只有在 xml 解析的时候,才可能存在多一个 MappedStatement 拥有同一个 keyGenerator 的情况 + //当前每个方法都拥有一个自己的 keyGenerator 了,没必要添加 + //this.addKeyGenerator(selectId, keyGenerator); + + return new SelectKeyGenerator(keyMappedStatement, idInfo.isBefore()); + } + + + private static List createIdResultMaps(Configuration configuration, + String statementId, Class resultType, List resultMappings) { + ResultMap resultMap = new ResultMap.Builder(configuration, statementId, resultType, resultMappings, null) + .build(); + return Arrays.asList(resultMap); + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/RowCustomKeyGenerator.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/RowCustomKeyGenerator.java new file mode 100644 index 00000000..f62afe4a --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/RowCustomKeyGenerator.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.key; + +import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.exception.FlexExceptions; +import com.mybatisflex.core.row.Row; +import com.mybatisflex.core.row.RowKey; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; + +import java.sql.Statement; +import java.util.Map; + +/** + * 通过 java 编码的方式生成主键 + * 当主键类型配置为 KeyType#Generator 时,使用此生成器生成 + * {@link com.mybatisflex.core.enums.KeyType#Generator} + */ +public class RowCustomKeyGenerator implements KeyGenerator { + + protected RowKey rowKey; + protected IKeyGenerator keyGenerator; + + + public RowCustomKeyGenerator(RowKey rowKey) { + this.rowKey = rowKey; + this.keyGenerator = KeyGeneratorFactory.getKeyGenerator(rowKey.getValue()); + + ensuresKeyGeneratorNotNull(); + } + + private void ensuresKeyGeneratorNotNull() { + if (keyGenerator == null) { + throw FlexExceptions.wrap("The name of \"%s\" key generator not exist.", rowKey.getValue()); + } + } + + + @Override + public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + Row row = (Row) ((Map) parameter).get(FlexConsts.ROW); + Object generateId = keyGenerator.generate(row, rowKey.getKeyColumn()); + try { + row.put(rowKey.getKeyColumn(), generateId); + } catch (Exception e) { + throw FlexExceptions.wrap(e); + } + } + + + @Override + public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + //do nothing + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/RowJdbc3KeyGenerator.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/RowJdbc3KeyGenerator.java new file mode 100644 index 00000000..965edb78 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/RowJdbc3KeyGenerator.java @@ -0,0 +1,270 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.key; + +import com.mybatisflex.core.FlexConsts; +import org.apache.ibatis.binding.MapperMethod.ParamMap; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.ExecutorException; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.reflection.ArrayUtil; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.ParamNameResolver; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.defaults.DefaultSqlSession.StrictMap; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.TypeHandler; +import org.apache.ibatis.type.TypeHandlerRegistry; +import org.apache.ibatis.util.MapUtil; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.*; +import java.util.Map.Entry; + +/** + * 主要作用是为 Row 生成自增主键 + */ +public class RowJdbc3KeyGenerator implements KeyGenerator { + + private String keyProperty; + + + private static final String SECOND_GENERIC_PARAM_NAME = ParamNameResolver.GENERIC_NAME_PREFIX + "2"; + private static final String MSG_TOO_MANY_KEYS = "Too many keys are generated. There are only %d target objects. " + + "You either specified a wrong 'keyProperty' or encountered a driver bug like #1523."; + + + public RowJdbc3KeyGenerator(String keyProperty) { + this.keyProperty = keyProperty; + } + + @Override + public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + // do nothing + } + + @Override + public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + processBatch(ms, stmt, parameter); + } + + public void processBatch(MappedStatement ms, Statement stmt, Object parameter) { + final String[] keyProperties = new String[]{FlexConsts.ROW + "." + keyProperty}; + try (ResultSet rs = stmt.getGeneratedKeys()) { + final ResultSetMetaData rsmd = rs.getMetaData(); + final Configuration configuration = ms.getConfiguration(); + if (rsmd.getColumnCount() < keyProperties.length) { + // Error? + } else { + assignKeys(configuration, rs, rsmd, keyProperties, parameter); + } + } catch (Exception e) { + throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e); + } + } + + @SuppressWarnings("unchecked") + private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties, + Object parameter) throws SQLException { + if (parameter instanceof ParamMap || parameter instanceof StrictMap) { + // Multi-param or single param with @Param + assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map) parameter); + } else if (parameter instanceof ArrayList && !((ArrayList) parameter).isEmpty() + && ((ArrayList) parameter).get(0) instanceof ParamMap) { + // Multi-param or single param with @Param in batch operation + assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, (ArrayList>) parameter); + } else { + // Single param without @Param + assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter); + } + } + + private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, + String[] keyProperties, Object parameter) throws SQLException { + Collection params = collectionize(parameter); + if (params.isEmpty()) { + return; + } + List assignerList = new ArrayList<>(); + for (int i = 0; i < keyProperties.length; i++) { + assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i])); + } + Iterator iterator = params.iterator(); + while (rs.next()) { + if (!iterator.hasNext()) { + throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size())); + } + Object param = iterator.next(); + assignerList.forEach(x -> x.assign(rs, param)); + } + } + + private void assignKeysToParamMapList(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, + String[] keyProperties, ArrayList> paramMapList) throws SQLException { + Iterator> iterator = paramMapList.iterator(); + List assignerList = new ArrayList<>(); + long counter = 0; + while (rs.next()) { + if (!iterator.hasNext()) { + throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, counter)); + } + ParamMap paramMap = iterator.next(); + if (assignerList.isEmpty()) { + for (int i = 0; i < keyProperties.length; i++) { + assignerList + .add(getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i], keyProperties, false) + .getValue()); + } + } + assignerList.forEach(x -> x.assign(rs, paramMap)); + counter++; + } + } + + private void assignKeysToParamMap(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, + String[] keyProperties, Map paramMap) throws SQLException { + if (paramMap.isEmpty()) { + return; + } + Map, List>> assignerMap = new HashMap<>(); + for (int i = 0; i < keyProperties.length; i++) { + Entry entry = getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i], + keyProperties, true); + Entry, List> iteratorPair = MapUtil.computeIfAbsent(assignerMap, entry.getKey(), + k -> MapUtil.entry(collectionize(paramMap.get(k)).iterator(), new ArrayList<>())); + iteratorPair.getValue().add(entry.getValue()); + } + long counter = 0; + while (rs.next()) { + for (Entry, List> pair : assignerMap.values()) { + if (!pair.getKey().hasNext()) { + throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, counter)); + } + Object param = pair.getKey().next(); + pair.getValue().forEach(x -> x.assign(rs, param)); + } + counter++; + } + } + + private Entry getAssignerForParamMap(Configuration config, ResultSetMetaData rsmd, + int columnPosition, Map paramMap, String keyProperty, String[] keyProperties, boolean omitParamName) { + Set keySet = paramMap.keySet(); + // A caveat : if the only parameter has {@code @Param("param2")} on it, + // it must be referenced with param name e.g. 'param2.x'. + boolean singleParam = !keySet.contains(SECOND_GENERIC_PARAM_NAME); + int firstDot = keyProperty.indexOf('.'); + if (firstDot == -1) { + if (singleParam) { + return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName); + } + throw new ExecutorException("Could not determine which parameter to assign generated keys to. " + + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). " + + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are " + + keySet); + } + String paramName = keyProperty.substring(0, firstDot); + if (keySet.contains(paramName)) { + String argParamName = omitParamName ? null : paramName; + String argKeyProperty = keyProperty.substring(firstDot + 1); + return MapUtil.entry(paramName, new KeyAssigner(config, rsmd, columnPosition, argParamName, argKeyProperty)); + } else if (singleParam) { + return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName); + } else { + throw new ExecutorException("Could not find parameter '" + paramName + "'. " + + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). " + + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are " + + keySet); + } + } + + private Entry getAssignerForSingleParam(Configuration config, ResultSetMetaData rsmd, + int columnPosition, Map paramMap, String keyProperty, boolean omitParamName) { + // Assume 'keyProperty' to be a property of the single param. + String singleParamName = nameOfSingleParam(paramMap); + String argParamName = omitParamName ? null : singleParamName; + return MapUtil.entry(singleParamName, new KeyAssigner(config, rsmd, columnPosition, argParamName, keyProperty)); + } + + private static String nameOfSingleParam(Map paramMap) { + // There is virtually one parameter, so any key works. + return paramMap.keySet().iterator().next(); + } + + private static Collection collectionize(Object param) { + if (param instanceof Collection) { + return (Collection) param; + } else if (param instanceof Object[]) { + return Arrays.asList((Object[]) param); + } else { + return Arrays.asList(param); + } + } + + private class KeyAssigner { + private final Configuration configuration; + private final ResultSetMetaData rsmd; + private final TypeHandlerRegistry typeHandlerRegistry; + private final int columnPosition; + private final String paramName; + private final String propertyName; + private TypeHandler typeHandler; + + protected KeyAssigner(Configuration configuration, ResultSetMetaData rsmd, int columnPosition, String paramName, + String propertyName) { + super(); + this.configuration = configuration; + this.rsmd = rsmd; + this.typeHandlerRegistry = configuration.getTypeHandlerRegistry(); + this.columnPosition = columnPosition; + this.paramName = paramName; + this.propertyName = propertyName; + } + + protected void assign(ResultSet rs, Object param) { + if (paramName != null) { + // If paramName is set, param is ParamMap + param = ((ParamMap) param).get(paramName); + } + MetaObject metaParam = configuration.newMetaObject(param); + try { + if (typeHandler == null) { + if (metaParam.hasSetter(propertyName)) { + Class propertyType = metaParam.getSetterType(propertyName); + typeHandler = typeHandlerRegistry.getTypeHandler(propertyType, + JdbcType.forCode(rsmd.getColumnType(columnPosition))); + } else { + throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '" + + metaParam.getOriginalObject().getClass().getName() + "'."); + } + } + if (typeHandler == null) { + // Error? + } else { + Object value = typeHandler.getResult(rs, columnPosition); + metaParam.setValue(propertyName, value); + } + } catch (SQLException e) { + throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, + e); + } + } + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/RowKeyGenerator.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/RowKeyGenerator.java new file mode 100644 index 00000000..b44d48bc --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/RowKeyGenerator.java @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.key; + +import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.enums.KeyType; +import com.mybatisflex.core.row.Row; +import com.mybatisflex.core.row.RowKey; +import com.mybatisflex.core.util.ArrayUtil; +import com.mybatisflex.core.util.CollectionUtil; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.executor.keygen.NoKeyGenerator; +import org.apache.ibatis.executor.keygen.SelectKeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; +import org.apache.ibatis.mapping.SqlSource; +import org.apache.ibatis.mapping.StatementType; + +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 为 row 的主键生成器 + */ +public class RowKeyGenerator implements KeyGenerator, IMultiKeyGenerator { + private static KeyGenerator[] NO_KEY_GENERATORS = new KeyGenerator[0]; + + private MappedStatement ms; + private KeyGenerator[] keyGenerators; + private List autoKeyGeneratorNames; + + public RowKeyGenerator(MappedStatement methodMappedStatement) { + this.ms = methodMappedStatement; + } + + @Override + public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + Row row = (Row) ((Map) parameter).get(FlexConsts.ROW); + keyGenerators = buildRowKeyGenerators(row.obtainsPrimaryKeys()); + for (KeyGenerator keyGenerator : keyGenerators) { + keyGenerator.processBefore(executor, ms, stmt, parameter); + } + } + + + @Override + public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { + for (KeyGenerator keyGenerator : keyGenerators) { + keyGenerator.processAfter(executor, ms, stmt, parameter); + } + } + + + private KeyGenerator[] buildRowKeyGenerators(RowKey[] rowKeys) { + if (ArrayUtil.isEmpty(rowKeys)) { + return NO_KEY_GENERATORS; + } + + KeyGenerator[] keyGenerators = new KeyGenerator[rowKeys.length]; + for (int i = 0; i < rowKeys.length; i++) { + KeyGenerator keyGenerator = createByRowKey(rowKeys[i]); + keyGenerators[i] = keyGenerator; + } + return keyGenerators; + } + + + private KeyGenerator createByRowKey(RowKey rowKey) { + if (rowKey == null || rowKey.getKeyType() == KeyType.None) { + return NoKeyGenerator.INSTANCE; + } + + if (rowKey.getKeyType() == KeyType.Auto) { + if (autoKeyGeneratorNames == null) { + autoKeyGeneratorNames = new ArrayList<>(); + } + autoKeyGeneratorNames.add(rowKey.getKeyColumn()); + return new RowJdbc3KeyGenerator(rowKey.getKeyColumn()); + } + + if (rowKey.getKeyType() == KeyType.Generator) { + return new RowCustomKeyGenerator(rowKey); + } + //通过数据库的 sequence 生成主键 + else { + String selectId = "row." + SelectKeyGenerator.SELECT_KEY_SUFFIX; + String sequence = rowKey.getValue(); + SqlSource sqlSource = ms.getLang().createSqlSource(ms.getConfiguration(), sequence.trim(), Object.class); + MappedStatement.Builder msBuilder = new MappedStatement.Builder(ms.getConfiguration(), selectId, sqlSource, SqlCommandType.SELECT) + .resource(ms.getResource()) + .fetchSize(null) + .timeout(null) + .statementType(StatementType.PREPARED) + .keyGenerator(NoKeyGenerator.INSTANCE) + .keyProperty(FlexConsts.ROW + "." + rowKey.getKeyColumn()) + .keyColumn(FlexConsts.ROW + "." + rowKey.getKeyColumn()) + .databaseId(ms.getDatabaseId()) + .lang(ms.getLang()) + .resultOrdered(false) + .resultSets(null) + .resultMaps(new ArrayList<>()) + .resultSetType(null) + .flushCacheRequired(false) + .useCache(false) + .cache(ms.getCache()); + + MappedStatement keyMappedStatement = msBuilder.build(); + ms.getConfiguration().addMappedStatement(keyMappedStatement); + + //看到有的框架把 keyGenerator 添加到 mybatis 的当前配置里去,其实是完全没必要的 + //因为只有在 xml 解析的时候,才可能存在多一个 MappedStatement 拥有同一个 keyGenerator 的情况 + //当前每个方法都拥有一个自己的 keyGenerator 了,没必要添加 + //this.addKeyGenerator(selectId, keyGenerator); + return new SelectKeyGenerator(keyMappedStatement, rowKey.isBefore()); + } + + } + + /** + * 是否需要数据库生成主键 + * + * @return true 需要生成 + */ + @Override + public boolean isNeedGeneratedKeys() { + return CollectionUtil.isNotEmpty(autoKeyGeneratorNames); + } + + /** + * 数据库主键定义的 key + * + * @return key 数组 + */ + @Override + public String[] getKeyColumnNames() { + return autoKeyGeneratorNames.toArray(new String[0]); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/impl/UUIDKeyGenerator.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/impl/UUIDKeyGenerator.java new file mode 100644 index 00000000..a2511526 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/key/impl/UUIDKeyGenerator.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.key.impl; + +import com.mybatisflex.core.key.IKeyGenerator; + +import java.util.UUID; + +public class UUIDKeyGenerator implements IKeyGenerator { + + @Override + public Object generate(Object entity, String keyColumn) { + return UUID.randomUUID().toString().replace("-", ""); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexConfiguration.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexConfiguration.java new file mode 100644 index 00000000..5a718682 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexConfiguration.java @@ -0,0 +1,246 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.mybatis; + +import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.key.MultiEntityKeyGenerator; +import com.mybatisflex.core.key.MultiRowKeyGenerator; +import com.mybatisflex.core.key.MybatisKeyGeneratorUtil; +import com.mybatisflex.core.row.RowMapper; +import com.mybatisflex.core.key.RowKeyGenerator; +import com.mybatisflex.core.table.TableInfo; +import com.mybatisflex.core.table.TableInfos; +import com.mybatisflex.core.util.CollectionUtil; +import com.mybatisflex.core.util.StringUtil; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.executor.keygen.NoKeyGenerator; +import org.apache.ibatis.executor.keygen.SelectKeyGenerator; +import org.apache.ibatis.executor.parameter.ParameterHandler; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.logging.stdout.StdOutImpl; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.ResultMap; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.util.Map; + +public class FlexConfiguration extends Configuration { + + + public FlexConfiguration(Environment environment) { + super(environment); + setMapUnderscoreToCamelCase(true); + setLogImpl(StdOutImpl.class); + initDefaultMappers(); + } + + public FlexConfiguration() { + setLogImpl(StdOutImpl.class); + initDefaultMappers(); + } + + /** + * 设置 mybatis-flex 默认的 Mapper + * 当前只有 RowMapper {@link RowMapper} + */ + private void initDefaultMappers() { + addMapper(RowMapper.class); + } + + + /** + * 为原生 sql 设置参数 + */ + @Override + public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { + String mappedStatementId = mappedStatement.getId(); + // 以 "!selectKey" 结尾的 mappedStatementId,是用于主键生成的,无需为其设置参数 + if (!mappedStatementId.endsWith(SelectKeyGenerator.SELECT_KEY_SUFFIX) + && parameterObject instanceof Map + && ((Map) parameterObject).containsKey(FlexConsts.SQL_ARGS)) { + return new SqlArgsParameterHandler(mappedStatement, (Map) parameterObject, boundSql); + } else { + return super.newParameterHandler(mappedStatement, parameterObject, boundSql); + } + } + + /** + * 替换为 FlexRoutingStatementHandler,主要用来为实体类的多主键做支持 + * FlexRoutingStatementHandler 和 原生的 RoutingStatementHandler 对比,没有任何性能影响 + */ + @Override + public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + StatementHandler statementHandler = new FlexRoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); + statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); + return statementHandler; + } + + @Override + public void addMappedStatement(MappedStatement ms) { + //替换 RowMapper.insertRow 的主键生成器 + //替换 RowMapper.insertBatchWithFirstRowColumns 的主键生成器 + if (ms.getId().startsWith("com.mybatisflex.core.row.RowMapper.insert")) { + ms = replaceRowKeyGenerator(ms); + } + //entity insert methods + else if (StringUtil.endsWithAny(ms.getId(), "insert", "insertBatchWithFirstEntityColumns") + && ms.getKeyGenerator() == NoKeyGenerator.INSTANCE) { + ms = replaceEntityKeyGenerator(ms); + } + //entity select + else if (StringUtil.endsWithAny(ms.getId(), "selectOneById", "selectListByIds" + , "selectListByQuery", "selectCountByQuery")) { + ms = replaceResultHandler(ms); + } + + super.addMappedStatement(ms); + } + + + /** + * 替换 entity 查询的 ResultHandler + */ + private MappedStatement replaceResultHandler(MappedStatement ms) { + + TableInfo tableInfo = getTableInfo(ms); + if (tableInfo == null) { + return ms; + } + + String resultMapId = tableInfo.getEntityClass().getName(); + + ResultMap resultMap; + if (hasResultMap(resultMapId)) { + resultMap = getResultMap(resultMapId); + } else { + resultMap = tableInfo.buildResultMap(this); + this.addResultMap(resultMap); + } + + return new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), ms.getSqlSource(), ms.getSqlCommandType()) + .resource(ms.getResource()) + .fetchSize(ms.getFetchSize()) + .timeout(ms.getTimeout()) + .statementType(ms.getStatementType()) + .keyGenerator(NoKeyGenerator.INSTANCE) + .keyProperty(ms.getKeyProperties() == null ? null : String.join(",", ms.getKeyProperties())) + .keyColumn(ms.getKeyColumns() == null ? null : String.join(",", ms.getKeyColumns())) + .databaseId(databaseId) + .lang(ms.getLang()) + .resultOrdered(ms.isResultOrdered()) + .resultSets(ms.getResultSets() == null ? null : String.join(",", ms.getResultSets())) + .resultMaps(CollectionUtil.newArrayList(resultMap)) // 替换resultMap + .resultSetType(ms.getResultSetType()) + .flushCacheRequired(ms.isFlushCacheRequired()) + .useCache(ms.isUseCache()) + .cache(ms.getCache()) + .build(); + } + + /** + * 生成新的、已替换主键生成器的 MappedStatement + * + * @param ms MappedStatement + * @return replaced MappedStatement + */ + private MappedStatement replaceRowKeyGenerator(MappedStatement ms) { + + + KeyGenerator keyGenerator = new RowKeyGenerator(ms); + if (ms.getId().endsWith("insertBatchWithFirstRowColumns")) { + keyGenerator = new MultiRowKeyGenerator(keyGenerator); + } + + return new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), ms.getSqlSource(), ms.getSqlCommandType()) + .resource(ms.getResource()) + .fetchSize(ms.getFetchSize()) + .timeout(ms.getTimeout()) + .statementType(ms.getStatementType()) + .keyGenerator(keyGenerator) // 替换主键生成器 + .keyProperty(ms.getKeyProperties() == null ? null : String.join(",", ms.getKeyProperties())) + .keyColumn(ms.getKeyColumns() == null ? null : String.join(",", ms.getKeyColumns())) + .databaseId(databaseId) + .lang(ms.getLang()) + .resultOrdered(ms.isResultOrdered()) + .resultSets(ms.getResultSets() == null ? null : String.join(",", ms.getResultSets())) + .resultMaps(ms.getResultMaps()) + .resultSetType(ms.getResultSetType()) + .flushCacheRequired(ms.isFlushCacheRequired()) + .useCache(ms.isUseCache()) + .cache(ms.getCache()) + .build(); + } + + /** + * 生成新的、已替换主键生成器的 MappedStatement + * + * @param ms MappedStatement + * @return replaced MappedStatement + */ + private MappedStatement replaceEntityKeyGenerator(MappedStatement ms) { + + TableInfo tableInfo = getTableInfo(ms); + if (tableInfo == null) { + return ms; + } + + KeyGenerator keyGenerator = MybatisKeyGeneratorUtil.createTableKeyGenerator(tableInfo, ms); + if (keyGenerator == NoKeyGenerator.INSTANCE) { + return ms; + } + + //批量插入 + if (ms.getId().endsWith("insertBatchWithFirstEntityColumns")) { + keyGenerator = new MultiEntityKeyGenerator(keyGenerator); + } + + return new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), ms.getSqlSource(), ms.getSqlCommandType()) + .resource(ms.getResource()) + .fetchSize(ms.getFetchSize()) + .timeout(ms.getTimeout()) + .statementType(ms.getStatementType()) + .keyGenerator(keyGenerator) // 替换主键生成器 + .keyProperty(tableInfo.getMappedStatementKeyProperties()) + .keyColumn(tableInfo.getMappedStatementKeyColumns()) + .databaseId(databaseId) + .lang(ms.getLang()) + .resultOrdered(ms.isResultOrdered()) + .resultSets(ms.getResultSets() == null ? null : String.join(",", ms.getResultSets())) + .resultMaps(ms.getResultMaps()) + .resultSetType(ms.getResultSetType()) + .flushCacheRequired(ms.isFlushCacheRequired()) + .useCache(ms.isUseCache()) + .cache(ms.getCache()) + .build(); + } + + private TableInfo getTableInfo(MappedStatement ms) { + String mapperClassName = ms.getId().substring(0, ms.getId().lastIndexOf(".")); + try { + Class mapperClass = Class.forName(mapperClassName); + return TableInfos.ofMapperClass(mapperClass); + } catch (ClassNotFoundException e) { + return null; + } + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexPreparedStatementHandler.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexPreparedStatementHandler.java new file mode 100644 index 00000000..3853f0fe --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexPreparedStatementHandler.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.mybatis; + +import com.mybatisflex.core.key.IMultiKeyGenerator; +import com.mybatisflex.core.util.ArrayUtil; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.executor.statement.PreparedStatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.ResultSetType; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.sql.*; + +/** + * @author Michael Yang(fuhai999@gmail.com) + */ +public class FlexPreparedStatementHandler extends PreparedStatementHandler { + + public FlexPreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql); + } + + + @Override + protected Statement instantiateStatement(Connection connection) throws SQLException { +// String sql = boundSql.getSql(); +// if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) { +// String[] keyColumnNames = mappedStatement.getKeyColumns(); +// if (keyColumnNames == null) { +// return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); +// } else { +// return connection.prepareStatement(sql, keyColumnNames); +// } +// } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) { +// return connection.prepareStatement(sql); +// } else { +// return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY); +// } + + String sql = boundSql.getSql(); + KeyGenerator keyGenerator = mappedStatement.getKeyGenerator(); + if (keyGenerator instanceof Jdbc3KeyGenerator) { + String[] keyColumnNames = mappedStatement.getKeyColumns(); + if (keyColumnNames == null) { + return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); + } else { + return connection.prepareStatement(sql, keyColumnNames); + } + } + // 多主键的场景 + else if (keyGenerator instanceof IMultiKeyGenerator) { + if (((IMultiKeyGenerator) keyGenerator).isNeedGeneratedKeys()) { + String[] keyColumnNames = ((IMultiKeyGenerator) keyGenerator).getKeyColumnNames(); + if (ArrayUtil.isNotEmpty(keyColumnNames)) { + return connection.prepareStatement(sql, keyColumnNames); + } else { + return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); + } + } + } + + if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) { + return connection.prepareStatement(sql); + } else { + return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY); + } + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexRoutingStatementHandler.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexRoutingStatementHandler.java new file mode 100644 index 00000000..90e1102c --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexRoutingStatementHandler.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.mybatis; + +import org.apache.ibatis.cursor.Cursor; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.ExecutorException; +import org.apache.ibatis.executor.parameter.ParameterHandler; +import org.apache.ibatis.executor.statement.CallableStatementHandler; +import org.apache.ibatis.executor.statement.SimpleStatementHandler; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +/** + * 源于 {@link org.apache.ibatis.executor.statement.RoutingStatementHandler} + * 主要是替换 PreparedStatementHandler 为 FlexPreparedStatementHandler + */ +public class FlexRoutingStatementHandler implements StatementHandler { + + private final StatementHandler delegate; + + public FlexRoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + + switch (ms.getStatementType()) { + case STATEMENT: + delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + case PREPARED: + // delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + // use FlexPreparedStatementHandler to replace PreparedStatementHandler + delegate = new FlexPreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + case CALLABLE: + delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + default: + throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); + } + + } + + @Override + public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException { + return delegate.prepare(connection, transactionTimeout); + } + + @Override + public void parameterize(Statement statement) throws SQLException { + delegate.parameterize(statement); + } + + @Override + public void batch(Statement statement) throws SQLException { + delegate.batch(statement); + } + + @Override + public int update(Statement statement) throws SQLException { + return delegate.update(statement); + } + + @Override + public List query(Statement statement, ResultHandler resultHandler) throws SQLException { + return delegate.query(statement, resultHandler); + } + + @Override + public Cursor queryCursor(Statement statement) throws SQLException { + return delegate.queryCursor(statement); + } + + @Override + public BoundSql getBoundSql() { + return delegate.getBoundSql(); + } + + @Override + public ParameterHandler getParameterHandler() { + return delegate.getParameterHandler(); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexSqlSessionFactoryBuilder.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexSqlSessionFactoryBuilder.java new file mode 100644 index 00000000..deb3621e --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexSqlSessionFactoryBuilder.java @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.mybatis; + +import com.mybatisflex.core.FlexGlobalConfig; +import com.mybatisflex.core.dialect.DbType; +import com.mybatisflex.core.dialect.DbTypeUtil; +import com.mybatisflex.core.exception.FlexExceptions; +import com.mybatisflex.core.util.StringUtil; +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.exceptions.ExceptionFactory; +import org.apache.ibatis.executor.ErrorContext; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; + +import javax.sql.DataSource; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.Properties; + +public class FlexSqlSessionFactoryBuilder extends SqlSessionFactoryBuilder { + + @Override + public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { + try { + FlexXMLConfigBuilder parser = new FlexXMLConfigBuilder(inputStream, environment, properties); + return build(parser.parse()); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error building SqlSession.", e); + } finally { + ErrorContext.instance().reset(); + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + // Intentionally ignore. Prefer previous error. + } + } + } + + + @Override + public SqlSessionFactory build(Configuration configuration) { + if (!FlexConfiguration.class.isAssignableFrom(configuration.getClass())) { + throw FlexExceptions.wrap("only support FlexMybatisConfiguration."); + } + + SqlSessionFactory sessionFactory = super.build(configuration); + + DbType dbType = getDbType(configuration); + + //设置全局配置的 sessionFactory 和 dbType + initGlobalConfig(configuration, sessionFactory, dbType); + + return sessionFactory; + } + + + /** + * 设置全局配置 + * + * @param config + * @param sessionFactory + */ + private void initGlobalConfig(Configuration config, SqlSessionFactory sessionFactory, DbType dbType) { + FlexGlobalConfig flexGlobalConfig = new FlexGlobalConfig(); + flexGlobalConfig.setSqlSessionFactory(sessionFactory); + flexGlobalConfig.setDbType(dbType); + + String environmentId = config.getEnvironment().getId(); + FlexGlobalConfig.setConfig(environmentId, flexGlobalConfig); + } + + + /** + * 获取当前配置的 DbType + */ + private DbType getDbType(Configuration configuration) { + DataSource dataSource = configuration.getEnvironment().getDataSource(); + String jdbcUrl = getJdbcUrl(dataSource); + if (StringUtil.isNotBlank(jdbcUrl)){ + return DbTypeUtil.parseDbType(jdbcUrl); + } + + if ("org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory$EmbeddedDataSourceProxy" + .equals(dataSource.getClass().getName())){ + return DbType.H2; + } + + return null; + } + + /** + * 通过数据源中获取 jdbc 的 url 配置 + * 符合 HikariCP, druid, c3p0, DBCP, beecp 数据源框架 以及 MyBatis UnpooledDataSource 的获取规则 + * UnpooledDataSource 参考 @{@link UnpooledDataSource#getUrl()} + * TODO: 2023/2/18 可能极个别特殊的数据源无法获取 JDBC 配置的 URL + * + * @return jdbc url 配置 + */ + private String getJdbcUrl(DataSource dataSource) { + String[] methodNames = new String[]{"getUrl", "getJdbcUrl"}; + for (String methodName : methodNames) { + try { + Method method = dataSource.getClass().getMethod(methodName); + return (String) method.invoke(dataSource); + } catch (Exception e) { + //ignore + } + } + return null; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexXMLConfigBuilder.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexXMLConfigBuilder.java new file mode 100644 index 00000000..b636f5b1 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexXMLConfigBuilder.java @@ -0,0 +1,409 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.mybatis; + +import org.apache.ibatis.builder.BaseBuilder; +import org.apache.ibatis.builder.BuilderException; +import org.apache.ibatis.builder.xml.XMLMapperBuilder; +import org.apache.ibatis.builder.xml.XMLMapperEntityResolver; +import org.apache.ibatis.datasource.DataSourceFactory; +import org.apache.ibatis.executor.ErrorContext; +import org.apache.ibatis.executor.loader.ProxyFactory; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.io.VFS; +import org.apache.ibatis.logging.Log; +import org.apache.ibatis.mapping.DatabaseIdProvider; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.parsing.XNode; +import org.apache.ibatis.parsing.XPathParser; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.reflection.DefaultReflectorFactory; +import org.apache.ibatis.reflection.MetaClass; +import org.apache.ibatis.reflection.ReflectorFactory; +import org.apache.ibatis.reflection.factory.ObjectFactory; +import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory; +import org.apache.ibatis.session.*; +import org.apache.ibatis.transaction.TransactionFactory; +import org.apache.ibatis.type.JdbcType; + +import javax.sql.DataSource; +import java.io.InputStream; +import java.io.Reader; +import java.util.Properties; + +/** + * @author Clinton Begin + * @author Kazuki Shimizu + */ +public class FlexXMLConfigBuilder extends BaseBuilder { + + private boolean parsed; + private final XPathParser parser; + private String environment; + private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory(); + + public FlexXMLConfigBuilder(Reader reader) { + this(reader, null, null); + } + + public FlexXMLConfigBuilder(Reader reader, String environment) { + this(reader, environment, null); + } + + public FlexXMLConfigBuilder(Reader reader, String environment, Properties props) { + this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props); + } + + public FlexXMLConfigBuilder(InputStream inputStream) { + this(inputStream, null, null); + } + + public FlexXMLConfigBuilder(InputStream inputStream, String environment) { + this(inputStream, environment, null); + } + + public FlexXMLConfigBuilder(InputStream inputStream, String environment, Properties props) { + this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props); + } + + private FlexXMLConfigBuilder(XPathParser parser, String environment, Properties props) { + // 目前暂时只能通过这里替换 configuration + // 新的版本已经支持自定义 configuration,但需新版本 release: + // 详情 https://github.com/mybatis/mybatis-3/commit/d7826d71a7005a8b4d4e0e7a020db0f6c7e253a4 + super(new FlexConfiguration()); + ErrorContext.instance().resource("SQL Mapper Configuration"); + this.configuration.setVariables(props); + this.parsed = false; + this.environment = environment; + this.parser = parser; + } + + public Configuration parse() { + if (parsed) { + throw new BuilderException("Each XMLConfigBuilder can only be used once."); + } + parsed = true; + parseConfiguration(parser.evalNode("/configuration")); + return configuration; + } + + private void parseConfiguration(XNode root) { + try { + // issue #117 read properties first + propertiesElement(root.evalNode("properties")); + Properties settings = settingsAsProperties(root.evalNode("settings")); + loadCustomVfs(settings); + loadCustomLogImpl(settings); + typeAliasesElement(root.evalNode("typeAliases")); + pluginElement(root.evalNode("plugins")); + objectFactoryElement(root.evalNode("objectFactory")); + objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); + reflectorFactoryElement(root.evalNode("reflectorFactory")); + settingsElement(settings); + // read it after objectFactory and objectWrapperFactory issue #631 + environmentsElement(root.evalNode("environments")); + databaseIdProviderElement(root.evalNode("databaseIdProvider")); + typeHandlerElement(root.evalNode("typeHandlers")); + mapperElement(root.evalNode("mappers")); + } catch (Exception e) { + throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); + } + } + + private Properties settingsAsProperties(XNode context) { + if (context == null) { + return new Properties(); + } + Properties props = context.getChildrenAsProperties(); + // Check that all settings are known to the configuration class + MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); + for (Object key : props.keySet()) { + if (!metaConfig.hasSetter(String.valueOf(key))) { + throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); + } + } + return props; + } + + private void loadCustomVfs(Properties props) throws ClassNotFoundException { + String value = props.getProperty("vfsImpl"); + if (value != null) { + String[] clazzes = value.split(","); + for (String clazz : clazzes) { + if (!clazz.isEmpty()) { + @SuppressWarnings("unchecked") + Class vfsImpl = (Class)Resources.classForName(clazz); + configuration.setVfsImpl(vfsImpl); + } + } + } + } + + private void loadCustomLogImpl(Properties props) { + Class logImpl = resolveClass(props.getProperty("logImpl")); + configuration.setLogImpl(logImpl); + } + + private void typeAliasesElement(XNode parent) { + if (parent != null) { + for (XNode child : parent.getChildren()) { + if ("package".equals(child.getName())) { + String typeAliasPackage = child.getStringAttribute("name"); + configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); + } else { + String alias = child.getStringAttribute("alias"); + String type = child.getStringAttribute("type"); + try { + Class clazz = Resources.classForName(type); + if (alias == null) { + typeAliasRegistry.registerAlias(clazz); + } else { + typeAliasRegistry.registerAlias(alias, clazz); + } + } catch (ClassNotFoundException e) { + throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); + } + } + } + } + } + + private void pluginElement(XNode parent) throws Exception { + if (parent != null) { + for (XNode child : parent.getChildren()) { + String interceptor = child.getStringAttribute("interceptor"); + Properties properties = child.getChildrenAsProperties(); + Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); + interceptorInstance.setProperties(properties); + configuration.addInterceptor(interceptorInstance); + } + } + } + + private void objectFactoryElement(XNode context) throws Exception { + if (context != null) { + String type = context.getStringAttribute("type"); + Properties properties = context.getChildrenAsProperties(); + ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance(); + factory.setProperties(properties); + configuration.setObjectFactory(factory); + } + } + + private void objectWrapperFactoryElement(XNode context) throws Exception { + if (context != null) { + String type = context.getStringAttribute("type"); + ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).getDeclaredConstructor().newInstance(); + configuration.setObjectWrapperFactory(factory); + } + } + + private void reflectorFactoryElement(XNode context) throws Exception { + if (context != null) { + String type = context.getStringAttribute("type"); + ReflectorFactory factory = (ReflectorFactory) resolveClass(type).getDeclaredConstructor().newInstance(); + configuration.setReflectorFactory(factory); + } + } + + private void propertiesElement(XNode context) throws Exception { + if (context != null) { + Properties defaults = context.getChildrenAsProperties(); + String resource = context.getStringAttribute("resource"); + String url = context.getStringAttribute("url"); + if (resource != null && url != null) { + throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); + } + if (resource != null) { + defaults.putAll(Resources.getResourceAsProperties(resource)); + } else if (url != null) { + defaults.putAll(Resources.getUrlAsProperties(url)); + } + Properties vars = configuration.getVariables(); + if (vars != null) { + defaults.putAll(vars); + } + parser.setVariables(defaults); + configuration.setVariables(defaults); + } + } + + private void settingsElement(Properties props) { + configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL"))); + configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE"))); + configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true)); + configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory"))); + configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false)); + configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false)); + configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true)); + configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true)); + configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false)); + configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE"))); + configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null)); + configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null)); + configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType"))); + configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false)); + configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false)); + configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION"))); + configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER"))); + configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString")); + configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true)); + configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage"))); + configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler"))); + configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false)); + configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true)); + configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false)); + configuration.setLogPrefix(props.getProperty("logPrefix")); + configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory"))); + configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false)); + configuration.setArgNameBasedConstructorAutoMapping(booleanValueOf(props.getProperty("argNameBasedConstructorAutoMapping"), false)); + configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType"))); + configuration.setNullableOnForEach(booleanValueOf(props.getProperty("nullableOnForEach"), false)); + } + + private void environmentsElement(XNode context) throws Exception { + if (context != null) { + if (environment == null) { + environment = context.getStringAttribute("default"); + } + for (XNode child : context.getChildren()) { + String id = child.getStringAttribute("id"); + if (isSpecifiedEnvironment(id)) { + TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); + DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); + DataSource dataSource = dsFactory.getDataSource(); + Environment.Builder environmentBuilder = new Environment.Builder(id) + .transactionFactory(txFactory) + .dataSource(dataSource); + configuration.setEnvironment(environmentBuilder.build()); + break; + } + } + } + } + + private void databaseIdProviderElement(XNode context) throws Exception { + DatabaseIdProvider databaseIdProvider = null; + if (context != null) { + String type = context.getStringAttribute("type"); + // awful patch to keep backward compatibility + if ("VENDOR".equals(type)) { + type = "DB_VENDOR"; + } + Properties properties = context.getChildrenAsProperties(); + databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance(); + databaseIdProvider.setProperties(properties); + } + Environment environment = configuration.getEnvironment(); + if (environment != null && databaseIdProvider != null) { + String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource()); + configuration.setDatabaseId(databaseId); + } + } + + private TransactionFactory transactionManagerElement(XNode context) throws Exception { + if (context != null) { + String type = context.getStringAttribute("type"); + Properties props = context.getChildrenAsProperties(); + TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance(); + factory.setProperties(props); + return factory; + } + throw new BuilderException("Environment declaration requires a TransactionFactory."); + } + + private DataSourceFactory dataSourceElement(XNode context) throws Exception { + if (context != null) { + String type = context.getStringAttribute("type"); + Properties props = context.getChildrenAsProperties(); + DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance(); + factory.setProperties(props); + return factory; + } + throw new BuilderException("Environment declaration requires a DataSourceFactory."); + } + + private void typeHandlerElement(XNode parent) { + if (parent != null) { + for (XNode child : parent.getChildren()) { + if ("package".equals(child.getName())) { + String typeHandlerPackage = child.getStringAttribute("name"); + typeHandlerRegistry.register(typeHandlerPackage); + } else { + String javaTypeName = child.getStringAttribute("javaType"); + String jdbcTypeName = child.getStringAttribute("jdbcType"); + String handlerTypeName = child.getStringAttribute("handler"); + Class javaTypeClass = resolveClass(javaTypeName); + JdbcType jdbcType = resolveJdbcType(jdbcTypeName); + Class typeHandlerClass = resolveClass(handlerTypeName); + if (javaTypeClass != null) { + if (jdbcType == null) { + typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); + } else { + typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); + } + } else { + typeHandlerRegistry.register(typeHandlerClass); + } + } + } + } + } + + private void mapperElement(XNode parent) throws Exception { + if (parent != null) { + for (XNode child : parent.getChildren()) { + if ("package".equals(child.getName())) { + String mapperPackage = child.getStringAttribute("name"); + configuration.addMappers(mapperPackage); + } else { + String resource = child.getStringAttribute("resource"); + String url = child.getStringAttribute("url"); + String mapperClass = child.getStringAttribute("class"); + if (resource != null && url == null && mapperClass == null) { + ErrorContext.instance().resource(resource); + try(InputStream inputStream = Resources.getResourceAsStream(resource)) { + XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); + mapperParser.parse(); + } + } else if (resource == null && url != null && mapperClass == null) { + ErrorContext.instance().resource(url); + try(InputStream inputStream = Resources.getUrlAsStream(url)){ + XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); + mapperParser.parse(); + } + } else if (resource == null && url == null && mapperClass != null) { + Class mapperInterface = Resources.classForName(mapperClass); + configuration.addMapper(mapperInterface); + } else { + throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); + } + } + } + } + } + + private boolean isSpecifiedEnvironment(String id) { + if (environment == null) { + throw new BuilderException("No environment specified."); + } + if (id == null) { + throw new BuilderException("Environment requires an id attribute."); + } + return environment.equals(id); + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/SqlArgsParameterHandler.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/SqlArgsParameterHandler.java new file mode 100644 index 00000000..ecd3b072 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/SqlArgsParameterHandler.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.mybatis; + +import com.mybatisflex.core.FlexConsts; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Date; +import java.util.Map; + +public class SqlArgsParameterHandler extends DefaultParameterHandler { + + private final Map parameterObject; + + + public SqlArgsParameterHandler(MappedStatement mappedStatement, Map parameterObject, BoundSql boundSql) { + super(mappedStatement, parameterObject, boundSql); + this.parameterObject = parameterObject; + } + + + @Override + public void setParameters(PreparedStatement ps) { + try { + doSetParameters(ps); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + + private void doSetParameters(PreparedStatement ps) throws SQLException { + Object[] sqlArgs = (Object[]) ((Map) parameterObject).get(FlexConsts.SQL_ARGS); + if (sqlArgs != null && sqlArgs.length > 0) { + int index = 1; + for (Object value : sqlArgs) { + //在 Oracle、SqlServer 中 TIMESTAMP、DATE 类型的数据是支持 java.util.Date 给值的 + if (value instanceof java.util.Date) { + setDateParameter(ps, (Date) value, index++); + } else if (value instanceof byte[]) { + ps.setBytes(index++, (byte[]) value); + } else { + /** 在 MySql,Oracle 等驱动中,通过 PreparedStatement.setObject 后,驱动会自动根据 value 内容进行转换 + * 源码可参考: {{@link com.mysql.jdbc.PreparedStatement#setObject(int, Object)} + **/ + ps.setObject(index++, value); + } + } + } else { + super.setParameters(ps); + } + } + + /** + * Oracle、SqlServer 需要主动设置下 date 类型 + * MySql 通过 setObject 后会自动转换,具体查看 MySql 驱动源码 + * + * @param ps PreparedStatement + * @param value date value + * @param index set to index + * @throws SQLException + */ + private void setDateParameter(PreparedStatement ps, Date value, int index) throws SQLException { + if (value instanceof java.sql.Date) { + ps.setDate(index, (java.sql.Date) value); + } else if (value instanceof java.sql.Timestamp) { + ps.setTimestamp(index, (java.sql.Timestamp) value); + } else { + ps.setTimestamp(index, new java.sql.Timestamp(value.getTime())); + } + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/paginate/Page.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/paginate/Page.java new file mode 100644 index 00000000..83405586 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/paginate/Page.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.paginate; + +import java.io.Serializable; +import java.util.List; + +public class Page implements Serializable { + + private static final long serialVersionUID = 1L; + private static final int INIT_VALUE = -1; + + private List list; // list result of this page + private int pageNumber = INIT_VALUE; // page number + private int pageSize = INIT_VALUE; // result amount of this page + private long totalPage = INIT_VALUE; // total page + private long totalRow = INIT_VALUE; // total row + + public static Page of(int pageNumber, int pageSize) { + return new Page(pageNumber, pageSize); + } + + public static Page of(int pageNumber, int pageSize, long totalRow) { + return new Page(pageNumber, pageSize, totalRow); + } + + public Page() { + + } + + public Page(int pageNumber, int pageSize) { + this.pageNumber = pageNumber; + this.pageSize = pageSize; + } + + public Page(int pageNumber, int pageSize, long totalRow) { + this.pageNumber = pageNumber; + this.pageSize = pageSize; + this.totalRow = totalRow; + this.totalPage = totalRow % pageSize == 0 ? (totalRow / pageSize) : (totalRow / pageSize + 1); + } + + public Page(List list, int pageNumber, int pageSize, long totalRow) { + this.list = list; + this.pageNumber = pageNumber; + this.pageSize = pageSize; + this.totalRow = totalRow; + this.totalPage = totalRow % pageSize == 0 ? (totalRow / pageSize) : (totalRow / pageSize + 1); + } + + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public int getPageNumber() { + return pageNumber; + } + + public void setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + } + + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public long getTotalPage() { + return totalPage; + } + + public void setTotalPage(long totalPage) { + this.totalPage = totalPage; + } + + public long getTotalRow() { + return totalRow; + } + + public void setTotalRow(long totalRow) { + this.totalRow = totalRow; + this.totalPage = totalRow % pageSize == 0 ? (totalRow / pageSize) : (totalRow / pageSize + 1); + } + + public boolean isFirstPage() { + return pageNumber == 1; + } + + public boolean isLastPage() { + return pageNumber >= totalPage; + } + + @Override + public String toString() { + return "Page{" + + "pageNumber=" + pageNumber + + ", pageSize=" + pageSize + + ", totalPage=" + totalPage + + ", totalRow=" + totalRow + + ", list=" + list + + '}'; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/provider/EntitySqlProvider.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/provider/EntitySqlProvider.java new file mode 100644 index 00000000..d9b92370 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/provider/EntitySqlProvider.java @@ -0,0 +1,292 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.provider; + +import com.mybatisflex.core.dialect.DialectFactory; +import com.mybatisflex.core.exception.FlexExceptions; +import com.mybatisflex.core.querywrapper.QueryWrapper; +import com.mybatisflex.core.querywrapper.CPI; +import com.mybatisflex.core.table.TableInfo; +import com.mybatisflex.core.util.ArrayUtil; +import com.mybatisflex.core.util.CollectionUtil; +import org.apache.ibatis.builder.annotation.ProviderContext; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class EntitySqlProvider { + + + /** + * 不让实例化,使用静态方法的模式,效率更高,非静态方法每次都会实例化当前类 + * 参考源码: {{@link org.apache.ibatis.builder.annotation.ProviderSqlSource#getBoundSql(Object)} + */ + private EntitySqlProvider() { + } + + /** + * insert 的 sql 构建 + * + * @param params + * @param context + * @return sql + * @see com.mybatisflex.core.BaseMapper#insert(Object) + */ + public static String insert(Map params, ProviderContext context) { + Object entity = ProviderUtil.getEntity(params); + if (entity == null) { + throw FlexExceptions.wrap("entity can not be null."); + } + + TableInfo tableInfo = ProviderUtil.getTableInfo(context); + + Object[] values = tableInfo.obtainInsertValues(entity); + ProviderUtil.setSqlArgs(params, values); + + return DialectFactory.getDialect().forInsertEntity(tableInfo, entity); + } + + + /** + * insertBatchWithFirstEntityColumns 的 sql 构建 + * + * @param params + * @param context + * @return sql + * @see com.mybatisflex.core.BaseMapper#insertBatchWithFirstEntityColumns(List) + */ + public static String insertBatchWithFirstEntityColumns(Map params, ProviderContext context) { + List entities = ProviderUtil.getEntities(params); + if (CollectionUtil.isEmpty(entities)) { + throw FlexExceptions.wrap("entities can not be null or empty."); + } + + TableInfo tableInfo = ProviderUtil.getTableInfo(context); + Object[] values = new Object[0]; + for (Object entity : entities) { + values = ArrayUtil.concat(values, tableInfo.obtainInsertValues(entity)); + } + + ProviderUtil.setSqlArgs(params, values); + + return DialectFactory.getDialect().forInsertBatchWithFirstEntityColumns(tableInfo, entities); + } + + + /** + * deleteById 的 sql 构建 + * + * @param params + * @param context + * @return sql + * @see com.mybatisflex.core.BaseMapper#deleteById(Serializable) + */ + public static String deleteById(Map params, ProviderContext context) { + Object[] primaryValues = ProviderUtil.getPrimaryValues(params); + if (ArrayUtil.isEmpty(primaryValues)) { + throw FlexExceptions.wrap("primaryValues can not be null or empty."); + } + + TableInfo tableInfo = ProviderUtil.getTableInfo(context); + ProviderUtil.setSqlArgs(params, primaryValues); + + return DialectFactory.getDialect().forDeleteEntityById(tableInfo); + } + + + /** + * deleteBatchByIds 的 sql 构建 + * + * @param params + * @param context + * @return sql + * @see com.mybatisflex.core.BaseMapper#deleteBatchByIds(Collection) + */ + public static String deleteBatchByIds(Map params, ProviderContext context) { + Object[] primaryValues = ProviderUtil.getPrimaryValues(params); + if (ArrayUtil.isEmpty(primaryValues)) { + throw FlexExceptions.wrap("primaryValues can not be null or empty."); + } + + TableInfo tableInfo = ProviderUtil.getTableInfo(context); + ProviderUtil.setSqlArgs(params, primaryValues); + + return DialectFactory.getDialect().forDeleteEntityBatchById(tableInfo, primaryValues); + } + + + /** + * deleteByQuery 的 sql 构建 + * + * @param params + * @param context + * @return sql + * @see com.mybatisflex.core.BaseMapper#deleteByQuery(QueryWrapper) + */ + public static String deleteByQuery(Map params, ProviderContext context) { + QueryWrapper queryWrapper = ProviderUtil.getQueryWrapper(params); + if (queryWrapper == null) { + throw FlexExceptions.wrap("queryWrapper can not be null or empty."); + } + + ProviderUtil.setSqlArgs(params, CPI.getValueArray(queryWrapper)); + + TableInfo tableInfo = ProviderUtil.getTableInfo(context); + queryWrapper.from(tableInfo.getTableName()); + return DialectFactory.getDialect().forDeleteByQuery(queryWrapper); + } + + + /** + * update 的 sql 构建 + * + * @param params + * @param context + * @return sql + * @see com.mybatisflex.core.BaseMapper#update(Object, boolean) + */ + public static String update(Map params, ProviderContext context) { + Object entity = ProviderUtil.getEntity(params); + if (entity == null) { + throw FlexExceptions.wrap("entity can not be null"); + } + + boolean ignoreNulls = ProviderUtil.isIgnoreNulls(params); + + TableInfo tableInfo = ProviderUtil.getTableInfo(context); + Object[] updateValues = tableInfo.obtainUpdateValues(entity, ignoreNulls, false); + Object[] primaryValues = tableInfo.obtainPrimaryValues(entity); + ProviderUtil.setSqlArgs(params, ArrayUtil.concat(updateValues, primaryValues)); + + return DialectFactory.getDialect().forUpdateEntity(tableInfo, entity, ignoreNulls); + } + + + /** + * updateByQuery 的 sql 构建 + * + * @param params + * @param context + * @return sql + * @see com.mybatisflex.core.BaseMapper#updateByQuery(Object, QueryWrapper) + */ + public static String updateByQuery(Map params, ProviderContext context) { + Object entity = ProviderUtil.getEntity(params); + if (entity == null) { + throw FlexExceptions.wrap("entity can not be null"); + } + boolean ignoreNulls = ProviderUtil.isIgnoreNulls(params); + QueryWrapper queryWrapper = ProviderUtil.getQueryWrapper(params); + + + TableInfo tableInfo = ProviderUtil.getTableInfo(context); + Object[] values = tableInfo.obtainUpdateValues(entity, ignoreNulls, true); + + ProviderUtil.setSqlArgs(params, ArrayUtil.concat(values, CPI.getValueArray(queryWrapper))); + + return DialectFactory.getDialect().forUpdateEntityByQuery(tableInfo, entity, ignoreNulls, queryWrapper); + } + + + /** + * selectOneById 的 sql 构建 + * + * @param params + * @param context + * @return sql + * @see com.mybatisflex.core.BaseMapper#selectOneById(Serializable) + */ + public static String selectOneById(Map params, ProviderContext context) { + Object[] primaryValues = ProviderUtil.getPrimaryValues(params); + if (ArrayUtil.isEmpty(primaryValues)) { + throw FlexExceptions.wrap("primaryValues can not be null or empty."); + } + + ProviderUtil.setSqlArgs(params, primaryValues); + + TableInfo tableInfo = ProviderUtil.getTableInfo(context); + return DialectFactory.getDialect().forSelectOneEntityById(tableInfo); + } + + + /** + * selectListByIds 的 sql 构建 + * + * @param params + * @param context + * @return sql + * @see com.mybatisflex.core.BaseMapper#selectListByIds(Collection) + */ + public static String selectListByIds(Map params, ProviderContext context) { + Object[] primaryValues = ProviderUtil.getPrimaryValues(params); + if (ArrayUtil.isEmpty(primaryValues)) { + throw FlexExceptions.wrap("primaryValues can not be null or empty."); + } + + ProviderUtil.setSqlArgs(params, primaryValues); + + TableInfo tableInfo = ProviderUtil.getTableInfo(context); + return DialectFactory.getDialect().forSelectEntityListByIds(tableInfo, primaryValues); + } + + + /** + * selectListByQuery 的 sql 构建 + * + * @param params + * @param context + * @return sql + * @see com.mybatisflex.core.BaseMapper#selectListByQuery(QueryWrapper) + */ + public static String selectListByQuery(Map params, ProviderContext context) { + QueryWrapper queryWrapper = ProviderUtil.getQueryWrapper(params); + if (queryWrapper == null) { + throw FlexExceptions.wrap("queryWrapper can not be null."); + } + Object[] values = CPI.getValueArray(queryWrapper); + ProviderUtil.setSqlArgs(params, values); + + TableInfo tableInfo = ProviderUtil.getTableInfo(context); + queryWrapper.from(tableInfo.getTableName()); + return DialectFactory.getDialect().forSelectListByQuery(queryWrapper); + } + + /** + * selectCountByQuery 的 sql 构建 + * + * @param params + * @param context + * @return sql + * @see com.mybatisflex.core.BaseMapper#selectCountByQuery(QueryWrapper) + */ + public static String selectCountByQuery(Map params, ProviderContext context) { + QueryWrapper queryWrapper = ProviderUtil.getQueryWrapper(params); + if (queryWrapper == null) { + throw FlexExceptions.wrap("queryWrapper can not be null."); + } + + Object[] values = CPI.getValueArray(queryWrapper); + ProviderUtil.setSqlArgs(params, values); + + TableInfo tableInfo = ProviderUtil.getTableInfo(context); + queryWrapper.from(tableInfo.getTableName()); + return DialectFactory.getDialect().forSelectCountByQuery(queryWrapper); + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/provider/ProviderUtil.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/provider/ProviderUtil.java new file mode 100644 index 00000000..a1046751 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/provider/ProviderUtil.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.provider; + +import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.exception.FlexExceptions; +import com.mybatisflex.core.querywrapper.QueryWrapper; +import com.mybatisflex.core.row.Row; +import com.mybatisflex.core.table.TableInfo; +import com.mybatisflex.core.table.TableInfos; +import com.mybatisflex.core.util.StringUtil; +import org.apache.ibatis.builder.annotation.ProviderContext; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +class ProviderUtil { + + private static final Object[] NULL_ARGS = new Object[0]; + + public static String getSqlString(Map params) { + return (String) params.get(FlexConsts.SQL); + } + + public static void setSqlArgs(Map params, Object[] args) { + params.put(FlexConsts.SQL_ARGS, args); + } + + public static String getTableName(Map params) { + return params.get(FlexConsts.TABLE_NAME).toString().trim(); + } + + public static String[] getPrimaryKeys(Map params) { + String primaryKey = (String) params.get(FlexConsts.PRIMARY_KEY); + if (StringUtil.isBlank(primaryKey)) { + throw FlexExceptions.wrap("primaryKey can not be null or blank."); + } + String[] primaryKeys = primaryKey.split(","); + for (int i = 0; i < primaryKeys.length; i++) { + primaryKeys[i] = primaryKeys[i].trim(); + } + return primaryKeys; + } + + public static Object[] getPrimaryValues(Map params) { + Object primaryValue = params.get(FlexConsts.PRIMARY_VALUE); + if (primaryValue == null) { + return NULL_ARGS; + } + if (primaryValue.getClass().isArray()) { + return (Object[]) primaryValue; + } else if (primaryValue instanceof Collection) { + return ((Collection) primaryValue).toArray(); + } else { + return new Object[]{primaryValue}; + } + } + + public static QueryWrapper getQueryWrapper(Map params) { + return (QueryWrapper) params.get(FlexConsts.QUERY); + } + + public static Row getRow(Map params) { + return (Row) params.get(FlexConsts.ROW); + } + + public static List getRows(Map params) { + return (List) params.get(FlexConsts.ROWS); + } + + public static TableInfo getTableInfo(ProviderContext context){ + return TableInfos.ofMapperClass(context.getMapperType()); + } + + public static Object getEntity(Map params) { + return params.get(FlexConsts.ENTITY); + } + + + public static List getEntities(Map params) { + return (List) params.get(FlexConsts.ENTITIES); + } + + public static boolean isIgnoreNulls(Map params){ + return params.containsKey(FlexConsts.IGNORE_NULLS) && (boolean) params.get(FlexConsts.IGNORE_NULLS); + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/provider/RowSqlProvider.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/provider/RowSqlProvider.java new file mode 100644 index 00000000..a307f69c --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/provider/RowSqlProvider.java @@ -0,0 +1,272 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.provider; + +import com.mybatisflex.core.dialect.DialectFactory; +import com.mybatisflex.core.exception.FlexExceptions; +import com.mybatisflex.core.querywrapper.CPI; +import com.mybatisflex.core.querywrapper.QueryWrapper; +import com.mybatisflex.core.row.Row; +import com.mybatisflex.core.row.RowMapper; +import com.mybatisflex.core.util.ArrayUtil; +import com.mybatisflex.core.util.CollectionUtil; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class RowSqlProvider { + + + public static final String METHOD_RAW_SQL = "providerRawSql"; + + /** + * 不让实例化,使用静态方法的模式,效率更高,非静态方法每次都会实例化当前类 + * 参考源码: {{@link org.apache.ibatis.builder.annotation.ProviderSqlSource#getBoundSql(Object)} + */ + private RowSqlProvider() { + } + + /** + * 执行原生 sql 的方法 + * + * @param params + * @return sql + * @see RowMapper#insertBySql(String, Object...) + * @see RowMapper#deleteBySql(String, Object...) + * @see RowMapper#updateBySql(String, Object...) + */ + public static String providerRawSql(Map params) { + return ProviderUtil.getSqlString(params); + } + + /** + * insertRow 的 sql 构建 + * + * @param params + * @return sql + * @see RowMapper#insertRow(String, Row) + */ + public static String insertRow(Map params) { + String tableName = ProviderUtil.getTableName(params); + Row row = ProviderUtil.getRow(params); + ProviderUtil.setSqlArgs(params, row.obtainModifyValues()); + return DialectFactory.getDialect().forInsertRow(tableName, row); + } + + /** + * insertBatch 的 sql 构建 + * + * @param params + * @return sql + * @see RowMapper#insertBatchWithFirstRowColumns(String, List) + */ + public static String insertBatchWithFirstRowColumns(Map params) { + String tableName = ProviderUtil.getTableName(params); + List rows = ProviderUtil.getRows(params); + if (rows == null || rows.isEmpty()) { + throw FlexExceptions.wrap("rows can not be null or empty."); + } + + //让所有 row 的列顺序和值的数量与第条数据保持一致 + Set modifyAttrs = rows.get(0).obtainModifyAttrs(); + rows.forEach(row -> row.keep(modifyAttrs)); + + + Object[] values = new Object[]{}; + for (Row row : rows) { + values = ArrayUtil.concat(values, row.obtainModifyValues()); + } + ProviderUtil.setSqlArgs(params, values); + + + //sql: INSERT INTO `tb_table`(`name`, `sex`) VALUES (?, ?),(?, ?),(?, ?) + return DialectFactory.getDialect().forInsertBatchWithFirstRowColumns(tableName, rows); + } + + /** + * deleteById 的 sql 构建 + * + * @param params + * @return sql + * @see RowMapper#deleteById(String, String, Object) + */ + public static String deleteById(Map params) { + String tableName = ProviderUtil.getTableName(params); + String[] primaryKeys = ProviderUtil.getPrimaryKeys(params); + Object[] primaryValues = ProviderUtil.getPrimaryValues(params); + + if (primaryValues.length == 0) { + throw FlexExceptions.wrap("primaryValue can not be null"); + } else { + ProviderUtil.setSqlArgs(params, primaryValues); + } + + return DialectFactory.getDialect().forDeleteById(tableName, primaryKeys); + } + + /** + * deleteBatchByIds 的 sql 构建 + * + * @param params + * @return sql + * @see RowMapper#deleteBatchByIds(String, String, Collection) + */ + public static String deleteBatchByIds(Map params) { + String tableName = ProviderUtil.getTableName(params); + String[] primaryKeys = ProviderUtil.getPrimaryKeys(params); + Object[] primaryValues = ProviderUtil.getPrimaryValues(params); + + ProviderUtil.setSqlArgs(params, primaryValues); + return DialectFactory.getDialect().forDeleteBatchByIds(tableName, primaryKeys, primaryValues); + } + + + /** + * deleteByQuery 的 sql 构建 + * + * @param params + * @return sql + * @see RowMapper#deleteByQuery(String, QueryWrapper) + */ + public static String deleteByQuery(Map params) { + String tableName = ProviderUtil.getTableName(params); + QueryWrapper queryWrapper = ProviderUtil.getQueryWrapper(params); + + Object[] valueArray = CPI.getValueArray(queryWrapper); + ProviderUtil.setSqlArgs(params, valueArray); + + queryWrapper.from(tableName); + return DialectFactory.getDialect().forDeleteByQuery(queryWrapper); + } + + /** + * updateById 的 sql 构建 + * + * @param params + * @return sql + * @see RowMapper#updateById(String, Row) + */ + public static String updateById(Map params) { + String tableName = ProviderUtil.getTableName(params); + Row row = ProviderUtil.getRow(params); + ProviderUtil.setSqlArgs(params, row.obtainModifyValuesAndPrimaryValues()); + return DialectFactory.getDialect().forUpdateById(tableName, row); + } + + + /** + * updateByQuery 的 sql 构建 + * + * @param params + * @return sql + * @see RowMapper#updateByQuery(String, Row, QueryWrapper) + */ + public static String updateByQuery(Map params) { + String tableName = ProviderUtil.getTableName(params); + Row data = ProviderUtil.getRow(params); + QueryWrapper queryWrapper = ProviderUtil.getQueryWrapper(params); + + + Object[] modifyValues = data.obtainModifyValues(); + Object[] valueArray = CPI.getValueArray(queryWrapper); + + ProviderUtil.setSqlArgs(params, ArrayUtil.concat(modifyValues, valueArray)); + + return DialectFactory.getDialect().forUpdateByQuery(tableName, data, queryWrapper); + } + + + /** + * updateBatchById 的 sql 构建 + * mysql 等链接配置需要开启 allowMultiQueries=true + * + * @param params + * @return sql + * @see RowMapper#updateBatchById(String, List) + */ + public static String updateBatchById(Map params) { + String tableName = ProviderUtil.getTableName(params); + List rows = ProviderUtil.getRows(params); + if (CollectionUtil.isEmpty(rows)) { + throw FlexExceptions.wrap("rows can not be null or empty."); + } + + Object[] values = new Object[0]; + for (Row row : rows) { + values = ArrayUtil.concat(values, row.obtainModifyValuesAndPrimaryValues()); + } + ProviderUtil.setSqlArgs(params, values); + return DialectFactory.getDialect().forUpdateBatchById(tableName, rows); + } + + + /** + * selectOneById 的 sql 构建 + * + * @param params + * @return sql + * @see RowMapper#selectOneById(String, String, Object) + */ + public static String selectOneById(Map params) { + String tableName = ProviderUtil.getTableName(params); + String[] primaryKeys = ProviderUtil.getPrimaryKeys(params); + Object[] primaryValues = ProviderUtil.getPrimaryValues(params); + + ProviderUtil.setSqlArgs(params, primaryValues); + + return DialectFactory.getDialect().forSelectOneById(tableName, primaryKeys, primaryValues); + } + + + /** + * selectListByQuery 的 sql 构建 + * + * @param params + * @return sql + * @see RowMapper#selectListByQuery(String, QueryWrapper) + */ + public static String selectListByQuery(Map params) { + String tableName = ProviderUtil.getTableName(params); + QueryWrapper queryWrapper = ProviderUtil.getQueryWrapper(params); + + Object[] valueArray = CPI.getValueArray(queryWrapper); + ProviderUtil.setSqlArgs(params, valueArray); + + queryWrapper.from(tableName); + return DialectFactory.getDialect().forSelectListByQuery(queryWrapper); + } + + /** + * selectCountByQuery 的 sql 构建 + * + * @param params + * @return sql + * @see RowMapper#selectCountByQuery(String, QueryWrapper) + */ + public static String selectCountByQuery(Map params) { + String tableName = ProviderUtil.getTableName(params); + QueryWrapper queryWrapper = ProviderUtil.getQueryWrapper(params); + + Object[] valueArray = CPI.getValueArray(queryWrapper); + ProviderUtil.setSqlArgs(params, valueArray); + + return DialectFactory.getDialect().forSelectCountByQuery(queryWrapper); + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/BaseQueryWrapper.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/BaseQueryWrapper.java new file mode 100644 index 00000000..b647a1c1 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/BaseQueryWrapper.java @@ -0,0 +1,204 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +public class BaseQueryWrapper implements Serializable { + + protected List queryTables; + protected String datasource ; + + protected List selectColumns; + protected List joins; + protected List joinTables; + protected QueryCondition whereQueryCondition; + protected List groupByColumns; + protected QueryCondition havingQueryCondition; + protected List orderBys; + + protected Integer limitOffset; + protected Integer limitRows; + + + protected T addSelectColumn(QueryColumn queryColumn){ + if (selectColumns == null){ + selectColumns = new LinkedList<>(); + } + + selectColumns.add(queryColumn); + return (T) this; + } + + + protected T AddJoin(Join join){ + if (joins == null){ + joins = new LinkedList<>(); + } + joins.add(join); + return (T) this; + } + + + protected T setWhereQueryCondition(QueryCondition queryCondition){ + if (whereQueryCondition != null){ + queryCondition.connect(whereQueryCondition,SqlConnector.AND); + } + + whereQueryCondition = queryCondition; + return (T) this; + } + + + protected T addWhereQueryCondition(QueryCondition queryCondition,SqlConnector connector){ + if (queryCondition != null) { + if (whereQueryCondition == null) { + whereQueryCondition = queryCondition; + } else { + whereQueryCondition.connect(queryCondition, connector); + } + } + return (T) this; + } + + + + protected T addGroupByColumns(QueryColumn queryColumn){ + if (groupByColumns == null){ + groupByColumns = new LinkedList<>(); + } + + groupByColumns.add(queryColumn); + return (T) this; + } + + + + protected T addHavingQueryCondition(QueryCondition queryCondition,SqlConnector connector){ + if (havingQueryCondition == null){ + havingQueryCondition = queryCondition; + }else { + havingQueryCondition.connect(queryCondition,connector); + } + return (T) this; + } + + + + protected T addOrderBy(QueryOrderBy queryOrderBy){ + if (orderBys == null){ + orderBys = new LinkedList<>(); + } + orderBys.add(queryOrderBy); + return (T) this; + } + + + protected void addJoinTable(QueryTable queryTable){ + if (joinTables == null){ + joinTables = new ArrayList<>(); + } + joinTables.add(queryTable); + } + + + protected List getQueryTables() { + return queryTables; + } + + protected void setQueryTables(List queryTables) { + this.queryTables = queryTables; + } + + protected String getDatasource() { + return datasource; + } + + protected void setDatasource(String datasource) { + this.datasource = datasource; + } + + protected List getSelectColumns() { + return selectColumns; + } + + protected void setSelectColumns(List selectColumns) { + this.selectColumns = selectColumns; + } + + protected List getJoins() { + return joins; + } + + protected void setJoins(List joins) { + this.joins = joins; + } + + protected List getJoinTables() { + return joinTables; + } + + protected void setJoinTables(List joinTables) { + this.joinTables = joinTables; + } + + protected QueryCondition getWhereQueryCondition() { + return whereQueryCondition; + } + + protected List getGroupByColumns() { + return groupByColumns; + } + + protected void setGroupByColumns(List groupByColumns) { + this.groupByColumns = groupByColumns; + } + + protected QueryCondition getHavingQueryCondition() { + return havingQueryCondition; + } + + protected void setHavingQueryCondition(QueryCondition havingQueryCondition) { + this.havingQueryCondition = havingQueryCondition; + } + + protected List getOrderBys() { + return orderBys; + } + + protected void setOrderBys(List orderBys) { + this.orderBys = orderBys; + } + + protected Integer getLimitOffset() { + return limitOffset; + } + + protected void setLimitOffset(Integer limitOffset) { + this.limitOffset = limitOffset; + } + + protected Integer getLimitRows() { + return limitRows; + } + + protected void setLimitRows(Integer limitRows) { + this.limitRows = limitRows; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/Brackets.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/Brackets.java new file mode 100644 index 00000000..81073e5e --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/Brackets.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + +import com.mybatisflex.core.dialect.IDialect; + +import java.util.List; + +/** + * 括号 + */ +public class Brackets extends QueryCondition { + + private QueryCondition childCondition; + + + public Brackets(QueryCondition childCondition) { + this.childCondition = childCondition; + } + + + @Override + public QueryCondition and(QueryCondition nextCondition) { + connectToChild(nextCondition, SqlConnector.AND); + return this; + } + + @Override + public QueryCondition or(QueryCondition nextCondition) { + connectToChild(nextCondition, SqlConnector.OR); + return this; + } + + protected void connectToChild(QueryCondition nextCondition, SqlConnector connector) { + childCondition.connect(nextCondition, connector); + } + + @Override + public Object getValue() { + return checkEffective() ? WrapperUtil.getValues(childCondition) : null; + } + + @Override + public String toSql(List queryTables, IDialect dialect) { + StringBuilder sql = new StringBuilder(); + if (checkEffective()) { + String childSql = childCondition.toSql(queryTables, dialect); + QueryCondition effectiveBefore = getEffectiveBefore(); + if (effectiveBefore != null) { + childSql = effectiveBefore.connector + "(" + childSql + ")"; + } + sql.append(childSql); + } + + if (this.next != null) { + return sql + next.toSql(queryTables, dialect); + } + + return sql.toString(); + } + + + @Override + public String toString() { + return "Brackets{" + + "childCondition=" + childCondition + + '}'; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/CPI.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/CPI.java new file mode 100644 index 00000000..153aa704 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/CPI.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + +import com.mybatisflex.core.dialect.IDialect; + +import java.util.List; + +/** + * Cross Package Invoke + * 夸包调用工具类,这么设计的原因,是需要保证 QueryWrapper 方法对于用户的纯净性 + * 而 framework 又可以通过 CPI 来调用 QueryWrapper 的其他方法 + */ + +public class CPI { + + + public static Object[] getValueArray(QueryWrapper queryWrapper) { + return queryWrapper.getValueArray(); + } + + + public static List getQueryTables(QueryWrapper queryWrapper) { + return queryWrapper.getQueryTables(); + } + + public static void setQueryTable(QueryWrapper queryWrapper, List queryTables) { + queryWrapper.setQueryTables(queryTables); + } + + public static String getDatasource(QueryWrapper queryWrapper) { + return queryWrapper.getDatasource(); + } + + public static void setDatasource(QueryWrapper queryWrapper, String datasource) { + queryWrapper.setDatasource(datasource); + } + + public static List getSelectColumns(QueryWrapper queryWrapper) { + return queryWrapper.getSelectColumns(); + } + + public static void setSelectColumns(QueryWrapper queryWrapper, List selectColumns) { + queryWrapper.setSelectColumns(selectColumns); + } + + public static List getJoins(QueryWrapper queryWrapper) { + return queryWrapper.getJoins(); + } + + public static void setJoins(QueryWrapper queryWrapper, List joins) { + queryWrapper.setJoins(joins); + } + + + public static List getJoinTables(QueryWrapper queryWrapper) { + return queryWrapper.getJoinTables(); + } + + public static void setJoinTables(QueryWrapper queryWrapper,List joinTables) { + queryWrapper.setJoinTables(joinTables); + } + + + public static QueryCondition getWhereQueryCondition(QueryWrapper queryWrapper) { + return queryWrapper.getWhereQueryCondition(); + } + + public static List getGroupByColumns(QueryWrapper queryWrapper) { + return queryWrapper.getGroupByColumns(); + } + + public static void setGroupByColumns(QueryWrapper queryWrapper, List groupByColumns) { + queryWrapper.setGroupByColumns(groupByColumns); + } + + public static QueryCondition getHavingQueryCondition(QueryWrapper queryWrapper) { + return queryWrapper.getHavingQueryCondition(); + } + + public static void setHavingQueryCondition(QueryWrapper queryWrapper, QueryCondition havingQueryCondition) { + queryWrapper.setHavingQueryCondition(havingQueryCondition); + } + + public static List getOrderBys(QueryWrapper queryWrapper) { + return queryWrapper.getOrderBys(); + } + + public static void setOrderBys(QueryWrapper queryWrapper, List orderBys) { + queryWrapper.setOrderBys(orderBys); + } + + public static Integer getLimitOffset(QueryWrapper queryWrapper) { + return queryWrapper.getLimitOffset(); + } + + public static void setLimitOffset(QueryWrapper queryWrapper, Integer limitOffset) { + queryWrapper.setLimitOffset(limitOffset); + } + + public static Integer getLimitRows(QueryWrapper queryWrapper) { + return queryWrapper.getLimitRows(); + } + + public static void setLimitRows(QueryWrapper queryWrapper, Integer limitRows) { + queryWrapper.setLimitRows(limitRows); + } + + + public static String toConditionSql(QueryColumn queryColumn,List queryTables, IDialect dialect) { + return queryColumn.toConditionSql(queryTables,dialect); + } + + public static String toSelectSql(QueryColumn queryColumn,List queryTables, IDialect dialect) { + return queryColumn.toSelectSql(queryTables,dialect); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/DistinctQueryColumn.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/DistinctQueryColumn.java new file mode 100644 index 00000000..d294d33c --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/DistinctQueryColumn.java @@ -0,0 +1,25 @@ +package com.mybatisflex.core.querywrapper; + +import com.mybatisflex.core.dialect.IDialect; +import com.mybatisflex.core.util.CollectionUtil; +import com.mybatisflex.core.util.StringUtil; + +import java.util.List; + +public class DistinctQueryColumn extends QueryColumn { + + private List queryColumns; + + public DistinctQueryColumn(QueryColumn... queryColumns) { + this.queryColumns = CollectionUtil.newArrayList(queryColumns); + } + + @Override + public String toSelectSql(List queryTables, IDialect dialect) { + if (CollectionUtil.isEmpty(queryTables)) { + return ""; + } + return " DISTINCT " + StringUtil.join(",", queryColumns, queryColumn -> + queryColumn.toSelectSql(queryTables, dialect)); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/FunctionQueryColumn.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/FunctionQueryColumn.java new file mode 100644 index 00000000..e5fd22b1 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/FunctionQueryColumn.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + +import com.mybatisflex.core.dialect.IDialect; +import com.mybatisflex.core.util.StringUtil; + +import java.util.List; + +/** + * 数据库 聚合函数,例如 count(id) ,max(account.age) 等等 + */ +public class FunctionQueryColumn extends QueryColumn { + + protected String fnName; + protected QueryColumn column; + + public FunctionQueryColumn(String fnName, String column) { + this.fnName = fnName; + this.column = new QueryColumn(column); + } + + public FunctionQueryColumn(String fnName, QueryColumn column) { + this.fnName = fnName; + this.column = column; + } + + public String getFnName() { + return fnName; + } + + public void setFnName(String fnName) { + this.fnName = fnName; + } + + public QueryColumn getColumn() { + return column; + } + + public void setColumn(QueryColumn column) { + this.column = column; + } + + @Override + public String toSelectSql(List queryTables, IDialect dialect) { + String sql = column.toSelectSql(queryTables, dialect); + return StringUtil.isBlank(sql) ? "" : fnName + "(" + sql + ")" + WrapperUtil.buildAsAlias(alias); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/Join.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/Join.java new file mode 100644 index 00000000..877dc843 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/Join.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + +import com.mybatisflex.core.dialect.IDialect; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/1/14 + */ +public class Join implements Serializable { + + private static final long serialVersionUID = 1L; + + static final String TYPE_LEFT = " LEFT JOIN "; + static final String TYPE_RIGHT = " RIGHT JOIN "; + static final String TYPE_INNER = " INNER JOIN "; + static final String TYPE_FULL = " FULL JOIN "; + static final String TYPE_CROSS = " CROSS JOIN "; + + + private String type; + private QueryTable queryTable; + private QueryCondition on; + private boolean effective; + + public Join(String type, String table, boolean when) { + this.type = type; + this.queryTable = new QueryTable(table); + this.effective = when; + } + + public Join(String type, QueryWrapper queryWrapper, boolean when) { + this.type = type; + this.queryTable = new SelectQueryTable(queryWrapper); + this.effective = when; + } + + + + QueryTable getQueryTable() { + return queryTable; + } + + + public void on(QueryCondition condition) { + this.on = condition; + } + + + public boolean checkEffective() { + return effective; + } + + public void when(boolean when) { + this.effective = when; + } + + public void when(Supplier fn) { + this.effective = fn.get(); + } + + public String toSql(List queryTables, IDialect dialect) { + //left join, right join, inner join ... + StringBuilder sql = new StringBuilder(type); + sql.append(queryTable.toSql(dialect)); + + //left join xxx as xxx2 on xxx2.id = xxx3.other + List newQueryTables = new ArrayList<>(queryTables); + newQueryTables.add(queryTable); + sql.append(" ON ").append(on.toSql(newQueryTables, dialect)); + return sql.toString(); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/Joiner.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/Joiner.java new file mode 100644 index 00000000..3b6d30d9 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/Joiner.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/1/14 + */ +public class Joiner { + + private M queryWrapper; + private Join join; + + public Joiner(M queryWrapper, Join join) { + this.queryWrapper = queryWrapper; + this.join = join; + } + + public Joiner as(String alias) { + join.getQueryTable().as(alias); + return this; + } + + public M on(String on) { + join.on(new StringQueryCondition(on)); + return queryWrapper; + } + + public M on(QueryCondition on) { + join.on(on); + return queryWrapper; + } +} + diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/OperatorQueryCondition.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/OperatorQueryCondition.java new file mode 100644 index 00000000..710ffe72 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/OperatorQueryCondition.java @@ -0,0 +1,49 @@ +package com.mybatisflex.core.querywrapper; + +import com.mybatisflex.core.dialect.IDialect; +import com.mybatisflex.core.util.StringUtil; + +import java.util.List; + +/** + * 操作类型的操作 + * 示例1:and not ( id > 100 and name like %%) + */ +public class OperatorQueryCondition extends QueryCondition { + + private String operator; + private QueryCondition child; + + public OperatorQueryCondition(String operator, QueryCondition child) { + this.operator = operator; + this.child = child; + } + + @Override + public String toSql(List queryTables, IDialect dialect) { + StringBuilder sql = new StringBuilder(); + + //检测是否生效 + if (checkEffective()) { + String childSql = child.toSql(queryTables, dialect); + if (StringUtil.isNotBlank(childSql)) { + QueryCondition effectiveBefore = getEffectiveBefore(); + if (effectiveBefore != null) { + sql.append(effectiveBefore.connector); + } + sql.append(operator).append("(").append(childSql).append(")"); + } + } + + if (this.next != null) { + return sql + next.toSql(queryTables, dialect); + } + + return sql.toString(); + } + + @Override + public Object getValue() { + return WrapperUtil.getValues(child); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/OperatorSelectCondition.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/OperatorSelectCondition.java new file mode 100644 index 00000000..a26a5fc9 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/OperatorSelectCondition.java @@ -0,0 +1,51 @@ +package com.mybatisflex.core.querywrapper; + +import com.mybatisflex.core.dialect.IDialect; +import com.mybatisflex.core.util.StringUtil; + +import java.util.List; + +/** + * 操作类型的操作 + * 示例1:and exist (select 1 from ... where ....) + * 示例2:and not exist (select ... from ... where ....) + */ +public class OperatorSelectCondition extends QueryCondition { + //操作符,例如 exist, not exist + private String operator; + private QueryWrapper queryWrapper; + + public OperatorSelectCondition(String operator, QueryWrapper queryWrapper) { + this.operator = operator; + this.queryWrapper = queryWrapper; + } + + @Override + public String toSql(List queryTables, IDialect dialect) { + StringBuilder sql = new StringBuilder(); + + //检测是否生效 + if (checkEffective()) { + String childSql = dialect.buildSelectSql(queryWrapper); + if (StringUtil.isNotBlank(childSql)) { + + QueryCondition effectiveBefore = getEffectiveBefore(); + if (effectiveBefore != null) { + sql.append(effectiveBefore.connector); + } + sql.append(operator).append("(").append(childSql).append(")"); + } + } + + if (this.next != null) { + return sql + next.toSql(queryTables, dialect); + } + + return sql.toString(); + } + + @Override + public Object getValue() { + return queryWrapper.getValueArray(); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryColumn.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryColumn.java new file mode 100644 index 00000000..b17f48f0 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryColumn.java @@ -0,0 +1,325 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + + +import com.mybatisflex.core.dialect.IDialect; +import com.mybatisflex.core.table.TableDef; +import com.mybatisflex.core.util.StringUtil; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; + +/** + * 查询列,描述的是一张表的字段 + */ +public class QueryColumn implements Serializable { + + protected QueryTable table; + protected String name; + protected String alias; + + + public QueryColumn() { + } + + public QueryColumn(String name) { + this.name = name; + } + + public QueryColumn(String tableName, String name) { + this.table = new QueryTable(tableName); + this.name = name; + } + + public QueryColumn(TableDef tableDef, String name) { + this.table = new QueryTable(tableDef.getTableName()); + this.name = name; + } + + + public QueryTable getTable() { + return table; + } + + public void setTable(QueryTable table) { + this.table = table; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public QueryColumn as(String alias) { + QueryColumn newColumn = new QueryColumn(); + newColumn.table = this.table; + newColumn.name = this.name; + newColumn.alias = alias; + return newColumn; + } + + + // query methods /////// + + /** + * equals + * + * @param value + * @return + */ + public QueryCondition eq(Object value) { + return QueryCondition.create(this, QueryCondition.LOGIC_EQUALS, value); + } + + + /** + * not equals != + * + * @param value + * @return + */ + public QueryCondition ne(Object value) { + return QueryCondition.create(this, QueryCondition.LOGIC_NOT_EQUALS, value); + } + + + public QueryCondition like(Object value) { + return QueryCondition.create(this, QueryCondition.LOGIC_LIKE, "%" + value + "%"); + } + + + public QueryCondition likeLeft(Object value) { + return QueryCondition.create(this, QueryCondition.LOGIC_LIKE, "%" + value); + } + + + public QueryCondition likeRight(Object value) { + return QueryCondition.create(this, QueryCondition.LOGIC_LIKE, value + "%"); + } + + /** + * 大于 greater than + * + * @param value + * @return + */ + public QueryCondition gt(Object value) { + return QueryCondition.create(this, QueryCondition.LOGIC_GT, value); + } + + /** + * 大于等于 greater or equal + * + * @param value + * @return + */ + public QueryCondition ge(Object value) { + return QueryCondition.create(this, QueryCondition.LOGIC_GE, value); + } + + /** + * 小于 less than + * + * @param value + * @return + */ + public QueryCondition lt(Object value) { + return QueryCondition.create(this, QueryCondition.LOGIC_LT, value); + } + + /** + * 小于等于 less or equal + * + * @param value + * @return + */ + public QueryCondition le(Object value) { + return QueryCondition.create(this, QueryCondition.LOGIC_LE, value); + } + + + /** + * IS NULL + * + * @param name + * @return + */ + public QueryCondition isNull(String name) { + return QueryCondition.create(this, QueryCondition.LOGIC_IS_NULL, null); + } + + + + /** + * IS NOT NULL + * + * @param name + * @return + */ + public QueryCondition isNotNull(String name) { + return QueryCondition.create(this, QueryCondition.LOGIC_IS_NOT_NULL, null); + } + + + /** + * in arrays + * + * @param arrays + * @return + */ + public QueryCondition in(Object... arrays) { + //忽略 QueryWrapper.in("name", null) 的情况 + if (arrays != null && arrays.length == 1 && arrays[0] == null) { + return QueryCondition.createEmpty(); + } + return QueryCondition.create(this, QueryCondition.LOGIC_IN, arrays); + } + + /** + * in child select + * @param queryWrapper + * @return + */ + public QueryCondition in(QueryWrapper queryWrapper) { + return QueryCondition.create(this, QueryCondition.LOGIC_IN, queryWrapper); + } + + + /** + * in Collection + * + * @param collection + * @return + */ + public QueryCondition in(Collection collection) { + if (collection != null && !collection.isEmpty()) { + return in(collection.toArray()); + } + return QueryCondition.createEmpty(); + } + + /** + * not int arrays + * + * @param arrays + * @return + */ + public QueryCondition notIn(Object... arrays) { + //忽略 QueryWrapper.notIn("name", null) 的情况 + if (arrays != null && arrays.length == 1 && arrays[0] == null) { + return QueryCondition.createEmpty(); + } + return QueryCondition.create(this, QueryCondition.LOGIC_NOT_IN, arrays); + } + + + /** + * not in Collection + * + * @param collection + * @return + */ + public QueryCondition notIn(Collection collection) { + if (collection != null && !collection.isEmpty()) { + return notIn(collection.toArray()); + } + return QueryCondition.createEmpty(); + } + + /** + * not in child select + * @param queryWrapper + */ + public QueryCondition notIn(QueryWrapper queryWrapper) { + return QueryCondition.create(this, QueryCondition.LOGIC_NOT_IN, queryWrapper); + } + + + /** + * between + * @param start + * @param end + */ + public QueryCondition between(Object start, Object end) { + return QueryCondition.create(this, QueryCondition.LOGIC_BETWEEN, new Object[]{start, end}); + } + + /** + * not between + * + * @param start + * @param end + */ + public QueryCondition notBetween(Object start, Object end) { + return QueryCondition.create(this, QueryCondition.LOGIC_NOT_BETWEEN, new Object[]{start, end}); + } + + + ////orrder by //// + public QueryOrderBy asc() { + QueryOrderBy queryOrderBy = new QueryOrderBy(this); + return queryOrderBy; + } + + + public QueryOrderBy desc() { + return new QueryOrderBy(this, "DESC"); + } + + + public String wrap(IDialect dialect, String table, String column) { + if (StringUtil.isNotBlank(table)) { + return dialect.wrap(table) + "." + dialect.wrap(column); + } else { + return dialect.wrap(column); + } + } + + String toConditionSql(List queryTables, IDialect dialect) { + String tableName = WrapperUtil.getRealTableName(queryTables, table); + return wrap(dialect, tableName, name); + } + + + String toSelectSql(List queryTables, IDialect dialect) { + String tableName = WrapperUtil.getRealTableName(queryTables, table); + return wrap(dialect, tableName, name) + WrapperUtil.buildAsAlias(dialect.wrap(alias)); + } + + @Override + public String toString() { + return "QueryColumn{" + + "table=" + table + + ", name='" + name + '\'' + + ", alias='" + alias + '\'' + + '}'; + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryCondition.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryCondition.java new file mode 100644 index 00000000..01a0329d --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryCondition.java @@ -0,0 +1,252 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + + +import com.mybatisflex.core.dialect.IDialect; + +import java.io.Serializable; +import java.util.List; +import java.util.function.Supplier; + +public class QueryCondition implements Serializable { + + public static final String LOGIC_LIKE = "LIKE"; + public static final String LOGIC_GT = ">"; + public static final String LOGIC_GE = ">="; + public static final String LOGIC_LT = "<"; + public static final String LOGIC_LE = "<="; + public static final String LOGIC_EQUALS = "="; + public static final String LOGIC_NOT_EQUALS = "!="; + + public static final String LOGIC_IS_NULL = "IS NULL"; + public static final String LOGIC_IS_NOT_NULL = "IS NOT NULL"; + + public static final String LOGIC_IN = "IN"; + public static final String LOGIC_NOT_IN = "NOT IN"; + public static final String LOGIC_BETWEEN = "BETWEEN"; + public static final String LOGIC_NOT_BETWEEN = "NOT BETWEEN"; + + + protected QueryColumn column; + protected String logic; + protected Object value; + protected boolean effective = true; + + //当前条件的上个条件 + protected QueryCondition before; + //当前条件的上个下一个 + protected QueryCondition next; + //两个条件直接的连接符 + protected SqlConnector connector; + + + public static QueryCondition createEmpty() { + return new QueryCondition().when(false); + } + + + public static QueryCondition create(QueryColumn queryColumn, Object value) { + return create(queryColumn, LOGIC_EQUALS, value); + } + + public static QueryCondition create(QueryColumn queryColumn, String logic, Object value) { + QueryCondition column = new QueryCondition(); + column.setColumn(queryColumn); + column.setLogic(logic); + column.setValue(value); + return column; + } + + public QueryCondition() { + } + + public QueryColumn getColumn() { + return column; + } + + public void setColumn(QueryColumn column) { + this.column = column; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public String getLogic() { + return logic; + } + + public void setLogic(String logic) { + this.logic = logic; + } + + /** + * 计算问号(?)的数量 + * + * @return 问号的数量 + */ + public int calculateQuestionMarkCount() { + if (LOGIC_IS_NULL.equals(logic) + || LOGIC_IS_NOT_NULL.equals(logic) + || value instanceof QueryColumn + || value instanceof QueryWrapper) { + return 0; + } + //between, not between + else if (LOGIC_BETWEEN.equals(logic) || LOGIC_NOT_BETWEEN.equals(logic)) { + return 2; + } + //in, not in + else if (LOGIC_IN.equals(logic) || LOGIC_NOT_IN.equals(logic)) { + return calculateValueArrayCount(); + } + // + else { + return 1; + } + } + + private int calculateValueArrayCount() { + Object[] values = (Object[]) value; + int paramsCount = 0; + for (Object v : values) { + if (v.getClass() == int[].class) { + paramsCount += ((int[]) v).length; + } else if (v.getClass() == long[].class) { + paramsCount += ((long[]) v).length; + } else if (v.getClass() == short[].class) { + paramsCount += ((short[]) v).length; + } else { + paramsCount++; + } + } + return paramsCount; + } + + public QueryCondition when(boolean effective) { + this.effective = effective; + return this; + } + + public void when(Supplier fn) { + this.effective = fn.get(); + } + + public boolean checkEffective() { + return effective; + } + + + public QueryCondition and(QueryCondition nextCondition) { + return new Brackets(this).and(nextCondition); + } + + + public QueryCondition or(QueryCondition nextCondition) { + return new Brackets(this).or(nextCondition); + } + + + protected void connect(QueryCondition nextCondition, SqlConnector connector) { + if (this.next != null) { + this.next.connect(nextCondition, connector); + } else { + this.next = nextCondition; + this.connector = connector; + nextCondition.before = this; + } + } + + public String toSql(List queryTables, IDialect dialect) { + StringBuilder sql = new StringBuilder(); + //检测是否生效 + if (checkEffective()) { + QueryCondition effectiveBefore = getEffectiveBefore(); + if (effectiveBefore != null) { + sql.append(effectiveBefore.connector); + } + sql.append(getColumn().toConditionSql(queryTables, dialect)); + sql.append(" ").append(logic).append(" "); + if (value instanceof QueryColumn) { + sql.append(((QueryColumn) value).toConditionSql(queryTables, dialect)); + } + //子查询 + else if (value instanceof QueryWrapper) { + sql.append("(").append(dialect.buildSelectSql((QueryWrapper) value)).append(")"); + } + //正常查询,构建问号 + else { + appendQuestionMark(sql, calculateQuestionMarkCount()); + } + } + + if (this.next != null) { + return sql + next.toSql(queryTables, dialect); + } + + return sql.toString(); + } + + + protected QueryCondition getEffectiveBefore() { + if (before != null && before.checkEffective()) { + return before; + } else if (before != null) { + return before.getEffectiveBefore(); + } else { + return null; + } + } + + + protected static void appendQuestionMark(StringBuilder sqlBuilder, int paramsCount) { + if (paramsCount == 1) { + sqlBuilder.append(" ? "); + } + //between, not between + else if (paramsCount == 2) { + sqlBuilder.append(" ? AND ? "); + } + //in, not in + else if (paramsCount > 0) { + sqlBuilder.append('('); + for (int i = 0; i < paramsCount; i++) { + sqlBuilder.append('?'); + if (i != paramsCount - 1) { + sqlBuilder.append(','); + } + } + sqlBuilder.append(')'); + } else { + // paramsCount == 0, ignore + } + } + + @Override + public String toString() { + return "QueryCondition{" + + "column=" + column + + ", logic='" + logic + '\'' + + ", value=" + value + + ", effective=" + effective + + '}'; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryMethods.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryMethods.java new file mode 100644 index 00000000..12deaaed --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryMethods.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + +public class QueryMethods { + + public static FunctionQueryColumn count() { + return new FunctionQueryColumn("COUNT", "*"); + } + + public static FunctionQueryColumn count(String column) { + return new FunctionQueryColumn("COUNT", column); + } + + public static FunctionQueryColumn count(QueryColumn column) { + return new FunctionQueryColumn("COUNT", column); + } + + public static FunctionQueryColumn max(String column) { + return new FunctionQueryColumn("MAX", column); + } + + public static FunctionQueryColumn max(QueryColumn column) { + return new FunctionQueryColumn("MAX", column); + } + + public static FunctionQueryColumn min(String column) { + return new FunctionQueryColumn("MIN", column); + } + + public static FunctionQueryColumn min(QueryColumn column) { + return new FunctionQueryColumn("MIN", column); + } + + public static FunctionQueryColumn avg(String column) { + return new FunctionQueryColumn("AVG", column); + } + + public static FunctionQueryColumn avg(QueryColumn column) { + return new FunctionQueryColumn("AVG", column); + } + + public static FunctionQueryColumn sum(String column) { + return new FunctionQueryColumn("SUM", column); + } + + public static FunctionQueryColumn sum(QueryColumn column) { + return new FunctionQueryColumn("SUM", column); + } + + public static DistinctQueryColumn distinct(QueryColumn... columns) { + return new DistinctQueryColumn(columns); + } + + public static QueryCondition exist(QueryWrapper queryWrapper) { + return new OperatorSelectCondition(" EXIST ", queryWrapper); + } + + public static QueryCondition notExist(QueryWrapper queryWrapper) { + return new OperatorSelectCondition(" NOT EXIST ", queryWrapper); + } + + + public static QueryCondition not(QueryCondition childCondition) { + return new OperatorQueryCondition(" NOT ", childCondition); + } + + public static QueryCondition noCondition(){ + return QueryCondition.createEmpty(); + } + + + private static QueryWrapper newWrapper() { + return new QueryWrapper(); + } + + public static QueryWrapper select(QueryColumn... queryColumns) { + return newWrapper().select(queryColumns); + } + + public static QueryWrapper selectOne() { + return select(new StringQueryColumn("1")); + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryOrderBy.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryOrderBy.java new file mode 100644 index 00000000..5d83c911 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryOrderBy.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + + +import com.mybatisflex.core.dialect.IDialect; + +import java.io.Serializable; +import java.util.List; + +/** + * 排序字段 + */ +public class QueryOrderBy implements Serializable { + + private QueryColumn queryColumn; + + private String orderType = "ASC"; //asc desc + + private boolean nullsFirst = false; + private boolean nullsLast = false; + + protected QueryOrderBy() { + } + + public QueryOrderBy(QueryColumn queryColumn, String orderType) { + this.queryColumn = queryColumn; + this.orderType = orderType; + } + + + public QueryOrderBy(QueryColumn queryColumn) { + this.queryColumn = queryColumn; + } + + + public QueryOrderBy nullsFirst() { + this.nullsFirst = true; + this.nullsLast = false; + return this; + } + + + public QueryOrderBy nullsLast() { + this.nullsFirst = false; + this.nullsLast = true; + return this; + } + + + public String toSql(List queryTables, IDialect dialect) { + String sql = queryColumn.toConditionSql(queryTables, dialect) + " " + orderType; + if (nullsFirst) { + sql = sql + " NULLS FIRST"; + } else if (nullsLast) { + sql = sql + " NULLS LAST"; + } + return sql; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryTable.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryTable.java new file mode 100644 index 00000000..86536a87 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryTable.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + +import com.mybatisflex.core.dialect.IDialect; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 查询列,描述的是一张表的字段 + */ +public class QueryTable implements Serializable { + + protected String name; + protected String alias; + + public QueryTable() { + } + + public QueryTable(String name) { + this.name = name; + } + + public QueryTable(String table, String alias) { + this.name = table; + this.alias = alias; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + + public QueryTable as(String alias) { + this.alias = alias; + return this; + } + + boolean isSameTable(QueryTable table) { + return table != null && Objects.equals(name, table.name); + } + + + public String toSql(IDialect dialect) { + return dialect.wrap(name) + WrapperUtil.buildAsAlias(dialect.wrap(alias)); + } + + @Override + public String toString() { + return "QueryTable{" + + "name='" + name + '\'' + + ", alias='" + alias + '\'' + + '}'; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryWrapper.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryWrapper.java new file mode 100644 index 00000000..a5acd847 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/QueryWrapper.java @@ -0,0 +1,315 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + +import com.mybatisflex.core.exception.FlexExceptions; +import com.mybatisflex.core.table.TableDef; +import com.mybatisflex.core.util.ArrayUtil; +import com.mybatisflex.core.util.CollectionUtil; + +import java.util.ArrayList; +import java.util.Map; + +public class QueryWrapper extends BaseQueryWrapper { + + + public static QueryWrapper create() { + return new QueryWrapper(); + } + + public QueryWrapper select(QueryColumn... queryColumns) { + for (QueryColumn column : queryColumns) { + if (column != null) { + addSelectColumn(column); + } + } + return this; + } + + + public QueryWrapper from(TableDef tableDef) { + return from(tableDef.getTableName()); + } + + + public QueryWrapper from(String table) { + return from(new QueryTable(table)); + } + + + public QueryWrapper from(QueryTable table) { + if (CollectionUtil.isEmpty(queryTables)) { + queryTables = new ArrayList<>(); + queryTables.add(table); + } else { + boolean contains = false; + for (QueryTable queryTable : queryTables) { + if (queryTable.isSameTable(table)) { + contains = true; + } + } + if (!contains) { + queryTables.add(table); + } + } + return this; + } + + + public QueryWrapper from(QueryWrapper queryWrapper) { + return from(new SelectQueryTable(queryWrapper)); + } + + public QueryWrapper as(String alias) { + if (CollectionUtil.isEmpty(queryTables)) { + throw new IllegalArgumentException("query table must not be empty."); + } + if (queryTables.size() > 1) { + throw FlexExceptions.wrap("QueryWrapper.as(...) only support 1 table"); + } + queryTables.get(0).alias = alias; + return this; + } + + + public QueryWrapper where(QueryCondition queryCondition) { + this.setWhereQueryCondition(queryCondition); + return this; + } + + public QueryWrapper where(String sql) { + this.setWhereQueryCondition(new StringQueryCondition(sql)); + return this; + } + + + public QueryWrapper where(String sql, Object... params) { + this.setWhereQueryCondition(new StringQueryCondition(sql, params)); + return this; + } + + + public QueryWrapper where(Map whereConditions) { + if (whereConditions != null) { + whereConditions.forEach((s, o) -> and(QueryCondition.create(new QueryColumn(s), o))); + } + return this; + } + + + public QueryWrapper and(QueryCondition queryCondition) { + return addWhereQueryCondition(queryCondition, SqlConnector.AND); + } + + public QueryWrapper and(String sql) { + this.addWhereQueryCondition(new StringQueryCondition(sql), SqlConnector.AND); + return this; + } + + + public QueryWrapper and(String sql, Object... params) { + this.addWhereQueryCondition(new StringQueryCondition(sql, params), SqlConnector.AND); + return this; + } + + + public QueryWrapper or(QueryCondition queryCondition) { + return addWhereQueryCondition(queryCondition, SqlConnector.OR); + } + + + public Joiner leftJoin(String table) { + return joining(Join.TYPE_LEFT, table, true); + } + + + public Joiner leftJoinIf(String table, boolean condition) { + return joining(Join.TYPE_LEFT, table, condition); + } + + public Joiner leftJoin(TableDef table) { + return joining(Join.TYPE_LEFT, table.getTableName(), true); + } + + + public Joiner leftJoinIf(TableDef table, boolean condition) { + return joining(Join.TYPE_LEFT, table.getTableName(), condition); + } + + public Joiner leftJoin(QueryWrapper table) { + return joining(Join.TYPE_LEFT, table, true); + } + + public Joiner leftJoinIf(QueryWrapper table, boolean condition) { + return joining(Join.TYPE_LEFT, table, condition); + } + + public Joiner rightJoin(String table) { + return joining(Join.TYPE_RIGHT, table, true); + } + + public Joiner rightJoinIf(String table, boolean condition) { + return joining(Join.TYPE_RIGHT, table, condition); + } + + public Joiner rightJoin(QueryWrapper table) { + return joining(Join.TYPE_RIGHT, table, true); + } + + public Joiner rightJoinIf(QueryWrapper table, boolean condition) { + return joining(Join.TYPE_RIGHT, table, condition); + } + + public Joiner innerJoin(String table) { + return joining(Join.TYPE_INNER, table, true); + } + + public Joiner innerJoinIf(String table, boolean condition) { + return joining(Join.TYPE_INNER, table, condition); + } + + public Joiner innerJoin(QueryWrapper table) { + return joining(Join.TYPE_INNER, table, true); + } + + public Joiner innerJoinIf(QueryWrapper table, boolean condition) { + return joining(Join.TYPE_INNER, table, condition); + } + + public Joiner fullJoin(String table) { + return joining(Join.TYPE_FULL, table, true); + } + + public Joiner fullJoinIf(String table, boolean condition) { + return joining(Join.TYPE_FULL, table, condition); + } + + public Joiner fullJoin(QueryWrapper table) { + return joining(Join.TYPE_FULL, table, true); + } + + public Joiner fullJoinIf(QueryWrapper table, boolean condition) { + return joining(Join.TYPE_FULL, table, condition); + } + public Joiner crossJoin(String table) { + return joining(Join.TYPE_CROSS, table, true); + } + + public Joiner crossJoinIf(String table, boolean condition) { + return joining(Join.TYPE_CROSS, table, condition); + } + + public Joiner crossJoin(QueryWrapper table) { + return joining(Join.TYPE_CROSS, table, true); + } + + public Joiner crossJoinIf(QueryWrapper table, boolean condition) { + return joining(Join.TYPE_CROSS, table, condition); + } + + + + protected Joiner joining(String type, String table, boolean condition) { + Join join = new Join(type, table, condition); + addJoinTable(join.getQueryTable()); + return new Joiner<>(AddJoin(join), join); + } + + protected Joiner joining(String type, QueryWrapper queryWrapper, boolean condition) { + Join join = new Join(type, queryWrapper, condition); + addJoinTable(join.getQueryTable()); + return new Joiner<>(AddJoin(join), join); + } + + + public QueryWrapper groupBy(String name) { + addGroupByColumns(new QueryColumn(name)); + return this; + } + + public QueryWrapper groupBy(String... names) { + for (String name : names) { + groupBy(name); + } + return this; + } + + public QueryWrapper groupBy(QueryColumn column) { + addGroupByColumns(column); + return this; + } + + public QueryWrapper groupBy(QueryColumn... columns) { + for (QueryColumn column : columns) { + groupBy(column); + } + return this; + } + + + public QueryWrapper having(QueryCondition queryCondition) { + addHavingQueryCondition(queryCondition, SqlConnector.AND); + return this; + } + + public QueryWrapper orderBy(QueryOrderBy... orderBys) { + for (QueryOrderBy queryOrderBy : orderBys) { + addOrderBy(queryOrderBy); + } + return this; + } + + public QueryWrapper orderBy(String... orderBys) { + for (String queryOrderBy : orderBys) { + addOrderBy(new StringQueryOrderBy(queryOrderBy)); + } + return this; + } + + + public QueryWrapper limit(Integer rows) { + setLimitRows(rows); + return this; + } + + public QueryWrapper offset(Integer offset) { + setLimitOffset(offset); + return this; + } + + public QueryWrapper limit(Integer offset, Integer rows) { + setLimitOffset(offset); + setLimitRows(rows); + return this; + } + + public QueryWrapper datasource(String datasource) { + setDatasource(datasource); + return this; + } + + /** + * 获取 queryWrapper 的参数 + * 在构建 sql 的时候,需要保证 where 在 having 的前面 + */ + Object[] getValueArray() { + Object[] whereValues = WrapperUtil.getValues(whereQueryCondition); + Object[] havingValues = WrapperUtil.getValues(havingQueryCondition); + return ArrayUtil.concat(whereValues, havingValues); + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/SelectQueryTable.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/SelectQueryTable.java new file mode 100644 index 00000000..e4c70ba0 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/SelectQueryTable.java @@ -0,0 +1,37 @@ +package com.mybatisflex.core.querywrapper; + +import com.mybatisflex.core.dialect.IDialect; +import com.mybatisflex.core.util.StringUtil; + +/** + * 查询的 table, + * 实例1:用于构建 select * from (select ...) 中的第二个 select + * 实例2:用于构建 left join (select ...) 中的 select + */ +public class SelectQueryTable extends QueryTable { + + private QueryWrapper queryWrapper; + + public SelectQueryTable(QueryWrapper queryWrapper) { + super(); + this.queryWrapper = queryWrapper; + } + + public QueryWrapper getQueryWrapper() { + return queryWrapper; + } + + public void setQueryWrapper(QueryWrapper queryWrapper) { + this.queryWrapper = queryWrapper; + } + + @Override + public String toSql(IDialect dialect) { + String sql = dialect.buildSelectSql(queryWrapper); + if (StringUtil.isNotBlank(alias)) { + return "(" + sql + ") AS " + dialect.wrap(alias); + } else { + return sql; + } + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/SqlConnector.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/SqlConnector.java new file mode 100644 index 00000000..25563d6b --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/SqlConnector.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + +public enum SqlConnector { + + + AND(" AND "), +// AND_NOT(" AND NOT "), +// AND_EXISTS(" AND EXISTS "), +// AND_NOT_EXISTS(" AND NOT EXISTS "), + OR(" OR "), +// OR_NOT(" OR NOT "), +// OR_EXISTS(" OR EXISTS "), +// OR_NOT_EXISTS(" OR NOT EXISTS "), +// NOT(" NOT "), + ; + + + private String value; + + SqlConnector(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/StringQueryColumn.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/StringQueryColumn.java new file mode 100644 index 00000000..b52fa840 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/StringQueryColumn.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + + +import com.mybatisflex.core.dialect.IDialect; + +import java.util.List; + +/** + * 自定义字符串列,用于扩展 + */ +public class StringQueryColumn extends QueryColumn { + + protected String content; + + + public StringQueryColumn(String content) { + this.content = content; + } + + @Override + String toConditionSql(List queryTables, IDialect dialect) { + return content; + } + + @Override + String toSelectSql(List queryTables, IDialect dialect) { + return content; + } + + @Override + public String toString() { + return "StringQueryColumn{" + + "content='" + content + '\'' + + '}'; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/StringQueryCondition.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/StringQueryCondition.java new file mode 100644 index 00000000..4465e1e6 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/StringQueryCondition.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + + +import com.mybatisflex.core.dialect.IDialect; + +import java.util.List; + +/** + * 自定义字符串列,用于扩展 + */ +public class StringQueryCondition extends QueryCondition { + + protected String sqlContent; + + + public StringQueryCondition(String content) { + this.sqlContent = content; + } + + public StringQueryCondition(String content, Object... paras) { + this.sqlContent = content; + this.setValue(paras); + } + + @Override + public String toSql(List queryTables, IDialect dialect) { + StringBuilder sql = new StringBuilder(); + + //检测是否生效 + if (checkEffective()) { + QueryCondition effectiveBefore = getEffectiveBefore(); + if (effectiveBefore != null) { + sql.append(effectiveBefore.connector); + } + sql.append(" ").append(sqlContent).append(" "); + } + + if (this.next != null) { + return sql + next.toSql(queryTables, dialect); + } + + return sql.toString(); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/StringQueryOrderBy.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/StringQueryOrderBy.java new file mode 100644 index 00000000..2fd92757 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/StringQueryOrderBy.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + + +import com.mybatisflex.core.dialect.IDialect; + +import java.util.List; + +/** + * 排序字段 + */ +public class StringQueryOrderBy extends QueryOrderBy { + + private String orderBy; + + public StringQueryOrderBy(String orderBy) { + this.orderBy = orderBy; + } + + @Override + public String toSql(List queryTables, IDialect dialect) { + return orderBy; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/WrapperUtil.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/WrapperUtil.java new file mode 100644 index 00000000..42e7f37e --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/querywrapper/WrapperUtil.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.querywrapper; + + +import com.mybatisflex.core.util.CollectionUtil; +import com.mybatisflex.core.util.StringUtil; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +class WrapperUtil { + + + static String buildAsAlias(String alias) { + return StringUtil.isBlank(alias) ? "" : " AS " + alias; + } + + static final Object[] NULL_PARA_ARRAY = new Object[0]; + + static Object[] getValues(QueryCondition condition) { + if (condition == null) { + return NULL_PARA_ARRAY; + } + + List paras = new LinkedList<>(); + getValues(condition, paras); + + return paras.isEmpty() ? NULL_PARA_ARRAY : paras.toArray(); + } + + + private static void getValues(QueryCondition condition, List paras) { + if (condition == null) { + return; + } + Object value = condition.getValue(); + if (value != null) { + if (value.getClass().isArray()) { + Object[] values = (Object[]) value; + for (Object v : values) { + if (v.getClass() == int[].class) { + addAll(paras, (int[]) v); + } else if (v.getClass() == long[].class) { + addAll(paras, (long[]) v); + } else if (v.getClass() == short[].class) { + addAll(paras, (short[]) v); + } else { + paras.add(v); + } + } + } else if (value instanceof QueryWrapper) { + Object[] valueArray = ((QueryWrapper) value).getValueArray(); + paras.addAll(Arrays.asList(valueArray)); + } else { + paras.add(value); + } + } + + getValues(condition.next, paras); + } + + + private static void addAll(List paras, int[] ints) { + for (int i : ints) { + paras.add(i); + } + } + + private static void addAll(List paras, long[] longs) { + for (long i : longs) { + paras.add(i); + } + } + + + private static void addAll(List paras, short[] shorts) { + for (short i : shorts) { + paras.add(i); + } + } + + + public static String getRealTableName(List queryTables, QueryTable queryTable) { + QueryTable realTable = getRealTable(queryTables, queryTable); + if (realTable == null) { + return ""; + } + return StringUtil.isNotBlank(realTable.alias) ? realTable.alias : realTable.name; + } + + public static QueryTable getRealTable(List queryTables, QueryTable queryTable) { + if (CollectionUtil.isEmpty(queryTables)) { + return queryTable; + } + + if (queryTable == null && queryTables.size() == 1) { + return queryTables.get(0); + } + + for (QueryTable table : queryTables) { + if (table.isSameTable(queryTable)) { + return table; + } + } + return queryTable; + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/Db.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/Db.java new file mode 100644 index 00000000..8f83b265 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/Db.java @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.row; + +import com.mybatisflex.core.FlexGlobalConfig; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.querywrapper.QueryWrapper; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.util.MapUtil; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 针对 RowMapper 的静态方法进行封装 + */ +public class Db { + + private static final Map INVOKER_MAP = new ConcurrentHashMap<>(); + static RowMapperInvoker defaultRowMapperInvoker; + + public static RowMapperInvoker invoker() { + if (defaultRowMapperInvoker == null) { + SqlSessionFactory sqlSessionFactory = FlexGlobalConfig.getDefaultConfig().getSqlSessionFactory(); + defaultRowMapperInvoker = new RowMapperInvoker(sqlSessionFactory); + } + return defaultRowMapperInvoker; + } + + public static RowMapperInvoker invoker(String environmentId) { + return MapUtil.computeIfAbsent(INVOKER_MAP, environmentId, key -> { + SqlSessionFactory sqlSessionFactory = FlexGlobalConfig.getConfig(key).getSqlSessionFactory(); + return new RowMapperInvoker(sqlSessionFactory); + }); + } + + public static int insertBySql(String sql, Object... args) { + return invoker().insertBySql(sql, args); + } + + public static int insertRow(String tableName, Row row) { + return invoker().insertRow(tableName, row); + } + + public static int[] insertBatch(String tableName, Collection rows) { + return insertBatch(tableName, rows, rows.size()); + } + + public static int[] insertBatch(String tableName, Collection rows, int batchSize) { + return invoker().insertBatch(tableName, rows, batchSize); + } + + public static int insertBatchWithFirstRowColumns(String tableName, List rows) { + return invoker().insertBatchWithFirstRowColumns(tableName, rows); + } + + public static int deleteBySql(String sql, Object... args) { + return invoker().deleteBySql(sql, args); + } + + public static int deleteById(String tableName, Row row) { + return invoker().deleteById(tableName, row); + } + + public static int deleteById(String tableName, String primaryKey, Object id) { + return invoker().deleteById(tableName, primaryKey, id); + } + + public static int deleteBatchByIds(String tableName, String primaryKey, Collection ids) { + return invoker().deleteBatchByIds(tableName, primaryKey, ids); + } + + public static int deleteByByMap(String tableName, Map whereColumns) { + return invoker().deleteByByMap(tableName, whereColumns); + } + + public static int deleteByQuery(String tableName, QueryWrapper queryWrapper) { + return invoker().deleteByQuery(tableName, queryWrapper); + } + + public static int updateBySql(String sql, Object... args) { + return invoker().updateBySql(sql, args); + } + + public static int updateById(String tableName, Row row) { + return invoker().updateById(tableName, row); + } + + public static int updateByMap(String tableName, Row data, Map whereColumns) { + return invoker().updateByMap(tableName, data, whereColumns); + } + + public static int updateByQuery(String tableName, Row data, QueryWrapper queryWrapper) { + return invoker().updateByQuery(tableName, data, queryWrapper); + } + + public static int updateBatchById(String tableName, List rows) { + return invoker().updateBatchById(tableName, rows); + } + + public static Row selectOneBySql(String sql, Object... args) { + return invoker().selectOneBySql(sql, args); + } + + public static Row selectOneById(String tableName, Row row) { + return invoker().selectOneById(tableName, row); + } + + public static Row selectOneById(String tableName, String primaryKey, Object id) { + return invoker().selectOneById(tableName, primaryKey, id); + } + + public static Row selectOneByMap(String tableName, Map whereColumns) { + return invoker().selectOneByMap(tableName, whereColumns); + } + + public static Row selectOneByQuery(String tableName, QueryWrapper queryWrapper) { + return invoker().selectOneByQuery(tableName, queryWrapper); + } + + public static List selectListBySql(String sql, Object... args) { + return invoker().selectListBySql(sql, args); + } + + public static List selectListByMap(String tableName, Map whereColumns) { + return invoker().selectListByMap(tableName, whereColumns); + } + + public static List selectListByQuery(String tableName, QueryWrapper queryWrapper) { + return invoker().selectListByQuery(tableName, queryWrapper); + } + + public static List selectAll(String tableName) { + return invoker().selectAll(tableName); + } + + public static Object selectObject(String sql, Object... args) { + return invoker().selectObject(sql, args); + } + + public static List selectObjectList(String sql, Object... args) { + return invoker().selectObjectList(sql, args); + } + + public static long selectCount(String sql, Object... args) { + return invoker().selectCount(sql, args); + } + + public static long selectCountByQuery(String tableName, QueryWrapper queryWrapper) { + return invoker().selectCountByQuery(tableName, queryWrapper); + } + + public static Page paginate(String tableName, int pageNumber, int pageSize, QueryWrapper queryWrapper) { + return invoker().paginate(tableName, pageNumber, pageSize, queryWrapper); + } + + public static Page paginate(String tableName, Page page, QueryWrapper queryWrapper) { + return invoker().paginate(tableName, page, queryWrapper); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/Row.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/Row.java new file mode 100644 index 00000000..504dc54b --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/Row.java @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.row; + +import com.mybatisflex.core.javassist.ModifyAttrsRecord; +import com.mybatisflex.core.table.TableInfo; +import com.mybatisflex.core.table.TableInfos; +import com.mybatisflex.core.util.ArrayUtil; + +import java.util.HashMap; +import java.util.Set; + +public class Row extends HashMap implements ModifyAttrsRecord { + private static final Object[] NULL_ARGS = new Object[0]; + + //主键,多个主键用英文逗号隔开 + private RowKey[] primaryKeys; + + public static Row of(String key, Object value) { + Row row = new Row(); + return row.set(key, value); + } + + + public static Row ofKey(String primaryKey, Object value) { + Row row = new Row(); + String[] primaryKeyStrings = primaryKey.split(","); + row.primaryKeys = new RowKey[primaryKeyStrings.length]; + + for (int i = 0; i < primaryKeyStrings.length; i++) { + row.primaryKeys[i] = RowKey.of(primaryKeyStrings[i].trim()); + } + + if (primaryKeyStrings.length > 0 && !value.getClass().isArray()) { + throw new IllegalArgumentException("the type of value[\"" + value + "\"] must be an array."); + } + + if (primaryKeyStrings.length == 1) { + row.put(primaryKey.trim(), value); + } else { + Object[] values = (Object[]) value; + for (int i = 0; i < primaryKeyStrings.length; i++) { + row.put(primaryKeyStrings[i].trim(), values[i]); + } + } + return row; + } + + public static Row ofKey(RowKey ...rowKeys) { + Row row = new Row(); + row.primaryKeys = rowKeys; + return row; + } + + + + public static Row ofKey(RowKey rowKey, Object value) { + Row row = new Row(); + row.primaryKeys = new RowKey[]{rowKey}; + row.put(rowKey.keyColumn, value); + return row; + } + + + public static Row ofKey(RowKey[] rowKeys, Object[] value) { + Row row = new Row(); + row.primaryKeys = rowKeys; + for (int i = 0; i < rowKeys.length; i++) { + row.put(rowKeys[i].keyColumn, value[i]); + } + return row; + } + + + public Row set(String key, Object value) { + put(key, value); + boolean isPrimaryKey = false; + if (this.primaryKeys != null){ + for (RowKey rowKey : primaryKeys) { + if (rowKey.getKeyColumn().equals(key)){ + isPrimaryKey = true; + break; + } + } + } + + if (!isPrimaryKey){ + addModifyAttr(key); + } + + return this; + } + + + public Object get(Object key, Object defaultValue) { + Object result = super.get(key); + return result != null ? result : defaultValue; + } + + + @Override + public Object remove(Object key) { + removeModifyAttr(key.toString()); + return super.remove(key); + } + + + public T toEntity(Class entityClass) { + TableInfo tableInfo = TableInfos.ofEntityClass(entityClass); + return tableInfo.newInstanceByRow(this); + } + + + public void keep(Set attrs) { + if (attrs == null) { + throw new NullPointerException("attrs is null."); + } + + clearModifyFlag(); + modifyAttrs.addAll(attrs); + } + + /** + * 获取修改的值,值需要保持顺序 + * 返回的内容不包含主键的值 + * + * @return values 数组 + */ + public Object[] obtainModifyValues() { + Object[] values = new Object[modifyAttrs.size()]; + int index = 0; + for (String modifyAttr : modifyAttrs) { + values[index++] = get(modifyAttr); + } + return values; + } + + + public String[] obtainsPrimaryKeyStrings() { + String[] returnKeys = new String[primaryKeys.length]; + for (int i = 0; i < primaryKeys.length; i++) { + returnKeys[i] = primaryKeys[i].keyColumn; + } + return returnKeys; + } + + + public RowKey[] obtainsPrimaryKeys() { + return this.primaryKeys; + } + + + public Object[] obtainsPrimaryValues() { + if (ArrayUtil.isEmpty(primaryKeys)) { + return NULL_ARGS; + } + Object[] values = new Object[primaryKeys.length]; + for (int i = 0; i < primaryKeys.length; i++) { + values[i] = get(primaryKeys[i].keyColumn); + } + return values; + } + + + public Object[] obtainModifyValuesAndPrimaryValues() { + return ArrayUtil.concat(obtainModifyValues(), obtainsPrimaryValues()); + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowKey.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowKey.java new file mode 100644 index 00000000..1205af1c --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowKey.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.row; + +import com.mybatisflex.core.enums.KeyType; + +/** + * row 的主键策略 + */ +public class RowKey { + + /** + * 自增 ID + */ + public static final RowKey ID_AUTO = new UnModifiableRowKey("id",KeyType.Auto,null,false); + + /** + * UUID 的 ID + */ + public static final RowKey ID_UUID = new UnModifiableRowKey("id",KeyType.Generator,"uuid",true); + + + public static RowKey of(String keyColumn) { + RowKey rowKey = new RowKey(); + rowKey.keyColumn = keyColumn; + return rowKey; + } + + public static RowKey of(String keyColumn, KeyType keyType) { + RowKey rowKey = new RowKey(); + rowKey.keyColumn = keyColumn; + rowKey.keyType = keyType; + return rowKey; + } + + public static RowKey of(String keyColumn, KeyType keyType, String keyTypeValue) { + RowKey rowKey = new RowKey(); + rowKey.keyColumn = keyColumn; + rowKey.keyType = keyType; + rowKey.value = keyTypeValue; + return rowKey; + } + + public static RowKey of(String keyColumn, KeyType keyType, String keyTypeValue, boolean before) { + RowKey rowKey = new RowKey(); + rowKey.keyColumn = keyColumn; + rowKey.keyType = keyType; + rowKey.value = keyTypeValue; + rowKey.before = before; + return rowKey; + } + + /** + * 主键字段 + */ + protected String keyColumn; + + /** + * 主键类型 + */ + protected KeyType keyType = KeyType.Auto; + + /** + * 主键类型为 Sequence 和 Generator 时的对应的内容 + */ + protected String value; + + /** + * 是否前执行 + */ + protected boolean before = true; + + + public String getKeyColumn() { + return keyColumn; + } + + public void setKeyColumn(String keyColumn) { + this.keyColumn = keyColumn; + } + + public KeyType getKeyType() { + return keyType; + } + + public void setKeyType(KeyType keyType) { + this.keyType = keyType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public boolean isBefore() { + return before; + } + + public void setBefore(boolean before) { + this.before = before; + } + + static class UnModifiableRowKey extends RowKey{ + + public UnModifiableRowKey(String keyColumn, KeyType keyType, String value, boolean before) { + super(); + this.keyColumn = keyColumn; + this.keyType = keyType; + this.value = value; + this.before = before; + } + + @Override + public void setKeyColumn(String keyColumn) { + throw new UnsupportedOperationException("unsupported setKeyColumn!"); + } + + @Override + public void setKeyType(KeyType keyType) { + throw new UnsupportedOperationException("unsupported setKeyType!"); + } + + @Override + public void setValue(String value) { + throw new UnsupportedOperationException("unsupported setValue!"); + } + + @Override + public void setBefore(boolean before) { + throw new UnsupportedOperationException("unsupported setBefore!"); + } + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowMapper.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowMapper.java new file mode 100644 index 00000000..fbf83835 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowMapper.java @@ -0,0 +1,446 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.row; + +import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.exception.FlexExceptions; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.provider.RowSqlProvider; +import com.mybatisflex.core.querywrapper.CPI; +import com.mybatisflex.core.querywrapper.QueryColumn; +import com.mybatisflex.core.querywrapper.QueryWrapper; +import com.mybatisflex.core.util.StringUtil; +import org.apache.ibatis.annotations.*; +import org.apache.ibatis.exceptions.TooManyResultsException; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + + +public interface RowMapper { + + //////insert ////// + + /** + * 执行 insert sql 语句 + * + * @param sql insert sql 语句 + * @param args 参数 + * @return 执行影响的行数 + * @see Db#insertBySql(String, Object...) + */ + @InsertProvider(value = RowSqlProvider.class, method = RowSqlProvider.METHOD_RAW_SQL) + int insertBySql(@Param(FlexConsts.SQL) String sql, @Param(FlexConsts.SQL_ARGS) Object... args); + + + /** + * 插入 row 到数据表 + * + * @param tableName 表名 + * @param row 数据内容,当设置有主键时,主键会自动填充 + * @return 执行影响的行数 + * @see RowSqlProvider#insertRow(Map) + */ + @InsertProvider(value = RowSqlProvider.class, method = "insertRow") + int insertRow(@Param(FlexConsts.TABLE_NAME) String tableName, @Param(FlexConsts.ROW) Row row); + + + /** + * 批量插入 rows 到数据表 + *

+ * 注意,批量插入中,只会根据第一条 row 数据来构建 Sql 插入字段,若每条数据字段不一致,可能造成个别字段无法插入的情况 + * + * @param tableName 表名 + * @param rows 数据内容,当设置有主键时,主键会自动填充 + * @return 执行影响的行数 + * @see RowSqlProvider#insertBatchWithFirstRowColumns(Map) + */ + @InsertProvider(value = RowSqlProvider.class, method = "insertBatchWithFirstRowColumns") + int insertBatchWithFirstRowColumns(@Param(FlexConsts.TABLE_NAME) String tableName, @Param(FlexConsts.ROWS) List rows); + + + /////// delete ///// + + /** + * 执行 delete sql 语言 + * + * @param sql delete sql 语句 + * @param args 参数 + * @return 执行影响的行数 + */ + @DeleteProvider(value = RowSqlProvider.class, method = RowSqlProvider.METHOD_RAW_SQL) + int deleteBySql(@Param(FlexConsts.SQL) String sql, @Param(FlexConsts.SQL_ARGS) Object... args); + + /** + * 根据 id 删除数据 + * + * @param tableName 表名 + * @param row id 和 值的数据,可以通过 {@link Row#ofKey(String, Object)} 来创建 + * @return 执行影响的行数 + */ + + default int deleteById(@Param(FlexConsts.TABLE_NAME) String tableName, @Param(FlexConsts.ROW) Row row) { + return deleteById(tableName, StringUtil.join(",", row.obtainsPrimaryKeyStrings()), row.obtainsPrimaryValues()); + } + + /** + * 根据 id 删除数据 + * + * @param tableName 表名 + * @param primaryKey 主键,多个主键用英文逗号隔开 + * @param id 数据,多个主键时传入数组,例如 new Object[]{1,2} + * @return 执行影响的行数 + * @see RowSqlProvider#deleteById(Map) + */ + @DeleteProvider(value = RowSqlProvider.class, method = "deleteById") + int deleteById(@Param(FlexConsts.TABLE_NAME) String tableName, @Param(FlexConsts.PRIMARY_KEY) String primaryKey, @Param(FlexConsts.PRIMARY_VALUE) Object id); + + + /** + * 根据 多个 id 值删除多条数据 + * + * @param tableName 表名 + * @param primaryKey 主键 + * @param ids id 的集合 + * @return 执行影响的行数 + * @see RowSqlProvider#deleteBatchByIds(Map) + */ + @DeleteProvider(value = RowSqlProvider.class, method = "deleteBatchByIds") + int deleteBatchByIds(@Param(FlexConsts.TABLE_NAME) String tableName, @Param(FlexConsts.PRIMARY_KEY) String primaryKey, @Param(FlexConsts.PRIMARY_VALUE) Collection ids); + + /** + * 根据 map 来构建条件删除数据 + *

+ * 注意: + * 删除 map 不允许为 null 或者 空内容,否则可能造成数据全部删除的情况 + * 若想删除全部数据,请执行 {@link RowMapper#deleteBySql(String, Object...)} 方法 + * + * @param tableName 表名 + * @param whereConditions 条件,通过 map 的 key:value 来构建,都是 and 的关系 + * @return 执行影响的行数 + */ + default int deleteByByMap(String tableName, Map whereConditions) { + if (whereConditions == null || whereConditions.isEmpty()) { + throw FlexExceptions.wrap("whereConditions can not be null or empty."); + } + return deleteByQuery(tableName, new QueryWrapper().where(whereConditions)); + } + + + /** + * 根据 queryWrapper 构建 where 条件来删除数据 + * + * @param tableName 表名 + * @param queryWrapper queryWrapper + * @return 执行影响的行数 + * @see RowSqlProvider#deleteByQuery(Map) + */ + @DeleteProvider(value = RowSqlProvider.class, method = "deleteByQuery") + int deleteByQuery(@Param(FlexConsts.TABLE_NAME) String tableName, @Param(FlexConsts.QUERY) QueryWrapper queryWrapper); + + + ////////update //// + + /** + * 执行 update sql 语句 + * + * @param sql sql 语句 + * @param args 参数内容 + * @return 执行影响的行数 + */ + @UpdateProvider(value = RowSqlProvider.class, method = RowSqlProvider.METHOD_RAW_SQL) + int updateBySql(@Param(FlexConsts.SQL) String sql, @Param(FlexConsts.SQL_ARGS) Object... args); + + + /** + * 根据主键来更新数据 + * + * @param tableName 表名 + * @param row 数据,其必须包含主键数据列名和值 + * @return 执行影响的行数 + * @see RowSqlProvider#updateById(Map) + */ + @UpdateProvider(value = RowSqlProvider.class, method = "updateById") + int updateById(@Param(FlexConsts.TABLE_NAME) String tableName, @Param(FlexConsts.ROW) Row row); + + + /** + * 根据 map 来更新数据 + * + * @param tableName 表名 + * @param data 要更新的数据 + * @param whereConditions 条件,通过 map 的 key:value 来构建,都是 and 的关系 + * @return 执行影响的行数 + */ + default int updateByMap(String tableName, Row data, Map whereConditions) { + return updateByQuery(tableName, data, new QueryWrapper().where(whereConditions)); + } + + + /** + * 根据 queryWrapper 来构建 where 条件更新数据 + * + * @param tableName 表名 + * @param data 更新数据 + * @param queryWrapper queryWrapper + * @return 执行影响的行数 + * @see RowSqlProvider#updateByQuery(Map) + */ + @UpdateProvider(value = RowSqlProvider.class, method = "updateByQuery") + int updateByQuery(@Param(FlexConsts.TABLE_NAME) String tableName, @Param(FlexConsts.ROW) Row data, @Param(FlexConsts.QUERY) QueryWrapper queryWrapper); + + + /** + * 根据主键来批量更新数据 + * 注意: + * 1、此方法需要在 mysql 等链接配置需要开启 allowMultiQueries=true + * 2、更新成功返回的结果也可能为 0 + * + * @param tableName 表名 + * @param rows 数据,其必须包含主键数据列名和值 + * @return 执行影响的行数 + * @see RowSqlProvider#updateBatchById(Map) + */ + @UpdateProvider(value = RowSqlProvider.class, method = "updateBatchById") + int updateBatchById(@Param(FlexConsts.TABLE_NAME) String tableName, @Param(FlexConsts.ROWS) List rows); + + ///////select ///// + + /** + * 通过原生 SQL 查询 1 条数据,要求数据必须返回 1 条内容,否则会报错 + * + * @param sql select sql 语句 + * @param args 参数 + * @return 返回一条数据 + */ + default Row selectOneBySql(String sql, Object... args) { + List rows = selectListBySql(sql, args); + if (rows == null || rows.isEmpty()) { + return null; + } else if (rows.size() == 1) { + return rows.get(0); + } else { + /** 当返回多条数据时,抛出异常, 保持和 Mybatis DefaultSqlSession 的统一逻辑, + * see: {@link org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(String, Object)} **/ + throw new TooManyResultsException("Expected one result (or null) to be returned by selectOneBySql(), but found: " + rows.size()); + } + } + + /** + * 通过主键来查询数据 + * + * @param tableName 表名 + * @param row 主键和ID的描述,通过 {@link Row#ofKey(String, Object)} 来进行构建 + * @return 返回一条数据,或者 null + */ + default Row selectOneById(String tableName, Row row) { + return selectOneById(tableName, StringUtil.join(",", row.obtainsPrimaryKeyStrings()), row.obtainsPrimaryValues()); + } + + + /** + * 根据主键来查询数据 + * + * @param tableName 表名 + * @param primaryKey 主键 + * @param id id 值 + * @return row or null + * @see RowSqlProvider#selectOneById(Map) + */ + @SelectProvider(value = RowSqlProvider.class, method = "selectOneById") + Row selectOneById(@Param(FlexConsts.TABLE_NAME) String tableName, @Param(FlexConsts.PRIMARY_KEY) String primaryKey, @Param(FlexConsts.PRIMARY_VALUE) Object id); + + /** + * 根据 map 组成的条件查询 1 条数据 + * + * @param tableName + * @param whereConditions + * @return + */ + default Row selectOneByMap(String tableName, Map whereConditions) { + return selectOneByQuery(tableName, new QueryWrapper().where(whereConditions)); + } + + + /** + * 根据 queryWrapper 来查询 1 条数据 + * + * @param tableName 表名 + * @param queryWrapper queryWrapper + * @return row or null + */ + default Row selectOneByQuery(String tableName, QueryWrapper queryWrapper) { + List rows = selectListByQuery(tableName, queryWrapper.limit(1)); + if (rows == null || rows.isEmpty()) { + return null; + } else { + return rows.get(0); + } + } + + /** + * 通过自定义 sql 来查询一个 Row 列表 + * + * @param sql 自定义的 sql + * @param args sql 参数 + * @return row 列表 + */ + @SelectProvider(value = RowSqlProvider.class, method = RowSqlProvider.METHOD_RAW_SQL) + List selectListBySql(@Param(FlexConsts.SQL) String sql, @Param(FlexConsts.SQL_ARGS) Object... args); + + + /** + * 根据 map 来查询一个 Row 列表 + * + * @param tableName 表名 + * @param whereConditions 条件 + * @return row 列表 + */ + default List selectListByMap(String tableName, Map whereConditions) { + return selectListByQuery(tableName, new QueryWrapper().where(whereConditions)); + } + + + /** + * 根据 queryWrapper 来查询一个 row 列表 + * + * @param tableName 表名 + * @param queryWrapper queryWrapper + * @return row 列表 + * @see RowSqlProvider#selectListByQuery(Map) + */ + @SelectProvider(value = RowSqlProvider.class, method = "selectListByQuery") + List selectListByQuery(@Param(FlexConsts.TABLE_NAME) String tableName, @Param(FlexConsts.QUERY) QueryWrapper queryWrapper); + + + /** + * 查询某张表的全部数据 + * + * @param tableName 表名 + * @return row 列表 + */ + default List selectAll(@Param(FlexConsts.TABLE_NAME) String tableName) { + return selectListByMap(tableName, null); + } + + /** + * 通过 sql 查询某一个数据,sql 执行的结果应该只有 1 行 1 列 + * 若返回有多列,则只取第一列的值,若有多行,则会出现 TooManyResultsException 错误 + * + * @param sql sql + * @param args sql 参数 + * @return object + */ + @SelectProvider(value = RowSqlProvider.class, method = RowSqlProvider.METHOD_RAW_SQL) + Object selectObject(@Param(FlexConsts.SQL) String sql, @Param(FlexConsts.SQL_ARGS) Object... args); + + + /** + * 通过 sql 查询多行数据,sql 执行的结果应该只有 1 列 + * + * @param sql sql 语句 + * @param args sql 参数 + * @return object list + */ + @SelectProvider(value = RowSqlProvider.class, method = RowSqlProvider.METHOD_RAW_SQL) + List selectObjectList(@Param(FlexConsts.SQL) String sql, @Param(FlexConsts.SQL_ARGS) Object... args); + + + /** + * 查询数据,一般用于 select count(*)... 的语言,也可用于执行的结果只有一个数值的其他 sql + * + * @param sql sql 语句 + * @param args sql 参数 + * @return 返回数据 + */ + default long selectCount(String sql, Object... args) { + Object object = selectObject(sql, args); + if (object == null) { + return 0; + } else if (object instanceof Number) { + return ((Number) object).longValue(); + } else { + throw FlexExceptions.wrap("selectCount error, Can not get number value for sql: %s", sql); + } + } + + /** + * 根据 queryWrapper 来查询数据 + * + * @param tableName 表名 + * @param queryWrapper queryWrapper + * @return 数量 + * @see RowSqlProvider#selectCountByQuery(Map) + */ + @SelectProvider(value = RowSqlProvider.class, method = "selectCountByQuery") + long selectCountByQuery(@Param(FlexConsts.TABLE_NAME) String tableName, @Param(FlexConsts.QUERY) QueryWrapper queryWrapper); + + + /** + * 分页查询某张表的数据 + * + * @param tableName 表名 + * @param pageNumber 当前页码 + * @param pageSize 每页的数据量 + * @param queryWrapper 条件封装 + * @return 一页数据 + */ + default Page paginate(String tableName, int pageNumber, int pageSize, QueryWrapper queryWrapper) { + Page page = new Page<>(pageNumber, pageSize); + return paginate(tableName, page, queryWrapper); + } + + + /** + * 分页查询数据 + * + * @param tableName 表名 + * @param page page 封装类 + * @param queryWrapper 条件 + * @return + */ + default Page paginate(String tableName, Page page, QueryWrapper queryWrapper) { + + + List groupByColumns = CPI.getGroupByColumns(queryWrapper); + + // 只有 totalRow 小于 0 的时候才会去查询总量 + // 这样方便用户做总数缓存,而非每次都要去查询总量 + // 一般的分页场景中,只有第一页的时候有必要去查询总量,第二页以后是不需要的 + if (page.getTotalRow() < 0) { + + //清除group by 去查询数据 + CPI.setGroupByColumns(queryWrapper, null); + long count = selectCountByQuery(tableName, queryWrapper); + page.setTotalRow(count); + } + + if (page.getTotalRow() == 0 || page.getPageNumber() > page.getTotalPage()) { + return page; + } + + //恢复数量查询清除的 groupBy + CPI.setGroupByColumns(queryWrapper, groupByColumns); + int offset = page.getPageSize() * (page.getPageNumber() - 1); + queryWrapper.limit(offset, page.getPageSize()); + List rows = selectListByQuery(tableName, queryWrapper); + page.setList(rows); + return page; + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowMapperInvoker.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowMapperInvoker.java new file mode 100644 index 00000000..00a013f4 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowMapperInvoker.java @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.row; + +import com.mybatisflex.core.FlexGlobalConfig; +import com.mybatisflex.core.dialect.DbType; +import com.mybatisflex.core.dialect.DialectFactory; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.querywrapper.QueryWrapper; +import org.apache.ibatis.executor.BatchResult; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public class RowMapperInvoker { + + private final SqlSessionFactory sqlSessionFactory; + private final DbType dbType; + private RowSessionManager rowSessionManager = RowSessionManager.DEFAULT; + + public RowMapperInvoker(SqlSessionFactory sqlSessionFactory) { + this.sqlSessionFactory = sqlSessionFactory; + this.dbType = FlexGlobalConfig.getConfig(sqlSessionFactory.getConfiguration().getEnvironment().getId()).getDbType(); + } + + public RowSessionManager getRowSessionManager() { + return rowSessionManager; + } + + public void setRowSessionManager(RowSessionManager rowSessionManager) { + this.rowSessionManager = rowSessionManager; + } + + private R execute(Function function) { + SqlSession sqlSession = rowSessionManager.getSqlSession(sqlSessionFactory); + try { + DialectFactory.setHintDbType(dbType); + RowMapper mapper = sqlSession.getMapper(RowMapper.class); + return function.apply(mapper); + } finally { + DialectFactory.clearHintDbType(); + rowSessionManager.releaseSqlSession(sqlSession, sqlSessionFactory); + } + } + + + public int insertBySql(String sql, Object... args) { + return execute(mapper -> mapper.insertBySql(sql, args)); + } + + public int insertRow(String tableName, Row row) { + return execute(mapper -> mapper.insertRow(tableName, row)); + } + + public int[] insertBatch(String tableName, Collection rows, int batchSize) { + int[] results = new int[rows.size()]; + SqlSession sqlSession = rowSessionManager.getSqlSession(sqlSessionFactory, ExecutorType.BATCH); + try { + DialectFactory.setHintDbType(dbType); + RowMapper mapper = sqlSession.getMapper(RowMapper.class); + int counter = 0; + int resultsPos = 0; + for (Row row : rows) { + if (++counter >= batchSize) { + counter = 0; + List batchResults = sqlSession.flushStatements(); + for (BatchResult batchResult : batchResults) { + int[] updateCounts = batchResult.getUpdateCounts(); + for (int updateCount : updateCounts) { + results[resultsPos++] = updateCount; + } + } + } else { + mapper.insertRow(tableName, row); + } + } + } finally { + DialectFactory.clearHintDbType(); + rowSessionManager.releaseSqlSession(sqlSession, sqlSessionFactory); + } + return results; + } + + public int insertBatchWithFirstRowColumns(String tableName, List rows) { + return execute(mapper -> mapper.insertBatchWithFirstRowColumns(tableName, rows)); + } + + public int deleteBySql(String sql, Object... args) { + return execute(mapper -> mapper.deleteBySql(sql, args)); + } + + public int deleteById(String tableName, Row row) { + return execute(mapper -> mapper.deleteById(tableName, row)); + } + + public int deleteById(String tableName, String primaryKey, Object id) { + return execute(mapper -> mapper.deleteById(tableName, primaryKey, id)); + } + + public int deleteBatchByIds(String tableName, String primaryKey, Collection ids) { + return execute(mapper -> mapper.deleteBatchByIds(tableName, primaryKey, ids)); + } + + public int deleteByByMap(String tableName, Map whereColumns) { + return execute(mapper -> mapper.deleteByByMap(tableName, whereColumns)); + } + + public int deleteByQuery(String tableName, QueryWrapper queryWrapper) { + return execute(mapper -> mapper.deleteByQuery(tableName, queryWrapper)); + } + + public int updateBySql(String sql, Object... args) { + return execute(mapper -> mapper.updateBySql(sql, args)); + } + + public int updateById(String tableName, Row row) { + return execute(mapper -> mapper.updateById(tableName, row)); + } + + public int updateByMap(String tableName, Row data, Map whereColumns) { + return execute(mapper -> mapper.updateByMap(tableName, data, whereColumns)); + } + + public int updateByQuery(String tableName, Row data, QueryWrapper queryWrapper) { + return execute(mapper -> mapper.updateByQuery(tableName, data, queryWrapper)); + } + + public int updateBatchById(String tableName, List rows) { + return execute(mapper -> mapper.updateBatchById(tableName, rows)); + } + + public Row selectOneBySql(String sql, Object... args) { + return execute(mapper -> mapper.selectOneBySql(sql, args)); + } + + public Row selectOneById(String tableName, Row row) { + return execute(mapper -> mapper.selectOneById(tableName, row)); + } + + public Row selectOneById(String tableName, String primaryKey, Object id) { + return execute(mapper -> mapper.selectOneById(tableName, primaryKey, id)); + } + + public Row selectOneByMap(String tableName, Map whereColumns) { + return execute(mapper -> mapper.selectOneByMap(tableName, whereColumns)); + } + + public Row selectOneByQuery(String tableName, QueryWrapper queryWrapper) { + return execute(mapper -> mapper.selectOneByQuery(tableName, queryWrapper)); + } + + public List selectListBySql(String sql, Object... args) { + return execute(mapper -> mapper.selectListBySql(sql, args)); + } + + public List selectListByMap(String tableName, Map whereColumns) { + return execute(mapper -> mapper.selectListByMap(tableName, whereColumns)); + } + + public List selectListByQuery(String tableName, QueryWrapper queryWrapper) { + return execute(mapper -> mapper.selectListByQuery(tableName, queryWrapper)); + } + + public List selectAll(String tableName) { + return execute(mapper -> mapper.selectAll(tableName)); + } + + public Object selectObject(String sql, Object... args) { + return execute(mapper -> mapper.selectObject(sql, args)); + } + + public List selectObjectList(String sql, Object... args) { + return execute(mapper -> mapper.selectObjectList(sql, args)); + } + + public long selectCount(String sql, Object... args) { + return execute(mapper -> mapper.selectCount(sql, args)); + } + + public long selectCountByQuery(String tableName, QueryWrapper queryWrapper) { + return execute(mapper -> mapper.selectCountByQuery(tableName, queryWrapper)); + } + + public Page paginate(String tableName, int pageNumber, int pageSize, QueryWrapper queryWrapper) { + return execute(mapper -> mapper.paginate(tableName, pageNumber, pageSize, queryWrapper)); + } + + public Page paginate(String tableName, Page page, QueryWrapper queryWrapper) { + return execute(mapper -> mapper.paginate(tableName, page, queryWrapper)); + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowSessionManager.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowSessionManager.java new file mode 100644 index 00000000..6c7e6b8a --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowSessionManager.java @@ -0,0 +1,45 @@ +package com.mybatisflex.core.row; + +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; + +public interface RowSessionManager { + RowSessionManager DEFAULT = new RowSessionManager() { + @Override + public SqlSession getSqlSession(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) { + return sqlSessionFactory.openSession(executorType); + } + + @Override + public void releaseSqlSession(SqlSession sqlSession, SqlSessionFactory sqlSessionFactory) { + sqlSession.commit(); + sqlSession.close(); + } + }; + + /** + * 获取 sqlSession + * + * @param sqlSessionFactory + */ + default SqlSession getSqlSession(SqlSessionFactory sqlSessionFactory){ + return getSqlSession(sqlSessionFactory,sqlSessionFactory.getConfiguration().getDefaultExecutorType()); + } + + + /** + * 获取 sqlSession + * @param sqlSessionFactory + * @param executorType + */ + SqlSession getSqlSession(SqlSessionFactory sqlSessionFactory, ExecutorType executorType); + + /** + * 释放 sqlSession + * + * @param sqlSession + */ + void releaseSqlSession(SqlSession sqlSession, SqlSessionFactory sqlSessionFactory); + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/BaseReflectorFactory.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/BaseReflectorFactory.java new file mode 100644 index 00000000..0b03371b --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/BaseReflectorFactory.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.table; + +import org.apache.ibatis.reflection.ReflectorFactory; + +public abstract class BaseReflectorFactory implements ReflectorFactory { + @Override + public boolean isClassCacheEnabled() { + return true; + } + + @Override + public void setClassCacheEnabled(boolean classCacheEnabled) { + + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/ColumnInfo.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/ColumnInfo.java new file mode 100644 index 00000000..e25109f9 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/ColumnInfo.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.table; + +public class ColumnInfo { + + /** + * 数据库列名 + */ + protected String column; + + /** + * java entity 定义的属性名称 + */ + protected String property; + + /** + * 属性类型 + */ + protected Class propertyType; + + public String getColumn() { + return column; + } + + public void setColumn(String column) { + this.column = column; + } + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + + public Class getPropertyType() { + return propertyType; + } + + public void setPropertyType(Class propertyType) { + this.propertyType = propertyType; + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/EntityMetaObject.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/EntityMetaObject.java new file mode 100644 index 00000000..52abfb54 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/EntityMetaObject.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.table; + +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.ReflectorFactory; +import org.apache.ibatis.reflection.factory.DefaultObjectFactory; +import org.apache.ibatis.reflection.factory.ObjectFactory; +import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory; +import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory; + + +public final class EntityMetaObject { + + public static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory(); + public static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory(); + + private EntityMetaObject() { + // Prevent Instantiation of Static Class + } + + + public static MetaObject forObject(Object object, ReflectorFactory reflectorFactory) { + return MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, reflectorFactory); + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/IdInfo.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/IdInfo.java new file mode 100644 index 00000000..c66e0aea --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/IdInfo.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.table; + +import com.mybatisflex.annotation.Id; +import com.mybatisflex.core.enums.KeyType; + +public class IdInfo extends ColumnInfo { + + /** + * id 生成策略 + */ + private KeyType keyType; + + /** + * 若 keyType 类型是 sequence, value 则代表的是 + * sequence 序列的 sql 内容 + * 例如:select SEQ_USER_ID.nextval as id from dual + * + * 若 keyType 是 Generator,value 则代表的是使用的那个 keyGenerator 的名称 + */ + private String value; + + + /** + * sequence 序列内容执行顺序 + * + * @see org.apache.ibatis.executor.keygen.SelectKeyGenerator + */ + private boolean before; + + + public IdInfo(ColumnInfo columnInfo) { + this.setColumn(columnInfo.getColumn()); + this.setProperty(columnInfo.getProperty()); + this.setPropertyType(columnInfo.getPropertyType()); + + //当 id 的类型为数值时,默认设置为自增的方式 + if (Number.class.isAssignableFrom(columnInfo.getPropertyType())) { + keyType = KeyType.Auto; + } else { + keyType = KeyType.None; + } + } + + public IdInfo(String column, String property, Class propertyType, Id id) { + this.column = column; + this.property = property; + this.propertyType = propertyType; + this.keyType = id.keyType(); + this.value = id.value(); + this.before = id.before(); + } + + public KeyType getKeyType() { + return keyType; + } + + public void setKeyType(KeyType keyType) { + this.keyType = keyType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public boolean isBefore() { + return before; + } + + public void setBefore(boolean before) { + this.before = before; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableDef.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableDef.java new file mode 100644 index 00000000..54ad989f --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableDef.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.table; + +import com.mybatisflex.core.querywrapper.QueryTable; + +import java.io.Serializable; + +public class TableDef implements Serializable { + + private String tableName; + + public TableDef(String tableName) { + this.tableName = tableName; + } + + public String getTableName() { + return tableName; + } + + public QueryTable as(String alias) { + return new QueryTable(tableName, alias); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfo.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfo.java new file mode 100644 index 00000000..b9c77e45 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfo.java @@ -0,0 +1,382 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.table; + +import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.enums.KeyType; +import com.mybatisflex.core.javassist.ModifyAttrsRecord; +import com.mybatisflex.core.row.Row; +import com.mybatisflex.core.util.ArrayUtil; +import com.mybatisflex.core.util.ClassUtil; +import com.mybatisflex.core.util.CollectionUtil; +import org.apache.ibatis.mapping.ResultFlag; +import org.apache.ibatis.mapping.ResultMap; +import org.apache.ibatis.mapping.ResultMapping; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.Reflector; +import org.apache.ibatis.reflection.ReflectorFactory; +import org.apache.ibatis.session.Configuration; + +import java.util.*; + +public class TableInfo { + + private String schema; //schema + private String tableName; //表名 + private Class entityClass; //实体类 + private boolean useCached = false; + private boolean camelToUnderline = true; + + + private String[] columns = new String[0]; // 所有的字段,但除了主键的列 + private String[] primaryKeys = new String[0]; //主键字段 + + //在插入数据的时候,支持主动插入的主键字段 + //排除主键为 auto 和 before 为 false 的主键字段 + private String[] insertPrimaryKeys; + + private List columnInfoList; + private List primaryKeyList; + + //column 和 java 属性的称的关系映射 + private Map columnPropertyMapping = new HashMap<>(); + private Map propertyColumnMapping = new HashMap<>(); + + private final ReflectorFactory reflectorFactory = new BaseReflectorFactory() { + @Override + public Reflector findForClass(Class type) { + return getReflector(); + } + }; + private Reflector reflector; //反射工具 + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public Class getEntityClass() { + return entityClass; + } + + public void setEntityClass(Class entityClass) { + this.entityClass = entityClass; + } + + public boolean isUseCached() { + return useCached; + } + + public void setUseCached(boolean useCached) { + this.useCached = useCached; + } + + public boolean isCamelToUnderline() { + return camelToUnderline; + } + + public void setCamelToUnderline(boolean camelToUnderline) { + this.camelToUnderline = camelToUnderline; + } + + public Reflector getReflector() { + return reflector; + } + + public ReflectorFactory getReflectorFactory() { + return reflectorFactory; + } + + public void setReflector(Reflector reflector) { + this.reflector = reflector; + } + + public String[] getColumns() { + return columns; + } + + + public void setColumns(String[] columns) { + this.columns = columns; + } + + public String[] getPrimaryKeys() { + return primaryKeys; + } + + public void setPrimaryKeys(String[] primaryKeys) { + this.primaryKeys = primaryKeys; + } + + + public List getColumnInfoList() { + return columnInfoList; + } + + + void setColumnInfoList(List columnInfoList) { + this.columnInfoList = columnInfoList; + this.columns = new String[columnInfoList.size()]; + for (int i = 0; i < columnInfoList.size(); i++) { + ColumnInfo columnInfo = columnInfoList.get(i); + columns[i] = columnInfo.getColumn(); + columnPropertyMapping.put(columnInfo.column, columnInfo.property); + propertyColumnMapping.put(columnInfo.property, columnInfo.column); + } + } + + public List getPrimaryKeyList() { + return primaryKeyList; + } + + void setPrimaryKeyList(List primaryKeyList) { + this.primaryKeyList = primaryKeyList; + this.primaryKeys = new String[primaryKeyList.size()]; + + List insertIdFields = new ArrayList<>(); + for (int i = 0; i < primaryKeyList.size(); i++) { + IdInfo idInfo = primaryKeyList.get(i); + primaryKeys[i] = idInfo.getColumn(); + + if (idInfo.getKeyType() != KeyType.Auto && idInfo.isBefore()) { + insertIdFields.add(idInfo.getColumn()); + } + + columnPropertyMapping.put(idInfo.column, idInfo.property); + propertyColumnMapping.put(idInfo.property, idInfo.column); + } + this.insertPrimaryKeys = insertIdFields.toArray(new String[0]); + } + + + /** + * 插入(新增)数据时,获取所有要插入的字段 + * + * @return 字段列表 + */ + public String[] obtainInsertColumns() { + return ArrayUtil.concat(insertPrimaryKeys, columns); + } + + /** + * 根据 插入字段 获取所有插入的值 + * + * @param entity 从 entity 中获取 + * @return 数组 + */ + public Object[] obtainInsertValues(Object entity) { + MetaObject metaObject = EntityMetaObject.forObject(entity, reflectorFactory); + String[] insertColumns = obtainInsertColumns(); + Object[] values = new Object[insertColumns.length]; + for (int i = 0; i < insertColumns.length; i++) { + Object value = getColumnValue(metaObject, insertColumns[i]); + values[i] = value; + } + + return values; + } + + + /** + * 获取要修改的值 + * + * @param entity + * @param ignoreNulls + * @return + */ + public Set obtainUpdateColumns(Object entity, boolean ignoreNulls, boolean includePrimary) { + MetaObject metaObject = EntityMetaObject.forObject(entity, reflectorFactory); + Set columns = new LinkedHashSet<>(); //需使用 LinkedHashSet 保证 columns 的顺序 + if (entity instanceof ModifyAttrsRecord) { + Set properties = ((ModifyAttrsRecord) entity).obtainModifyAttrs(); + if (properties.isEmpty()) { + return Collections.emptySet(); + } + for (String property : properties) { + String column = getColumnByProperty(property); + if (!includePrimary && ArrayUtil.contains(primaryKeys, column)) { + continue; + } + Object value = getPropertyValue(metaObject, property); + if (ignoreNulls && value == null) { + continue; + } + columns.add(column); + } + } + //not ModifyAttrsRecord + else { + for (String column : this.columns) { + Object value = getColumnValue(metaObject, column); + if (ignoreNulls && value == null) { + continue; + } + columns.add(column); + } + + // 普通 entity(非 ModifyAttrsRecord) 忽略 includePrimary 的设置 +// if (includePrimary) { +// for (String column : this.primaryKeys) { +// Object value = getColumnValue(metaObject, column); +// if (ignoreNulls && value == null) { +// continue; +// } +// columns.add(column); +// } +// } + } + return columns; + } + + /** + * 获取所有要修改的值,默认为全部除了主键以外的字段 + * + * @param entity 实体对象 + * @return 数组 + */ + public Object[] obtainUpdateValues(Object entity, boolean ignoreNulls, boolean includePrimary) { + MetaObject metaObject = EntityMetaObject.forObject(entity, reflectorFactory); + List values = new ArrayList<>(); + if (entity instanceof ModifyAttrsRecord) { + Set properties = ((ModifyAttrsRecord) entity).obtainModifyAttrs(); + if (properties.isEmpty()) { + return values.toArray(); + } + for (String property : properties) { + String column = getColumnByProperty(property); + if (!includePrimary && ArrayUtil.contains(primaryKeys, column)) { + continue; + } + Object value = getPropertyValue(metaObject, property); + if (ignoreNulls && value == null) { + continue; + } + values.add(value); + } + } + // not ModifyAttrsRecord + else { + for (String column : this.columns) { + Object value = getColumnValue(metaObject, column); + if (ignoreNulls && value == null) { + continue; + } + values.add(value); + } + // 普通 entity 忽略 includePrimary 的设置 +// if (includePrimary) { +// } + } + + return values.toArray(); + } + + + public Object[] obtainPrimaryValues(Object entity) { + MetaObject metaObject = EntityMetaObject.forObject(entity, reflectorFactory); + Object[] values = new Object[primaryKeys.length]; + for (int i = 0; i < primaryKeys.length; i++) { + values[i] = getColumnValue(metaObject, primaryKeys[i]); + } + return values; + } + + + public String getMappedStatementKeyProperties() { + StringJoiner joiner = new StringJoiner(","); + for (IdInfo value : primaryKeyList) { + joiner.add(FlexConsts.ENTITY + "." + value.getProperty()); + } + return joiner.toString(); + } + + + public String getMappedStatementKeyColumns() { + StringJoiner joiner = new StringJoiner(","); + for (IdInfo value : primaryKeyList) { + joiner.add(FlexConsts.ENTITY + "." + value.getColumn()); + } + return joiner.toString(); + } + + public ResultMap buildResultMap(Configuration configuration) { + String resultMapId = entityClass.getName(); + List resultMappings = new ArrayList<>(); + + for (ColumnInfo columnInfo : columnInfoList) { + ResultMapping mapping = new ResultMapping.Builder(configuration, columnInfo.getProperty(), + columnInfo.getColumn(), columnInfo.getPropertyType()).build(); + resultMappings.add(mapping); + } + for (IdInfo idInfo : primaryKeyList) { + ResultMapping mapping = new ResultMapping.Builder(configuration, idInfo.getProperty(), + idInfo.getColumn(), idInfo.getPropertyType()) + .flags(CollectionUtil.newArrayList(ResultFlag.ID)) +// .typeHandler() + .build(); + resultMappings.add(mapping); + } + + return new ResultMap.Builder(configuration, resultMapId, entityClass, resultMappings).build(); + + } + + + private Object getColumnValue(MetaObject metaObject, String column) { + return getPropertyValue(metaObject, columnPropertyMapping.get(column)); + } + + + private Object getPropertyValue(MetaObject metaObject, String property) { + if (property != null && metaObject.hasGetter(property)) { + return metaObject.getValue(property); + } + return null; + } + + + public String getColumnByProperty(String property) { + return propertyColumnMapping.get(property); + } + + + /** + * 通过 row 实例类转换为一个 entity + * @return entity + */ + public T newInstanceByRow(Row row) { + Object instance = ClassUtil.newInstance(entityClass); + MetaObject metaObject = EntityMetaObject.forObject(instance, reflectorFactory); + for (String column : row.keySet()) { + String property = columnPropertyMapping.get(column); + if (metaObject.hasSetter(property)) { + metaObject.setValue(property, row.get(column)); + } + } + return (T) instance; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfos.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfos.java new file mode 100644 index 00000000..d6d254b5 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfos.java @@ -0,0 +1,166 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.table; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.Table; +import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.util.ClassUtil; +import com.mybatisflex.core.util.CollectionUtil; +import com.mybatisflex.core.util.StringUtil; +import org.apache.ibatis.reflection.Reflector; +import org.apache.ibatis.util.MapUtil; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class TableInfos { + private static final Set> defaultSupportColumnTypes = CollectionUtil.newHashSet( + int.class, Integer.class, + short.class, Short.class, + long.class, Long.class, + float.class, Float.class, + double.class, Double.class, + boolean.class, Boolean.class, + Date.class, java.sql.Date.class, LocalDate.class, LocalDateTime.class, LocalTime.class, + byte[].class, Byte[].class, + BigInteger.class, BigDecimal.class, + char.class, String.class + ); + + + private static Map, TableInfo> tableInfoMap = new ConcurrentHashMap<>(); + + + public static TableInfo ofMapperClass(Class mapperClass) { + return MapUtil.computeIfAbsent(tableInfoMap, mapperClass, key -> { + Class entityClass = getEntityClass(key); + return entityClass != null ? ofEntityClass(entityClass) : null; + }); + } + + + public static TableInfo ofEntityClass(Class entityClass) { + return MapUtil.computeIfAbsent(tableInfoMap, entityClass, key -> createTableInfo(entityClass)); + } + + + private static Class getEntityClass(Class mapperClass) { + Type[] genericInterfaces = mapperClass.getGenericInterfaces(); + if (genericInterfaces.length == 1) { + Type type = genericInterfaces[0]; + if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getActualTypeArguments()[0]; + } else { + return getEntityClass((Class) type); + } + } + + return null; + } + + + private static TableInfo createTableInfo(Class entityClass) { + TableInfo tableInfo = new TableInfo(); + tableInfo.setEntityClass(entityClass); + tableInfo.setReflector(new Reflector(entityClass)); + + + //初始化表名 + Table table = entityClass.getAnnotation(Table.class); + if (table != null) { + tableInfo.setTableName(table.value()); + tableInfo.setSchema(table.schema()); + tableInfo.setUseCached(table.useCached()); + tableInfo.setCamelToUnderline(table.camelToUnderline()); + } else { + //默认为类名转驼峰下划线 + tableInfo.setTableName(StringUtil.camelToUnderline(entityClass.getSimpleName())); + } + + //初始化字段相关 + List columnInfoList = new ArrayList<>(); + List idInfos = new ArrayList<>(); + + Field idField = null; + + List entityFields = ClassUtil.getAllFields(entityClass); + for (Field field : entityFields) { + + //只支持基本数据类型,不支持比如 list set 或者自定义的类等 + if (!defaultSupportColumnTypes.contains(field.getType())) { + continue; + } + + Column column = field.getAnnotation(Column.class); + if (column != null && column.ignore()) { + continue; // ignore + } + + String columnName = column != null && StringUtil.isNotBlank(column.value()) + ? column.value() + : (tableInfo.isCamelToUnderline() ? StringUtil.camelToUnderline(field.getName()) : field.getName()); + + Id id = field.getAnnotation(Id.class); + ColumnInfo columnInfo; + if (id != null) { + columnInfo = new IdInfo(columnName, field.getName(), field.getType(), id); + idInfos.add((IdInfo) columnInfo); + } else { + columnInfo = new ColumnInfo(); + columnInfoList.add(columnInfo); + } + + columnInfo.setColumn(columnName); + columnInfo.setProperty(field.getName()); + columnInfo.setPropertyType(field.getType()); + + if (FlexConsts.DEFAULT_PRIMARY_FIELD.equals(field.getName())) { + idField = field; + } + } + + + if (idInfos.isEmpty() && idField != null) { + int index = -1; + for (int i = 0; i < columnInfoList.size(); i++) { + ColumnInfo columnInfo = columnInfoList.get(i); + if (FlexConsts.DEFAULT_PRIMARY_FIELD.equals(columnInfo.getProperty())) { + index = i; + break; + } + } + if (index >= 0) { + ColumnInfo removedColumnInfo = columnInfoList.remove(index); + idInfos.add(new IdInfo(removedColumnInfo)); + } + } + + tableInfo.setColumnInfoList(columnInfoList); + tableInfo.setPrimaryKeyList(idInfos); + + return tableInfo; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ArrayUtil.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ArrayUtil.java new file mode 100644 index 00000000..5472c485 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ArrayUtil.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.util; + +import java.util.Arrays; +import java.util.Objects; + +public class ArrayUtil { + + + /** + * 判断数组是否为空 + * + * @param array + * @param + * @return 空 true + */ + public static boolean isEmpty(T[] array) { + return array == null || array.length == 0; + } + + + /** + * 判断数组是否不为空 + * + * @param array + * @param + * @return + */ + public static boolean isNotEmpty(T[] array) { + return !isEmpty(array); + } + + + /** + * 合并两个数组为一个新的数组 + * + * @param first 第一个数组 + * @param second 第二个数组 + * @param + * @return 新的数组 + */ + public static T[] concat(T[] first, T[] second) { + if (first == null && second == null) { + throw new IllegalArgumentException("not allow first and second are null."); + } else if (isEmpty(first)) { + return second; + } else if (isEmpty(second)) { + return first; + } else { + T[] result = Arrays.copyOf(first, first.length + second.length); + System.arraycopy(second, 0, result, first.length, second.length); + return result; + } + } + + + /** + * 查看数组中是否包含某一个值 + * + * @param arrays 数组 + * @param object 用于检测的值 + * @param + * @return true 包含 + */ + public static boolean contains(T[] arrays, T object) { + if (isEmpty(arrays)) { + return false; + } + for (T array : arrays) { + if (Objects.equals(array, object)) { + return true; + } + } + return false; + } + + /** + * 急速构建数组 + */ + public static T[] asArray(T... elements) { + return elements; + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ClassUtil.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ClassUtil.java new file mode 100644 index 00000000..7d3810a3 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ClassUtil.java @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.util; + + +import java.lang.reflect.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 类实例创建者创建者 + * Created by michael on 17/3/21. + */ +public class ClassUtil { + + + //proxy frameworks + private static final List PROXY_CLASS_NAMES = Arrays.asList("net.sf.cglib.proxy.Factory" + // cglib + , "org.springframework.cglib.proxy.Factory" + , "javassist.util.proxy.ProxyObject" + // javassist + , "org.apache.ibatis.javassist.util.proxy.ProxyObject"); + + public static boolean isProxy(Class clazz) { + for (Class cls : clazz.getInterfaces()) { + if (PROXY_CLASS_NAMES.contains(cls.getName())) { + return true; + } + } + //java proxy + return Proxy.isProxyClass(clazz); + } + + private static final String ENHANCER_BY = "$$EnhancerBy"; + private static final String JAVASSIST_BY = "_$$_"; + + public static Class getUsefulClass(Class clazz) { + if (isProxy(clazz)) { + return (Class) clazz.getSuperclass(); + } + + //ControllerTest$ServiceTest$$EnhancerByGuice$$40471411#hello -------> Guice + //com.demo.blog.Blog$$EnhancerByCGLIB$$69a17158 ----> CGLIB + //io.jboot.test.app.TestAppListener_$$_jvstb9f_0 ------> javassist + + final String name = clazz.getName(); + if (name.contains(ENHANCER_BY) || name.contains(JAVASSIST_BY)) { + return (Class) clazz.getSuperclass(); + } + + return clazz; + } + + public static T newInstance(Class clazz) { + try { + Constructor constructor = clazz.getDeclaredConstructor(); + return (T) constructor.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + + public static T newInstance(Class clazz, Object... paras) { + try { + Constructor[] constructors = clazz.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + if (isMatchedParas(constructor, paras)) { + Object ret = constructor.newInstance(paras); + return (T) ret; + } + } + + throw new IllegalArgumentException("Can not matched constructor by paras" + Arrays.toString(paras) + " for class: " + clazz.getName()); + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + + private static boolean isMatchedParas(Constructor constructor, Object[] paras) { + if (constructor.getParameterCount() == 0) { + return paras == null || paras.length == 0; + } + + if (constructor.getParameterCount() > 0 + && (paras == null || paras.length != constructor.getParameterCount())) { + return false; + } + + Class[] parameterTypes = constructor.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + Class parameterType = parameterTypes[i]; + Object paraObject = paras[i]; + if (paraObject != null && !parameterType.isAssignableFrom(paraObject.getClass())) { + return false; + } + } + + return true; + } + + + public static String buildMethodString(Method method) { + StringBuilder sb = new StringBuilder() + .append(method.getDeclaringClass().getName()) + .append(".") + .append(method.getName()) + .append("("); + + Class[] params = method.getParameterTypes(); + int in = 0; + for (Class clazz : params) { + sb.append(clazz.getName()); + if (++in < params.length) { + sb.append(","); + } + } + + return sb.append(")").toString(); + } + + + public static boolean hasClass(String className) { + try { + Class.forName(className, false, getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + + public static ClassLoader getClassLoader() { + ClassLoader ret = Thread.currentThread().getContextClassLoader(); + return ret != null ? ret : ClassUtil.class.getClassLoader(); + } + + public static List getAllFields(Class cl) { + List fields = new ArrayList<>(); + doGetFields(cl, fields); + return fields; + } + + private static void doGetFields(Class cl, List fields) { + if (cl == null || cl == Object.class) { + return; + } + + Field[] declaredFields = cl.getDeclaredFields(); + for (Field declaredField : declaredFields) { + fields.add(declaredField); + } + + doGetFields(cl.getSuperclass(), fields); + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/CollectionUtil.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/CollectionUtil.java new file mode 100644 index 00000000..13c46311 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/CollectionUtil.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.util; + +import java.util.*; +import java.util.function.Function; + + +public class CollectionUtil { + + + public static boolean isEmpty(Collection coll) { + return (coll == null || coll.isEmpty()); + } + + + public static boolean isNotEmpty(Collection coll) { + return !isEmpty(coll); + } + + + public static boolean isEmpty(Map map) { + return (map == null || map.isEmpty()); + } + + + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + + /** + * 合并 list + */ + public static List merge(List list, List other) { + if (list == null && other == null) { + return new ArrayList<>(); + } else if (isEmpty(other)) { + return list; + } else if (isEmpty(list)) { + return other; + } + List newList = new ArrayList<>(list); + newList.addAll(other); + return newList; + } + + + public static HashMap newHashMap() { + return new HashMap<>(); + } + + /** + * 主要是用于修复 concurrentHashMap 在 jdk1.8 下的死循环问题 + * + * @see https://bugs.openjdk.org/browse/JDK-8161372 + */ + public static V computeIfAbsent(Map concurrentHashMap, K key, Function mappingFunction) { + V v = concurrentHashMap.get(key); + if (v != null) { + return v; + } + return concurrentHashMap.computeIfAbsent(key, mappingFunction); + } + + public static Set newHashSet(T... elements) { + return new HashSet<>(Arrays.asList(elements)); + } + + public static List newArrayList(T... elements) { + return new ArrayList<>(Arrays.asList(elements)); + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ObjectUtil.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ObjectUtil.java new file mode 100644 index 00000000..7d80e30b --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ObjectUtil.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.util; + +public class ObjectUtil { + + public static T requireNonNullElse(T t1, T t2) { + return t1 == null ? t2 : t1; + } + + public static boolean areNotNull(Object ... objs){ + for (Object obj : objs) { + if (obj == null){ + return false; + } + } + return true; + } + + public static boolean areNull(Object ... objs){ + for (Object obj : objs) { + if (obj != null){ + return false; + } + } + return true; + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/StringUtil.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/StringUtil.java new file mode 100644 index 00000000..94c2ff42 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/StringUtil.java @@ -0,0 +1,251 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.util; + + +import java.util.Collection; +import java.util.function.Function; +import java.util.regex.Pattern; + +public class StringUtil { + + + /** + * 第一个字符转换为小写 + * + * @param string + * @return + */ + public static String firstCharToLowerCase(String string) { + char firstChar = string.charAt(0); + if (firstChar >= 'A' && firstChar <= 'Z') { + char[] arr = string.toCharArray(); + arr[0] += ('a' - 'A'); + return new String(arr); + } + return string; + } + + + /** + * 第一个字符转换为大写 + * + * @param string + */ + public static String firstCharToUpperCase(String string) { + char firstChar = string.charAt(0); + if (firstChar >= 'a' && firstChar <= 'z') { + char[] arr = string.toCharArray(); + arr[0] -= ('a' - 'A'); + return new String(arr); + } + return string; + } + + + /** + * 驼峰转下划线格式 + * + * @param string + */ + public static String camelToUnderline(String string) { + if (isBlank(string)) { + return ""; + } + int len = string.length(); + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { + char c = string.charAt(i); + if (Character.isUpperCase(c) && i > 0) { + sb.append('_'); + } + sb.append(Character.toLowerCase(c)); + } + return sb.toString(); + } + + /** + * 下划线转驼峰格式 + * + * @param string + */ + public static String underlineToCamel(String string) { + if (isBlank(string)) { + return ""; + } + String temp = string.toLowerCase(); + int len = temp.length(); + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { + char c = temp.charAt(i); + if (c == '_') { + if (++i < len) { + sb.append(Character.toUpperCase(temp.charAt(i))); + } + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 字符串为 null 或者内部字符全部为 ' ', '\t', '\n', '\r' 这四类字符时返回 true + */ + public static boolean isBlank(String str) { + if (str == null) { + return true; + } + + for (int i = 0, len = str.length(); i < len; i++) { + if (str.charAt(i) > ' ') { + return false; + } + } + return true; + } + + + public static boolean isAnyBlank(String... strings) { + if (strings == null || strings.length == 0) { + throw new IllegalArgumentException("args is empty."); + } + + for (String str : strings) { + if (isBlank(str)) { + return true; + } + } + return false; + } + + + public static boolean isNotBlank(String str) { + return !isBlank(str); + } + + + public static boolean areNotBlank(String... strings) { + return !isAnyBlank(); + } + + + public static boolean startsWithAny(String str, String... prefixes) { + if (isBlank(str) || prefixes == null || prefixes.length == 0) { + return false; + } + + for (String prefix : prefixes) { + if (str.startsWith(prefix)) { + return true; + } + } + return false; + } + + + public static boolean endsWithAny(String str, String... suffixes) { + if (isBlank(str) || suffixes == null || suffixes.length == 0) { + return false; + } + + for (String suffix : suffixes) { + if (str.endsWith(suffix)) { + return true; + } + } + return false; + } + + + /** + * 正则匹配 + * + * @param regex + * @param input + * @return + */ + public static boolean matches(String regex, String input) { + if (null == regex || null == input) { + return false; + } + return Pattern.matches(regex, input); + } + + /** + * 合并字符串,优化 String.join() 方法 + * + * @param delimiter + * @param elements + * @return 新拼接好的字符串 + * @see String#join(CharSequence, CharSequence...) + */ + public static String join(String delimiter, CharSequence... elements) { + if (ArrayUtil.isEmpty(elements)) { + return ""; + } else if (elements.length == 1) { + return String.valueOf(elements[0]); + } else { + return String.join(delimiter, elements); + } + } + + + /** + * 合并字符串,优化 String.join() 方法 + * + * @param delimiter + * @param elements + * @return 新拼接好的字符串 + * @see String#join(CharSequence, CharSequence...) + */ + public static String join(String delimiter, Collection elements) { + if (CollectionUtil.isEmpty(elements)) { + return ""; + } else if (elements.size() == 1) { + return String.valueOf(elements.iterator().next()); + } else { + return String.join(delimiter, elements); + } + } + + /** + * 合并字符串,优化 String.join() 方法 + * + * @param delimiter + * @param objs + * @param function + * @param + * @return + */ + public static String join(String delimiter, Collection objs, Function function) { + if (CollectionUtil.isEmpty(objs)) { + return ""; + } else if (objs.size() == 1) { + T next = objs.iterator().next(); + return String.valueOf(function.apply(next)); + } else { + String[] strings = new String[objs.size()]; + int index = 0; + for (T obj : objs) { + strings[index++] = function.apply(obj); + } + return String.join(delimiter, strings); + } + } + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/UpdateEntity.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/UpdateEntity.java new file mode 100644 index 00000000..3cb9176e --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/UpdateEntity.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.util; + +import com.mybatisflex.core.javassist.ModifyAttrsRecordProxyFactory; +import org.apache.ibatis.reflection.DefaultReflectorFactory; +import org.apache.ibatis.reflection.Reflector; +import org.apache.ibatis.reflection.ReflectorFactory; + +public class UpdateEntity { + + private static ReflectorFactory reflectorFactory = new DefaultReflectorFactory(); + + + + public static T wrap(Class clazz) { + return ModifyAttrsRecordProxyFactory.getInstance().get(clazz); + } + + + + public static T ofNotNull(T entity) { + Class usefulClass = ClassUtil.getUsefulClass(entity.getClass()); + + T newEntity = (T) wrap(usefulClass); + + Reflector reflector = reflectorFactory.findForClass(usefulClass); + String[] propertyNames = reflector.getGetablePropertyNames(); + + for (String propertyName : propertyNames) { + try { + Object value = reflector.getGetInvoker(propertyName) + .invoke(entity, null); + if (value != null) { + reflector.getSetInvoker(propertyName).invoke(newEntity, new Object[]{value}); + } + } catch (Exception e) { + //ignore(); + } + } + + return newEntity; + } + + +} diff --git a/mybatis-flex-core/src/test/java/com/mybatisflex/test/Account.java b/mybatis-flex-core/src/test/java/com/mybatisflex/test/Account.java new file mode 100644 index 00000000..908af1d5 --- /dev/null +++ b/mybatis-flex-core/src/test/java/com/mybatisflex/test/Account.java @@ -0,0 +1,61 @@ +package com.mybatisflex.test; + +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.Table; + +import java.util.Date; + +@Table("tb_account") +public class Account { + + @Id + private Long id; + + private String userName; + + private Date birthday; + + private int sex; + + private boolean isNormal; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public Date getBirthday() { + return birthday; + } + + public void setBirthday(Date birthday) { + this.birthday = birthday; + } + + public int getSex() { + return sex; + } + + public void setSex(int sex) { + this.sex = sex; + } + + public boolean isNormal() { + return isNormal; + } + + public void setNormal(boolean normal) { + isNormal = normal; + } +} diff --git a/mybatis-flex-core/src/test/java/com/mybatisflex/test/AccountSqlTester.java b/mybatis-flex-core/src/test/java/com/mybatisflex/test/AccountSqlTester.java new file mode 100644 index 00000000..39569b0a --- /dev/null +++ b/mybatis-flex-core/src/test/java/com/mybatisflex/test/AccountSqlTester.java @@ -0,0 +1,75 @@ +package com.mybatisflex.test; + +import com.mybatisflex.core.dialect.IDialect; +import com.mybatisflex.core.dialect.CommonsDialectImpl; +import com.mybatisflex.core.querywrapper.CPI; +import com.mybatisflex.core.querywrapper.QueryWrapper; +import org.junit.Test; + +import java.util.Arrays; + +import static com.mybatisflex.core.querywrapper.QueryMethods.*; +import static com.mybatisflex.test.table.Tables.ACCOUNT; +import static com.mybatisflex.test.table.Tables.ARTICLE; + +public class AccountSqlTester { + + @Test + public void testJava(){ + StringBuilder sql = new StringBuilder("select * from xxx"); +// sql.(7," 你好 "); + System.out.println(sql); + } + + @Test + public void testSelectCountSql() { + QueryWrapper queryWrapper = QueryWrapper.create() + .select() + .from(ACCOUNT) + .where(ACCOUNT.ID.ge(100)) + .and(ACCOUNT.USER_NAME.like("michael")); + + IDialect dialect = new CommonsDialectImpl(); + String sql = dialect.forSelectCountByQuery(queryWrapper); + System.out.println(sql); + } + + + @Test + public void testrSelectLimitSql() { + QueryWrapper queryWrapper = QueryWrapper.create() + .select(ACCOUNT.ALL_COLUMNS) + .select(ARTICLE.ID.as("article_id")) + .select(distinct(ARTICLE.ID)) + .select(max(ACCOUNT.SEX)) + .select(count(distinct(ARTICLE.ID))) + .from(ACCOUNT).as("a1") +// .leftJoin(newWrapper().select().from(ARTICLE).where(ARTICLE.ID.ge(100))).as("aaa") + .leftJoin(ARTICLE).as("b1") + .on(ARTICLE.ACCOUNT_ID.eq(ACCOUNT.ID)) + .where(ACCOUNT.ID.ge(select(ARTICLE.ID).from(ARTICLE).as("cc").where(ARTICLE.ID.eq(111)))) + .and((true ? noCondition() : ARTICLE.ID.ge(22211)).and(ACCOUNT.ID.eq(10011)).when(false)) + .and(ACCOUNT.USER_NAME.like("michael")) + .and(ARTICLE.ID.in(select(ARTICLE.ID).from("aaa"))) + .and( + notExist( + selectOne().from("aaa").where(ARTICLE.ID.ge(333)) + ) + ) + .groupBy(ACCOUNT.ID).having(ARTICLE.ID.ge(0)) +// .and("bbb.id > ?",100) + .orderBy(ACCOUNT.ID.desc()) + .limit(10, 10); + + String mysqlSql = new CommonsDialectImpl().forSelectListByQuery(queryWrapper); + System.out.println(">>>>> mysql: \n" + mysqlSql); + System.out.println(">>>>> mysql: \n" + Arrays.toString(CPI.getValueArray(queryWrapper))); + +// String oracleSql = new OracleDialect().forSelectListByQuery(CPI.getQueryTable(queryWrapper).getName(), queryWrapper); +// System.out.println(">>>>> oracle: " + oracleSql); +// +// String informixSql = new InformixDialect().forSelectListByQuery(CPI.getQueryTable(queryWrapper).getName(), queryWrapper); +// System.out.println(">>>>> informix: " + informixSql); + } + +} diff --git a/mybatis-flex-core/src/test/java/com/mybatisflex/test/Article.java b/mybatis-flex-core/src/test/java/com/mybatisflex/test/Article.java new file mode 100644 index 00000000..aa3f02b2 --- /dev/null +++ b/mybatis-flex-core/src/test/java/com/mybatisflex/test/Article.java @@ -0,0 +1,29 @@ +package com.mybatisflex.test; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.Table; +import com.mybatisflex.core.enums.KeyType; + +import java.util.Date; + +@Table("tb_article") +public class Article { + + @Id(keyType = KeyType.Auto) + private Long id; + + private Long accountId; + + private String title; + + @Column(isLarge = true) + private String content; + + @Column(insert = "now()") + private Date created; + + @Column(update = "now()") + private Date modified; + +} diff --git a/mybatis-flex-spring-boot-starter/pom.xml b/mybatis-flex-spring-boot-starter/pom.xml new file mode 100644 index 00000000..f4269461 --- /dev/null +++ b/mybatis-flex-spring-boot-starter/pom.xml @@ -0,0 +1,67 @@ + + + + parent + com.mybatis-flex + 1.0.0-beta.1 + + 4.0.0 + + mybatis-flex-spring-boot-starter + + + 8 + 8 + + + + + + com.mybatis-flex + mybatis-flex-spring + 1.0.0-beta.1 + + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.mybatis.scripting + mybatis-freemarker + true + + + + org.mybatis.scripting + mybatis-velocity + true + + + + org.mybatis.scripting + mybatis-thymeleaf + true + + + + + + + + src/main/resources + + **/* + + + + *.properties + + + + + + \ No newline at end of file diff --git a/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/ConfigurationCustomizer.java b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/ConfigurationCustomizer.java new file mode 100644 index 00000000..c028c1d0 --- /dev/null +++ b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/ConfigurationCustomizer.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.spring.boot; + +import com.mybatisflex.core.mybatis.FlexConfiguration; + +/** + * 为 FlexConfiguration 做自定义的配置支持 {@link FlexConfiguration} + */ +@FunctionalInterface +public interface ConfigurationCustomizer { + + void customize(FlexConfiguration configuration); + +} diff --git a/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/DbAutoConfiguration.java b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/DbAutoConfiguration.java new file mode 100644 index 00000000..fa32c576 --- /dev/null +++ b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/DbAutoConfiguration.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.spring.boot; + +import com.mybatisflex.core.row.Db; +import com.mybatisflex.spring.SpringRowSessionManager; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Configuration; + +@ConditionalOnClass(Db.class) +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter({MybatisFlexAutoConfiguration.class}) +public class DbAutoConfiguration { + + public DbAutoConfiguration() { + Db.invoker().setRowSessionManager(new SpringRowSessionManager()); + } +} diff --git a/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MybatisFlexAutoConfiguration.java b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MybatisFlexAutoConfiguration.java new file mode 100644 index 00000000..407283a6 --- /dev/null +++ b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MybatisFlexAutoConfiguration.java @@ -0,0 +1,325 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.spring.boot; + +import com.mybatisflex.core.mybatis.FlexConfiguration; +import com.mybatisflex.spring.FlexSqlSessionFactoryBean; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.mapping.DatabaseIdProvider; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.scripting.LanguageDriver; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.type.TypeHandler; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.SqlSessionTemplate; +import org.mybatis.spring.mapper.MapperFactoryBean; +import org.mybatis.spring.mapper.MapperScannerConfigurer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.factory.*; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.env.Environment; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import javax.sql.DataSource; +import java.beans.PropertyDescriptor; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +/** + * Mybatis-Flex 的核心配置 + *

+ *

+ * 参考 {@link + * https://github.com/mybatis/spring-boot-starter/blob/master/mybatis-spring-boot-autoconfigure/src/main/java/org/mybatis/spring/boot/autoconfigure/MybatisAutoConfiguration.java} + *

+ * 为 Mybatis-Flex 开启自动配置功能,主要修改一下几个方面: + *

+ * 1、替换配置为 mybatis-flex 的配置前缀 + * 2、修改 SqlSessionFactory 为 FlexSqlSessionFactoryBean + * 3、修改 Configuration 为 FlexConfiguration + */ +@org.springframework.context.annotation.Configuration(proxyBeanMethods = false) +@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}) +@ConditionalOnSingleCandidate(DataSource.class) +@EnableConfigurationProperties(MybatisFlexProperties.class) +@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class}) +public class MybatisFlexAutoConfiguration implements InitializingBean { + + private static final Logger logger = LoggerFactory.getLogger(MybatisFlexAutoConfiguration.class); + + private final MybatisFlexProperties properties; + + private final Interceptor[] interceptors; + + private final TypeHandler[] typeHandlers; + + private final LanguageDriver[] languageDrivers; + + private final ResourceLoader resourceLoader; + + private final DatabaseIdProvider databaseIdProvider; + + private final List configurationCustomizers; + + private final List sqlSessionFactoryBeanCustomizers; + + public MybatisFlexAutoConfiguration(MybatisFlexProperties properties, ObjectProvider interceptorsProvider, + ObjectProvider typeHandlersProvider, ObjectProvider languageDriversProvider, + ResourceLoader resourceLoader, ObjectProvider databaseIdProvider, + ObjectProvider> configurationCustomizersProvider, + ObjectProvider> sqlSessionFactoryBeanCustomizers) { + this.properties = properties; + this.interceptors = interceptorsProvider.getIfAvailable(); + this.typeHandlers = typeHandlersProvider.getIfAvailable(); + this.languageDrivers = languageDriversProvider.getIfAvailable(); + this.resourceLoader = resourceLoader; + this.databaseIdProvider = databaseIdProvider.getIfAvailable(); + this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable(); + this.sqlSessionFactoryBeanCustomizers = sqlSessionFactoryBeanCustomizers.getIfAvailable(); + } + + @Override + public void afterPropertiesSet() { + checkConfigFileExists(); + } + + private void checkConfigFileExists() { + if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) { + Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation()); + Assert.state(resource.exists(), + "Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)"); + } + } + + @Bean + @ConditionalOnMissingBean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { +// SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); + SqlSessionFactoryBean factory = new FlexSqlSessionFactoryBean(); + factory.setDataSource(dataSource); + if (properties.getConfiguration() == null || properties.getConfiguration().getVfsImpl() == null) { + factory.setVfs(SpringBootVFS.class); + } + if (StringUtils.hasText(this.properties.getConfigLocation())) { + factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation())); + } + applyConfiguration(factory); + if (this.properties.getConfigurationProperties() != null) { + factory.setConfigurationProperties(this.properties.getConfigurationProperties()); + } + if (!ObjectUtils.isEmpty(this.interceptors)) { + factory.setPlugins(this.interceptors); + } + if (this.databaseIdProvider != null) { + factory.setDatabaseIdProvider(this.databaseIdProvider); + } + if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) { + factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage()); + } + if (this.properties.getTypeAliasesSuperType() != null) { + factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType()); + } + if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) { + factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage()); + } + if (!ObjectUtils.isEmpty(this.typeHandlers)) { + factory.setTypeHandlers(this.typeHandlers); + } + Resource[] mapperLocations = this.properties.resolveMapperLocations(); + if (!ObjectUtils.isEmpty(mapperLocations)) { + factory.setMapperLocations(mapperLocations); + } + Set factoryPropertyNames = Stream + .of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName) + .collect(Collectors.toSet()); + Class defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver(); + if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) { + // Need to mybatis-spring 2.0.2+ + factory.setScriptingLanguageDrivers(this.languageDrivers); + if (defaultLanguageDriver == null && this.languageDrivers.length == 1) { + defaultLanguageDriver = this.languageDrivers[0].getClass(); + } + } + if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) { + // Need to mybatis-spring 2.0.2+ + factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver); + } + applySqlSessionFactoryBeanCustomizers(factory); + return factory.getObject(); + } + + private void applyConfiguration(SqlSessionFactoryBean factory) { + MybatisFlexProperties.CoreConfiguration coreConfiguration = this.properties.getConfiguration(); +// Configuration configuration = null; + FlexConfiguration configuration = null; + if (coreConfiguration != null || !StringUtils.hasText(this.properties.getConfigLocation())) { + configuration = new FlexConfiguration(); + } + if (configuration != null && coreConfiguration != null) { + coreConfiguration.applyTo(configuration); + } + if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) { + for (ConfigurationCustomizer customizer : this.configurationCustomizers) { + customizer.customize(configuration); + } + } + factory.setConfiguration(configuration); + } + + private void applySqlSessionFactoryBeanCustomizers(SqlSessionFactoryBean factory) { + if (!CollectionUtils.isEmpty(this.sqlSessionFactoryBeanCustomizers)) { + for (SqlSessionFactoryBeanCustomizer customizer : this.sqlSessionFactoryBeanCustomizers) { + customizer.customize(factory); + } + } + } + + @Bean + @ConditionalOnMissingBean + public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { + ExecutorType executorType = this.properties.getExecutorType(); + if (executorType != null) { + return new SqlSessionTemplate(sqlSessionFactory, executorType); + } else { + return new SqlSessionTemplate(sqlSessionFactory); + } + } + + /** + * This will just scan the same base package as Spring Boot does. If you want more power, you can explicitly use + * {@link org.mybatis.spring.annotation.MapperScan} but this will get typed mappers working correctly, out-of-the-box, + * similar to using Spring Data JPA repositories. + */ + public static class AutoConfiguredMapperScannerRegistrar + implements BeanFactoryAware, EnvironmentAware, ImportBeanDefinitionRegistrar { + + private BeanFactory beanFactory; + private Environment environment; + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + + if (!AutoConfigurationPackages.has(this.beanFactory)) { + logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled."); + return; + } + + logger.debug("Searching for mappers annotated with @Mapper"); + + List packages = AutoConfigurationPackages.get(this.beanFactory); + if (logger.isDebugEnabled()) { + packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg)); + } + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class); + builder.addPropertyValue("processPropertyPlaceHolders", true); + builder.addPropertyValue("annotationClass", Mapper.class); + builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages)); + BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class); + Set propertyNames = Stream.of(beanWrapper.getPropertyDescriptors()).map(PropertyDescriptor::getName) + .collect(Collectors.toSet()); + if (propertyNames.contains("lazyInitialization")) { + // Need to mybatis-spring 2.0.2+ + builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}"); + } + if (propertyNames.contains("defaultScope")) { + // Need to mybatis-spring 2.0.6+ + builder.addPropertyValue("defaultScope", "${mybatis.mapper-default-scope:}"); + } + + // for spring-native + boolean injectSqlSession = environment.getProperty("mybatis.inject-sql-session-on-mapper-scan", Boolean.class, + Boolean.TRUE); + if (injectSqlSession && this.beanFactory instanceof ListableBeanFactory) { + ListableBeanFactory listableBeanFactory = (ListableBeanFactory) this.beanFactory; + Optional sqlSessionTemplateBeanName = Optional + .ofNullable(getBeanNameForType(SqlSessionTemplate.class, listableBeanFactory)); + Optional sqlSessionFactoryBeanName = Optional + .ofNullable(getBeanNameForType(SqlSessionFactory.class, listableBeanFactory)); + if (sqlSessionTemplateBeanName.isPresent() || !sqlSessionFactoryBeanName.isPresent()) { + builder.addPropertyValue("sqlSessionTemplateBeanName", + sqlSessionTemplateBeanName.orElse("sqlSessionTemplate")); + } else { + builder.addPropertyValue("sqlSessionFactoryBeanName", sqlSessionFactoryBeanName.get()); + } + } + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition()); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + private String getBeanNameForType(Class type, ListableBeanFactory factory) { + String[] beanNames = factory.getBeanNamesForType(type); + return beanNames.length > 0 ? beanNames[0] : null; + } + + } + + /** + * If mapper registering configuration or mapper scanning configuration not present, this configuration allow to scan + * mappers based on the same component-scanning path as Spring Boot itself. + */ + @org.springframework.context.annotation.Configuration(proxyBeanMethods = false) + @Import(AutoConfiguredMapperScannerRegistrar.class) + @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class}) + public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean { + + @Override + public void afterPropertiesSet() { + logger.debug( + "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer."); + } + + } + +} diff --git a/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MybatisFlexDependsOnDatabaseInitializationDetector.java b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MybatisFlexDependsOnDatabaseInitializationDetector.java new file mode 100644 index 00000000..90981229 --- /dev/null +++ b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MybatisFlexDependsOnDatabaseInitializationDetector.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.spring.boot; + +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDependsOnDatabaseInitializationDetector; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector; + +import java.util.Collections; +import java.util.Set; + +/** + * {@link DependsOnDatabaseInitializationDetector} for Mybatis-Flex. + */ +class MybatisFlexDependsOnDatabaseInitializationDetector + extends AbstractBeansOfTypeDependsOnDatabaseInitializationDetector { + + @Override + protected Set> getDependsOnDatabaseInitializationBeanTypes() { + return Collections.singleton(SqlSessionTemplate.class); + } + +} diff --git a/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MybatisFlexProperties.java b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MybatisFlexProperties.java new file mode 100644 index 00000000..aab26d17 --- /dev/null +++ b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MybatisFlexProperties.java @@ -0,0 +1,690 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.spring.boot; + +import org.apache.ibatis.io.VFS; +import org.apache.ibatis.logging.Log; +import org.apache.ibatis.mapping.ResultSetType; +import org.apache.ibatis.scripting.LanguageDriver; +import org.apache.ibatis.session.*; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.TypeHandler; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; + +import java.io.IOException; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Stream; + +/** + * Mybatis-Flex 的配置属性 + * 设置 mapUnderscoreToCamelCase 默认值为 true + */ +@ConfigurationProperties(prefix = "mybatis-flex") +public class MybatisFlexProperties { + + + private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); + + /** + * Location of MyBatis xml config file. + */ + private String configLocation; + + /** + * Locations of MyBatis mapper files. + */ + private String[] mapperLocations; + + /** + * Packages to search type aliases. (Package delimiters are ",; \t\n") + */ + private String typeAliasesPackage; + + /** + * The super class for filtering type alias. If this not specifies, the MyBatis deal as type alias all classes that + * searched from typeAliasesPackage. + */ + private Class typeAliasesSuperType; + + /** + * Packages to search for type handlers. (Package delimiters are ",; \t\n") + */ + private String typeHandlersPackage; + + /** + * Indicates whether perform presence check of the MyBatis xml config file. + */ + private boolean checkConfigLocation = false; + + /** + * Execution mode for {@link org.mybatis.spring.SqlSessionTemplate}. + */ + private ExecutorType executorType; + + /** + * The default scripting language driver class. (Available when use together with mybatis-spring 2.0.2+) + */ + private Class defaultScriptingLanguageDriver; + + /** + * Externalized properties for MyBatis configuration. + */ + private Properties configurationProperties; + + /** + * A Configuration object for customize default settings. If {@link #configLocation} is specified, this property is + * not used. + */ + private CoreConfiguration configuration; + + /** + * @since 1.1.0 + */ + public String getConfigLocation() { + return this.configLocation; + } + + /** + * @since 1.1.0 + */ + public void setConfigLocation(String configLocation) { + this.configLocation = configLocation; + } + + public String[] getMapperLocations() { + return this.mapperLocations; + } + + public void setMapperLocations(String[] mapperLocations) { + this.mapperLocations = mapperLocations; + } + + public String getTypeHandlersPackage() { + return this.typeHandlersPackage; + } + + public void setTypeHandlersPackage(String typeHandlersPackage) { + this.typeHandlersPackage = typeHandlersPackage; + } + + public String getTypeAliasesPackage() { + return this.typeAliasesPackage; + } + + public void setTypeAliasesPackage(String typeAliasesPackage) { + this.typeAliasesPackage = typeAliasesPackage; + } + + /** + * @since 1.3.3 + */ + public Class getTypeAliasesSuperType() { + return typeAliasesSuperType; + } + + /** + * @since 1.3.3 + */ + public void setTypeAliasesSuperType(Class typeAliasesSuperType) { + this.typeAliasesSuperType = typeAliasesSuperType; + } + + public boolean isCheckConfigLocation() { + return this.checkConfigLocation; + } + + public void setCheckConfigLocation(boolean checkConfigLocation) { + this.checkConfigLocation = checkConfigLocation; + } + + public ExecutorType getExecutorType() { + return this.executorType; + } + + public void setExecutorType(ExecutorType executorType) { + this.executorType = executorType; + } + + /** + * @since 2.1.0 + */ + public Class getDefaultScriptingLanguageDriver() { + return defaultScriptingLanguageDriver; + } + + /** + * @since 2.1.0 + */ + public void setDefaultScriptingLanguageDriver(Class defaultScriptingLanguageDriver) { + this.defaultScriptingLanguageDriver = defaultScriptingLanguageDriver; + } + + /** + * @since 1.2.0 + */ + public Properties getConfigurationProperties() { + return configurationProperties; + } + + /** + * @since 1.2.0 + */ + public void setConfigurationProperties(Properties configurationProperties) { + this.configurationProperties = configurationProperties; + } + + public CoreConfiguration getConfiguration() { + return configuration; + } + + public void setConfiguration(CoreConfiguration configuration) { + this.configuration = configuration; + } + + public Resource[] resolveMapperLocations() { + return Stream.of(Optional.ofNullable(this.mapperLocations).orElse(new String[0])) + .flatMap(location -> Stream.of(getResources(location))).toArray(Resource[]::new); + } + + private Resource[] getResources(String location) { + try { + return resourceResolver.getResources(location); + } catch (IOException e) { + return new Resource[0]; + } + } + + /** + * The configuration properties for mybatis core module. + * + * @since 3.0.0 + */ + public static class CoreConfiguration { + + /** + * Allows using RowBounds on nested statements. If allow, set the false. Default is false. + */ + private Boolean safeRowBoundsEnabled; + + /** + * Allows using ResultHandler on nested statements. If allow, set the false. Default is true. + */ + private Boolean safeResultHandlerEnabled; + + /** + * Enables automatic mapping from classic database column names A_COLUMN to camel case classic Java property names + * aColumn. Default is true. + */ + private Boolean mapUnderscoreToCamelCase = true; + + /** + * When enabled, any method call will load all the lazy properties of the object. Otherwise, each property is loaded + * on demand (see also lazyLoadTriggerMethods). Default is false. + */ + private Boolean aggressiveLazyLoading; + + /** + * Allows or disallows multiple ResultSets to be returned from a single statement (compatible driver required). + * Default is true. + */ + private Boolean multipleResultSetsEnabled; + + /** + * Allows JDBC support for generated keys. A compatible driver is required. This setting forces generated keys to be + * used if set to true, as some drivers deny compatibility but still work (e.g. Derby). Default is false. + */ + private Boolean useGeneratedKeys; + + /** + * Uses the column label instead of the column name. Different drivers behave differently in this respect. Refer to + * the driver documentation, or test out both modes to determine how your driver behaves. Default is true. + */ + private Boolean useColumnLabel; + + /** + * Globally enables or disables any caches configured in any mapper under this configuration. Default is true. + */ + private Boolean cacheEnabled; + + /** + * Specifies if setters or map's put method will be called when a retrieved value is null. It is useful when you + * rely on Map.keySet() or null value initialization. Note primitives such as (int,boolean,etc.) will not be set to + * null. Default is false. + */ + private Boolean callSettersOnNulls; + + /** + * Allow referencing statement parameters by their actual names declared in the method signature. To use this + * feature, your project must be compiled in Java 8 with -parameters option. Default is true. + */ + private Boolean useActualParamName; + + /** + * MyBatis, by default, returns null when all the columns of a returned row are NULL. When this setting is enabled, + * MyBatis returns an empty instance instead. Note that it is also applied to nested results (i.e. collectioin and + * association). Default is false. + */ + private Boolean returnInstanceForEmptyRow; + + /** + * Removes extra whitespace characters from the SQL. Note that this also affects literal strings in SQL. Default is + * false. + */ + private Boolean shrinkWhitespacesInSql; + + /** + * Specifies the default value of 'nullable' attribute on 'foreach' tag. Default is false. + */ + private Boolean nullableOnForEach; + + /** + * When applying constructor auto-mapping, argument name is used to search the column to map instead of relying on + * the column order. Default is false. + */ + private Boolean argNameBasedConstructorAutoMapping; + + /** + * Globally enables or disables lazy loading. When enabled, all relations will be lazily loaded. This value can be + * superseded for a specific relation by using the fetchType attribute on it. Default is False. + */ + private Boolean lazyLoadingEnabled; + + /** + * Sets the number of seconds the driver will wait for a response from the database. + */ + private Integer defaultStatementTimeout; + + /** + * Sets the driver a hint as to control fetching size for return results. This parameter value can be override by a + * query setting. + */ + private Integer defaultFetchSize; + + /** + * MyBatis uses local cache to prevent circular references and speed up repeated nested queries. By default + * (SESSION) all queries executed during a session are cached. If localCacheScope=STATEMENT local session will be + * used just for statement execution, no data will be shared between two different calls to the same SqlSession. + * Default is SESSION. + */ + private LocalCacheScope localCacheScope; + + /** + * Specifies the JDBC type for null values when no specific JDBC type was provided for the parameter. Some drivers + * require specifying the column JDBC type but others work with generic values like NULL, VARCHAR or OTHER. Default + * is OTHER. + */ + private JdbcType jdbcTypeForNull; + + /** + * Specifies a scroll strategy when omit it per statement settings. + */ + private ResultSetType defaultResultSetType; + + /** + * Configures the default executor. SIMPLE executor does nothing special. REUSE executor reuses prepared statements. + * BATCH executor reuses statements and batches updates. Default is SIMPLE. + */ + private ExecutorType defaultExecutorType; + + /** + * Specifies if and how MyBatis should automatically map columns to fields/properties. NONE disables auto-mapping. + * PARTIAL will only auto-map results with no nested result mappings defined inside. FULL will auto-map result + * mappings of any complexity (containing nested or otherwise). Default is PARTIAL. + */ + private AutoMappingBehavior autoMappingBehavior; + + /** + * Specify the behavior when detects an unknown column (or unknown property type) of automatic mapping target. + * Default is NONE. + */ + private AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior; + + /** + * Specifies the prefix string that MyBatis will add to the logger names. + */ + private String logPrefix; + + /** + * Specifies which Object's methods trigger a lazy load. Default is [equals,clone,hashCode,toString]. + */ + private Set lazyLoadTriggerMethods; + + /** + * Specifies which logging implementation MyBatis should use. If this setting is not present logging implementation + * will be autodiscovered. + */ + private Class logImpl; + + /** + * Specifies VFS implementations. + */ + private Class vfsImpl; + + /** + * Specifies an sql provider class that holds provider method. This class apply to the type(or value) attribute on + * sql provider annotation(e.g. @SelectProvider), when these attribute was omitted. + */ + private Class defaultSqlProviderType; + + /** + * Specifies the TypeHandler used by default for Enum. + */ + Class defaultEnumTypeHandler; + + /** + * Specifies the class that provides an instance of Configuration. The returned Configuration instance is used to + * load lazy properties of deserialized objects. This class must have a method with a signature static Configuration + * getConfiguration(). + */ + private Class configurationFactory; + + /** + * Specify any configuration variables. + */ + private Properties variables; + + public Boolean getSafeRowBoundsEnabled() { + return safeRowBoundsEnabled; + } + + public void setSafeRowBoundsEnabled(Boolean safeRowBoundsEnabled) { + this.safeRowBoundsEnabled = safeRowBoundsEnabled; + } + + public Boolean getSafeResultHandlerEnabled() { + return safeResultHandlerEnabled; + } + + public void setSafeResultHandlerEnabled(Boolean safeResultHandlerEnabled) { + this.safeResultHandlerEnabled = safeResultHandlerEnabled; + } + + public Boolean getMapUnderscoreToCamelCase() { + return mapUnderscoreToCamelCase; + } + + public void setMapUnderscoreToCamelCase(Boolean mapUnderscoreToCamelCase) { + this.mapUnderscoreToCamelCase = mapUnderscoreToCamelCase; + } + + public Boolean getAggressiveLazyLoading() { + return aggressiveLazyLoading; + } + + public void setAggressiveLazyLoading(Boolean aggressiveLazyLoading) { + this.aggressiveLazyLoading = aggressiveLazyLoading; + } + + public Boolean getMultipleResultSetsEnabled() { + return multipleResultSetsEnabled; + } + + public void setMultipleResultSetsEnabled(Boolean multipleResultSetsEnabled) { + this.multipleResultSetsEnabled = multipleResultSetsEnabled; + } + + public Boolean getUseGeneratedKeys() { + return useGeneratedKeys; + } + + public void setUseGeneratedKeys(Boolean useGeneratedKeys) { + this.useGeneratedKeys = useGeneratedKeys; + } + + public Boolean getUseColumnLabel() { + return useColumnLabel; + } + + public void setUseColumnLabel(Boolean useColumnLabel) { + this.useColumnLabel = useColumnLabel; + } + + public Boolean getCacheEnabled() { + return cacheEnabled; + } + + public void setCacheEnabled(Boolean cacheEnabled) { + this.cacheEnabled = cacheEnabled; + } + + public Boolean getCallSettersOnNulls() { + return callSettersOnNulls; + } + + public void setCallSettersOnNulls(Boolean callSettersOnNulls) { + this.callSettersOnNulls = callSettersOnNulls; + } + + public Boolean getUseActualParamName() { + return useActualParamName; + } + + public void setUseActualParamName(Boolean useActualParamName) { + this.useActualParamName = useActualParamName; + } + + public Boolean getReturnInstanceForEmptyRow() { + return returnInstanceForEmptyRow; + } + + public void setReturnInstanceForEmptyRow(Boolean returnInstanceForEmptyRow) { + this.returnInstanceForEmptyRow = returnInstanceForEmptyRow; + } + + public Boolean getShrinkWhitespacesInSql() { + return shrinkWhitespacesInSql; + } + + public void setShrinkWhitespacesInSql(Boolean shrinkWhitespacesInSql) { + this.shrinkWhitespacesInSql = shrinkWhitespacesInSql; + } + + public Boolean getNullableOnForEach() { + return nullableOnForEach; + } + + public void setNullableOnForEach(Boolean nullableOnForEach) { + this.nullableOnForEach = nullableOnForEach; + } + + public Boolean getArgNameBasedConstructorAutoMapping() { + return argNameBasedConstructorAutoMapping; + } + + public void setArgNameBasedConstructorAutoMapping(Boolean argNameBasedConstructorAutoMapping) { + this.argNameBasedConstructorAutoMapping = argNameBasedConstructorAutoMapping; + } + + public String getLogPrefix() { + return logPrefix; + } + + public void setLogPrefix(String logPrefix) { + this.logPrefix = logPrefix; + } + + public Class getLogImpl() { + return logImpl; + } + + public void setLogImpl(Class logImpl) { + this.logImpl = logImpl; + } + + public Class getVfsImpl() { + return vfsImpl; + } + + public void setVfsImpl(Class vfsImpl) { + this.vfsImpl = vfsImpl; + } + + public Class getDefaultSqlProviderType() { + return defaultSqlProviderType; + } + + public void setDefaultSqlProviderType(Class defaultSqlProviderType) { + this.defaultSqlProviderType = defaultSqlProviderType; + } + + public LocalCacheScope getLocalCacheScope() { + return localCacheScope; + } + + public void setLocalCacheScope(LocalCacheScope localCacheScope) { + this.localCacheScope = localCacheScope; + } + + public JdbcType getJdbcTypeForNull() { + return jdbcTypeForNull; + } + + public void setJdbcTypeForNull(JdbcType jdbcTypeForNull) { + this.jdbcTypeForNull = jdbcTypeForNull; + } + + public Set getLazyLoadTriggerMethods() { + return lazyLoadTriggerMethods; + } + + public void setLazyLoadTriggerMethods(Set lazyLoadTriggerMethods) { + this.lazyLoadTriggerMethods = lazyLoadTriggerMethods; + } + + public Integer getDefaultStatementTimeout() { + return defaultStatementTimeout; + } + + public void setDefaultStatementTimeout(Integer defaultStatementTimeout) { + this.defaultStatementTimeout = defaultStatementTimeout; + } + + public Integer getDefaultFetchSize() { + return defaultFetchSize; + } + + public void setDefaultFetchSize(Integer defaultFetchSize) { + this.defaultFetchSize = defaultFetchSize; + } + + public ResultSetType getDefaultResultSetType() { + return defaultResultSetType; + } + + public void setDefaultResultSetType(ResultSetType defaultResultSetType) { + this.defaultResultSetType = defaultResultSetType; + } + + public ExecutorType getDefaultExecutorType() { + return defaultExecutorType; + } + + public void setDefaultExecutorType(ExecutorType defaultExecutorType) { + this.defaultExecutorType = defaultExecutorType; + } + + public AutoMappingBehavior getAutoMappingBehavior() { + return autoMappingBehavior; + } + + public void setAutoMappingBehavior(AutoMappingBehavior autoMappingBehavior) { + this.autoMappingBehavior = autoMappingBehavior; + } + + public AutoMappingUnknownColumnBehavior getAutoMappingUnknownColumnBehavior() { + return autoMappingUnknownColumnBehavior; + } + + public void setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior) { + this.autoMappingUnknownColumnBehavior = autoMappingUnknownColumnBehavior; + } + + public Properties getVariables() { + return variables; + } + + public void setVariables(Properties variables) { + this.variables = variables; + } + + public Boolean getLazyLoadingEnabled() { + return lazyLoadingEnabled; + } + + public void setLazyLoadingEnabled(Boolean lazyLoadingEnabled) { + this.lazyLoadingEnabled = lazyLoadingEnabled; + } + + public Class getConfigurationFactory() { + return configurationFactory; + } + + public void setConfigurationFactory(Class configurationFactory) { + this.configurationFactory = configurationFactory; + } + + public Class getDefaultEnumTypeHandler() { + return defaultEnumTypeHandler; + } + + public void setDefaultEnumTypeHandler(Class defaultEnumTypeHandler) { + this.defaultEnumTypeHandler = defaultEnumTypeHandler; + } + + void applyTo(Configuration target) { + PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); + mapper.from(getSafeRowBoundsEnabled()).to(target::setSafeRowBoundsEnabled); + mapper.from(getSafeResultHandlerEnabled()).to(target::setSafeResultHandlerEnabled); + mapper.from(getMapUnderscoreToCamelCase()).to(target::setMapUnderscoreToCamelCase); + mapper.from(getAggressiveLazyLoading()).to(target::setAggressiveLazyLoading); + mapper.from(getMultipleResultSetsEnabled()).to(target::setMultipleResultSetsEnabled); + mapper.from(getUseGeneratedKeys()).to(target::setUseGeneratedKeys); + mapper.from(getUseColumnLabel()).to(target::setUseColumnLabel); + mapper.from(getCacheEnabled()).to(target::setCacheEnabled); + mapper.from(getCallSettersOnNulls()).to(target::setCallSettersOnNulls); + mapper.from(getUseActualParamName()).to(target::setUseActualParamName); + mapper.from(getReturnInstanceForEmptyRow()).to(target::setReturnInstanceForEmptyRow); + mapper.from(getShrinkWhitespacesInSql()).to(target::setShrinkWhitespacesInSql); + mapper.from(getNullableOnForEach()).to(target::setNullableOnForEach); + mapper.from(getArgNameBasedConstructorAutoMapping()).to(target::setArgNameBasedConstructorAutoMapping); + mapper.from(getLazyLoadingEnabled()).to(target::setLazyLoadingEnabled); + mapper.from(getLogPrefix()).to(target::setLogPrefix); + mapper.from(getLazyLoadTriggerMethods()).to(target::setLazyLoadTriggerMethods); + mapper.from(getDefaultStatementTimeout()).to(target::setDefaultStatementTimeout); + mapper.from(getDefaultFetchSize()).to(target::setDefaultFetchSize); + mapper.from(getLocalCacheScope()).to(target::setLocalCacheScope); + mapper.from(getJdbcTypeForNull()).to(target::setJdbcTypeForNull); + mapper.from(getDefaultResultSetType()).to(target::setDefaultResultSetType); + mapper.from(getDefaultExecutorType()).to(target::setDefaultExecutorType); + mapper.from(getAutoMappingBehavior()).to(target::setAutoMappingBehavior); + mapper.from(getAutoMappingUnknownColumnBehavior()).to(target::setAutoMappingUnknownColumnBehavior); + mapper.from(getVariables()).to(target::setVariables); + mapper.from(getLogImpl()).to(target::setLogImpl); + mapper.from(getVfsImpl()).to(target::setVfsImpl); + mapper.from(getDefaultSqlProviderType()).to(target::setDefaultSqlProviderType); + mapper.from(getConfigurationFactory()).to(target::setConfigurationFactory); + mapper.from(getDefaultEnumTypeHandler()).to(target::setDefaultEnumTypeHandler); + } + + } + +} diff --git a/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MybatisLanguageDriverAutoConfiguration.java b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MybatisLanguageDriverAutoConfiguration.java new file mode 100644 index 00000000..72f17369 --- /dev/null +++ b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/MybatisLanguageDriverAutoConfiguration.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.spring.boot; + +import org.apache.ibatis.scripting.LanguageDriver; +import org.mybatis.scripting.freemarker.FreeMarkerLanguageDriver; +import org.mybatis.scripting.freemarker.FreeMarkerLanguageDriverConfig; +import org.mybatis.scripting.thymeleaf.ThymeleafLanguageDriver; +import org.mybatis.scripting.thymeleaf.ThymeleafLanguageDriverConfig; +import org.mybatis.scripting.velocity.VelocityLanguageDriver; +import org.mybatis.scripting.velocity.VelocityLanguageDriverConfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 脚本语言驱动的自动配置,平常一般项目用不到,只为了同步 mybatis 自带的 MybatisLanguageDriverAutoConfiguration + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(LanguageDriver.class) +public class MybatisLanguageDriverAutoConfiguration { + + private static final String CONFIGURATION_PROPERTY_PREFIX = "mybatis-flex.scripting-language-driver"; + + /** + * Configuration class for mybatis-freemarker 1.1.x or under. + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(FreeMarkerLanguageDriver.class) + @ConditionalOnMissingClass("org.mybatis.scripting.freemarker.FreeMarkerLanguageDriverConfig") + public static class LegacyFreeMarkerConfiguration { + @Bean + @ConditionalOnMissingBean + FreeMarkerLanguageDriver freeMarkerLanguageDriver() { + return new FreeMarkerLanguageDriver(); + } + } + + /** + * Configuration class for mybatis-freemarker 1.2.x or above. + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ FreeMarkerLanguageDriver.class, FreeMarkerLanguageDriverConfig.class }) + public static class FreeMarkerConfiguration { + @Bean + @ConditionalOnMissingBean + FreeMarkerLanguageDriver freeMarkerLanguageDriver(FreeMarkerLanguageDriverConfig config) { + return new FreeMarkerLanguageDriver(config); + } + + @Bean + @ConditionalOnMissingBean + @ConfigurationProperties(CONFIGURATION_PROPERTY_PREFIX + ".freemarker") + public FreeMarkerLanguageDriverConfig freeMarkerLanguageDriverConfig() { + return FreeMarkerLanguageDriverConfig.newInstance(); + } + } + + /** + * Configuration class for mybatis-velocity 2.0 or under. + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(org.mybatis.scripting.velocity.Driver.class) + @ConditionalOnMissingClass("org.mybatis.scripting.velocity.VelocityLanguageDriverConfig") + @SuppressWarnings("deprecation") + public static class LegacyVelocityConfiguration { + @Bean + @ConditionalOnMissingBean + org.mybatis.scripting.velocity.Driver velocityLanguageDriver() { + return new org.mybatis.scripting.velocity.Driver(); + } + } + + /** + * Configuration class for mybatis-velocity 2.1.x or above. + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ VelocityLanguageDriver.class, VelocityLanguageDriverConfig.class }) + public static class VelocityConfiguration { + @Bean + @ConditionalOnMissingBean + VelocityLanguageDriver velocityLanguageDriver(VelocityLanguageDriverConfig config) { + return new VelocityLanguageDriver(config); + } + + @Bean + @ConditionalOnMissingBean + @ConfigurationProperties(CONFIGURATION_PROPERTY_PREFIX + ".velocity") + public VelocityLanguageDriverConfig velocityLanguageDriverConfig() { + return VelocityLanguageDriverConfig.newInstance(); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ThymeleafLanguageDriver.class) + public static class ThymeleafConfiguration { + @Bean + @ConditionalOnMissingBean + ThymeleafLanguageDriver thymeleafLanguageDriver(ThymeleafLanguageDriverConfig config) { + return new ThymeleafLanguageDriver(config); + } + + @Bean + @ConditionalOnMissingBean + @ConfigurationProperties(CONFIGURATION_PROPERTY_PREFIX + ".thymeleaf") + public ThymeleafLanguageDriverConfig thymeleafLanguageDriverConfig() { + return ThymeleafLanguageDriverConfig.newInstance(); + } + + // This class provides to avoid the https://github.com/spring-projects/spring-boot/issues/21626 as workaround. + @SuppressWarnings("unused") + private static class MetadataThymeleafLanguageDriverConfig extends ThymeleafLanguageDriverConfig { + + @ConfigurationProperties(CONFIGURATION_PROPERTY_PREFIX + ".thymeleaf.dialect") + @Override + public DialectConfig getDialect() { + return super.getDialect(); + } + + @ConfigurationProperties(CONFIGURATION_PROPERTY_PREFIX + ".thymeleaf.template-file") + @Override + public TemplateFileConfig getTemplateFile() { + return super.getTemplateFile(); + } + + } + + } + +} diff --git a/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/SpringBootVFS.java b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/SpringBootVFS.java new file mode 100644 index 00000000..067954bf --- /dev/null +++ b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/SpringBootVFS.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.spring.boot; + +import org.apache.ibatis.io.VFS; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.text.Normalizer; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Mybatis 的 VFS 支持 + */ +public class SpringBootVFS extends VFS { + + private static Charset urlDecodingCharset; + private final ResourcePatternResolver resourceResolver; + + static { + setUrlDecodingCharset(Charset.defaultCharset()); + } + + public SpringBootVFS() { + this.resourceResolver = new PathMatchingResourcePatternResolver(getClass().getClassLoader()); + } + + @Override + public boolean isValid() { + return true; + } + + @Override + protected List list(URL url, String path) throws IOException { + String urlString = URLDecoder.decode(url.toString(), urlDecodingCharset.name()); + String baseUrlString = urlString.endsWith("/") ? urlString : urlString.concat("/"); + Resource[] resources = resourceResolver.getResources(baseUrlString + "**/*.class"); + return Stream.of(resources).map(resource -> preserveSubpackageName(baseUrlString, resource, path)) + .collect(Collectors.toList()); + } + + /** + * Set the charset for decoding an encoded URL string. + *

+ * Default is system default charset. + *

+ * + * @param charset + * the charset for decoding an encoded URL string + * + * @since 2.3.0 + */ + public static void setUrlDecodingCharset(Charset charset) { + urlDecodingCharset = charset; + } + + private static String preserveSubpackageName(final String baseUrlString, final Resource resource, + final String rootPath) { + try { + return rootPath + (rootPath.endsWith("/") ? "" : "/") + Normalizer + .normalize(URLDecoder.decode(resource.getURL().toString(), urlDecodingCharset.name()), Normalizer.Form.NFC) + .substring(baseUrlString.length()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + +} diff --git a/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/SqlSessionFactoryBeanCustomizer.java b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/SqlSessionFactoryBeanCustomizer.java new file mode 100644 index 00000000..da6202d7 --- /dev/null +++ b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/SqlSessionFactoryBeanCustomizer.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.spring.boot; + +import org.mybatis.spring.SqlSessionFactoryBean; + +/** + * 为 FlexSqlSessionFactoryBean 做自定义的配置支持 + * @see com.mybatisflex.spring.FlexSqlSessionFactoryBean + */ +@FunctionalInterface +public interface SqlSessionFactoryBeanCustomizer { + + void customize(SqlSessionFactoryBean factoryBean); + +} diff --git a/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/package-info.java b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/package-info.java new file mode 100644 index 00000000..b90c4ad2 --- /dev/null +++ b/mybatis-flex-spring-boot-starter/src/main/java/com/mybatisflex/spring/boot/package-info.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.spring.boot; diff --git a/mybatis-flex-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/mybatis-flex-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 00000000..52ee23f0 --- /dev/null +++ b/mybatis-flex-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,30 @@ +{ + "properties": [ + { + "defaultValue": false, + "name": "mybatis-flex.lazy-initialization", + "description": "Set whether enable lazy initialization for mapper bean.", + "type": "java.lang.Boolean" + }, + { + "defaultValue": "", + "name": "mybatis-flex.mapper-default-scope", + "description": "A default scope for mapper bean that scanned by auto-configure.", + "type": "java.lang.String" + }, + { + "defaultValue": true, + "name": "mybatis-flex.inject-sql-session-on-mapper-scan", + "description": "Set whether inject a SqlSessionTemplate or SqlSessionFactory bean (If you want to back to the behavior of 2.2.1 or before, specify false). If you use together with spring-native, should be set true.", + "type": "java.lang.Boolean" + }, + { + "name": "mybatis-flex.scripting-language-driver.velocity.userdirective", + "deprecation": { + "level": "error", + "reason": "The 'userdirective' is deprecated since Velocity 2.x. This property defined for keeping backward compatibility with older velocity version.", + "replacement": "mybatis-flex.scripting-language-driver.velocity.velocity-settings.runtime.custom_directives" + } + } + ] +} diff --git a/mybatis-flex-spring-boot-starter/src/main/resources/META-INF/spring.factories b/mybatis-flex-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..397ff41a --- /dev/null +++ b/mybatis-flex-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,7 @@ +# Depends On Database Initialization Detectors +org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\ +com.mybatisflex.spring.boot.MybatisFlexDependsOnDatabaseInitializationDetector +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.mybatisflex.spring.boot.DbAutoConfiguration,\ + com.mybatisflex.spring.boot.MybatisFlexAutoConfiguration,\ + com.mybatisflex.spring.boot.MybatisLanguageDriverAutoConfiguration \ No newline at end of file diff --git a/mybatis-flex-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mybatis-flex-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..e2dfb7d7 --- /dev/null +++ b/mybatis-flex-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.mybatisflex.spring.boot.MybatisFlexAutoConfiguration +com.mybatisflex.spring.boot.MybatisLanguageDriverAutoConfiguration diff --git a/mybatis-flex-spring/pom.xml b/mybatis-flex-spring/pom.xml new file mode 100644 index 00000000..df55dc7b --- /dev/null +++ b/mybatis-flex-spring/pom.xml @@ -0,0 +1,44 @@ + + + + parent + com.mybatis-flex + 1.0.0-beta.1 + + 4.0.0 + + mybatis-flex-spring + + + 8 + 8 + + + + + com.mybatis-flex + mybatis-flex-core + 1.0.0-beta.1 + + + + org.mybatis + mybatis-spring + + + + org.springframework + spring-context-support + + + + org.springframework + spring-jdbc + + + + + + \ No newline at end of file diff --git a/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/FlexSqlSessionFactoryBean.java b/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/FlexSqlSessionFactoryBean.java new file mode 100644 index 00000000..5d173393 --- /dev/null +++ b/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/FlexSqlSessionFactoryBean.java @@ -0,0 +1,643 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.spring; + +import com.mybatisflex.core.mybatis.FlexConfiguration; +import com.mybatisflex.core.mybatis.FlexSqlSessionFactoryBuilder; +import com.mybatisflex.core.mybatis.FlexXMLConfigBuilder; +import org.apache.ibatis.builder.xml.XMLMapperBuilder; +import org.apache.ibatis.cache.Cache; +import org.apache.ibatis.executor.ErrorContext; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.io.VFS; +import org.apache.ibatis.mapping.DatabaseIdProvider; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.reflection.factory.ObjectFactory; +import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory; +import org.apache.ibatis.scripting.LanguageDriver; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.TransactionFactory; +import org.apache.ibatis.type.TypeHandler; +import org.mybatis.logging.Logger; +import org.mybatis.logging.LoggerFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.transaction.SpringManagedTransactionFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.NestedIOException; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.type.ClassMetadata; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.util.ClassUtils; + +import javax.sql.DataSource; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Stream; + +import static org.springframework.util.Assert.notNull; +import static org.springframework.util.Assert.state; +import static org.springframework.util.ObjectUtils.isEmpty; +import static org.springframework.util.StringUtils.hasLength; +import static org.springframework.util.StringUtils.tokenizeToStringArray; + +/** + * spring 在定义 SqlSessionFactoryBean 的时候,需要替换为 FlexSqlSessionFactoryBean + * 源于 {@link SqlSessionFactoryBean},主要是用于构建 {@link com.mybatisflex.core.mybatis.FlexConfiguration },而不是使用原生的 Configuration + * 此代码主要是用于修改 {@link FlexSqlSessionFactoryBean#buildSqlSessionFactory()} 部分 + */ +public class FlexSqlSessionFactoryBean extends SqlSessionFactoryBean + implements FactoryBean, InitializingBean, ApplicationListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(SqlSessionFactoryBean.class); + + private static final ResourcePatternResolver RESOURCE_PATTERN_RESOLVER = new PathMatchingResourcePatternResolver(); + private static final MetadataReaderFactory METADATA_READER_FACTORY = new CachingMetadataReaderFactory(); + + private Resource configLocation; + + private Configuration configuration; + + private Resource[] mapperLocations; + + private DataSource dataSource; + + private TransactionFactory transactionFactory; + + private Properties configurationProperties; + +// private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); + private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new FlexSqlSessionFactoryBuilder(); + + private SqlSessionFactory sqlSessionFactory; + + // EnvironmentAware requires spring 3.1 + private String environment = SqlSessionFactoryBean.class.getSimpleName(); + + private boolean failFast; + + private Interceptor[] plugins; + + private TypeHandler[] typeHandlers; + + private String typeHandlersPackage; + + @SuppressWarnings("rawtypes") + private Class defaultEnumTypeHandler; + + private Class[] typeAliases; + + private String typeAliasesPackage; + + private Class typeAliasesSuperType; + + private LanguageDriver[] scriptingLanguageDrivers; + + private Class defaultScriptingLanguageDriver; + + // issue #19. No default provider. + private DatabaseIdProvider databaseIdProvider; + + private Class vfs; + + private Cache cache; + + private ObjectFactory objectFactory; + + private ObjectWrapperFactory objectWrapperFactory; + + /** + * Sets the ObjectFactory. + * + * @param objectFactory a custom ObjectFactory + * @since 1.1.2 + */ + public void setObjectFactory(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + + /** + * Sets the ObjectWrapperFactory. + * + * @param objectWrapperFactory a specified ObjectWrapperFactory + * @since 1.1.2 + */ + public void setObjectWrapperFactory(ObjectWrapperFactory objectWrapperFactory) { + this.objectWrapperFactory = objectWrapperFactory; + } + + /** + * Gets the DatabaseIdProvider + * + * @return a specified DatabaseIdProvider + * @since 1.1.0 + */ + public DatabaseIdProvider getDatabaseIdProvider() { + return databaseIdProvider; + } + + /** + * Sets the DatabaseIdProvider. As of version 1.2.2 this variable is not initialized by default. + * + * @param databaseIdProvider a DatabaseIdProvider + * @since 1.1.0 + */ + public void setDatabaseIdProvider(DatabaseIdProvider databaseIdProvider) { + this.databaseIdProvider = databaseIdProvider; + } + + /** + * Gets the VFS. + * + * @return a specified VFS + */ + public Class getVfs() { + return this.vfs; + } + + /** + * Sets the VFS. + * + * @param vfs a VFS + */ + public void setVfs(Class vfs) { + this.vfs = vfs; + } + + /** + * Gets the Cache. + * + * @return a specified Cache + */ + public Cache getCache() { + return this.cache; + } + + /** + * Sets the Cache. + * + * @param cache a Cache + */ + public void setCache(Cache cache) { + this.cache = cache; + } + + /** + * Mybatis plugin list. + * + * @param plugins list of plugins + * @since 1.0.1 + */ + public void setPlugins(Interceptor... plugins) { + this.plugins = plugins; + } + + /** + * Packages to search for type aliases. + * + *

+ * Since 2.0.1, allow to specify a wildcard such as {@code com.example.*.model}. + * + * @param typeAliasesPackage package to scan for domain objects + * @since 1.0.1 + */ + public void setTypeAliasesPackage(String typeAliasesPackage) { + this.typeAliasesPackage = typeAliasesPackage; + } + + /** + * Super class which domain objects have to extend to have a type alias created. No effect if there is no package to + * scan configured. + * + * @param typeAliasesSuperType super class for domain objects + * @since 1.1.2 + */ + public void setTypeAliasesSuperType(Class typeAliasesSuperType) { + this.typeAliasesSuperType = typeAliasesSuperType; + } + + /** + * Packages to search for type handlers. + * + *

+ * Since 2.0.1, allow to specify a wildcard such as {@code com.example.*.typehandler}. + * + * @param typeHandlersPackage package to scan for type handlers + * @since 1.0.1 + */ + public void setTypeHandlersPackage(String typeHandlersPackage) { + this.typeHandlersPackage = typeHandlersPackage; + } + + /** + * Set type handlers. They must be annotated with {@code MappedTypes} and optionally with {@code MappedJdbcTypes} + * + * @param typeHandlers Type handler list + * @since 1.0.1 + */ + public void setTypeHandlers(TypeHandler... typeHandlers) { + this.typeHandlers = typeHandlers; + } + + /** + * Set the default type handler class for enum. + * + * @param defaultEnumTypeHandler The default type handler class for enum + * @since 2.0.5 + */ + public void setDefaultEnumTypeHandler( + @SuppressWarnings("rawtypes") Class defaultEnumTypeHandler) { + this.defaultEnumTypeHandler = defaultEnumTypeHandler; + } + + /** + * List of type aliases to register. They can be annotated with {@code Alias} + * + * @param typeAliases Type aliases list + * @since 1.0.1 + */ + public void setTypeAliases(Class... typeAliases) { + this.typeAliases = typeAliases; + } + + /** + * If true, a final check is done on Configuration to assure that all mapped statements are fully loaded and there is + * no one still pending to resolve includes. Defaults to false. + * + * @param failFast enable failFast + * @since 1.0.1 + */ + public void setFailFast(boolean failFast) { + this.failFast = failFast; + } + + /** + * Set the location of the MyBatis {@code SqlSessionFactory} config file. A typical value is + * "WEB-INF/mybatis-configuration.xml". + * + * @param configLocation a location the MyBatis config file + */ + public void setConfigLocation(Resource configLocation) { + this.configLocation = configLocation; + } + + /** + * Set a customized MyBatis configuration. + * + * @param configuration MyBatis configuration + * @since 1.3.0 + */ + public void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } + + /** + * Set locations of MyBatis mapper files that are going to be merged into the {@code SqlSessionFactory} configuration + * at runtime. + *

+ * This is an alternative to specifying "<sqlmapper>" entries in an MyBatis config file. This property being + * based on Spring's resource abstraction also allows for specifying resource patterns here: e.g. + * "classpath*:sqlmap/*-mapper.xml". + * + * @param mapperLocations location of MyBatis mapper files + */ + public void setMapperLocations(Resource... mapperLocations) { + this.mapperLocations = mapperLocations; + } + + /** + * Set optional properties to be passed into the SqlSession configuration, as alternative to a + * {@code <properties>} tag in the configuration xml file. This will be used to resolve placeholders in the + * config file. + * + * @param sqlSessionFactoryProperties optional properties for the SqlSessionFactory + */ + public void setConfigurationProperties(Properties sqlSessionFactoryProperties) { + this.configurationProperties = sqlSessionFactoryProperties; + } + + /** + * Set the JDBC {@code DataSource} that this instance should manage transactions for. The {@code DataSource} should + * match the one used by the {@code SqlSessionFactory}: for example, you could specify the same JNDI DataSource for + * both. + *

+ * A transactional JDBC {@code Connection} for this {@code DataSource} will be provided to application code accessing + * this {@code DataSource} directly via {@code DataSourceUtils} or {@code DataSourceTransactionManager}. + *

+ * The {@code DataSource} specified here should be the target {@code DataSource} to manage transactions for, not a + * {@code TransactionAwareDataSourceProxy}. Only data access code may work with + * {@code TransactionAwareDataSourceProxy}, while the transaction manager needs to work on the underlying target + * {@code DataSource}. If there's nevertheless a {@code TransactionAwareDataSourceProxy} passed in, it will be + * unwrapped to extract its target {@code DataSource}. + * + * @param dataSource a JDBC {@code DataSource} + */ + public void setDataSource(DataSource dataSource) { + if (dataSource instanceof TransactionAwareDataSourceProxy) { + // If we got a TransactionAwareDataSourceProxy, we need to perform + // transactions for its underlying target DataSource, else data + // access code won't see properly exposed transactions (i.e. + // transactions for the target DataSource). + this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource(); + } else { + this.dataSource = dataSource; + } + } + + /** + * Sets the {@code SqlSessionFactoryBuilder} to use when creating the {@code SqlSessionFactory}. + *

+ * This is mainly meant for testing so that mock SqlSessionFactory classes can be injected. By default, + * {@code SqlSessionFactoryBuilder} creates {@code DefaultSqlSessionFactory} instances. + * + * @param sqlSessionFactoryBuilder a SqlSessionFactoryBuilder + */ + public void setSqlSessionFactoryBuilder(SqlSessionFactoryBuilder sqlSessionFactoryBuilder) { + this.sqlSessionFactoryBuilder = sqlSessionFactoryBuilder; + } + + /** + * Set the MyBatis TransactionFactory to use. Default is {@code SpringManagedTransactionFactory} + *

+ * The default {@code SpringManagedTransactionFactory} should be appropriate for all cases: be it Spring transaction + * management, EJB CMT or plain JTA. If there is no active transaction, SqlSession operations will execute SQL + * statements non-transactionally. + * + * It is strongly recommended to use the default {@code TransactionFactory}. If not used, any attempt at + * getting an SqlSession through Spring's MyBatis framework will throw an exception if a transaction is active. + * + * @param transactionFactory the MyBatis TransactionFactory + * @see SpringManagedTransactionFactory + */ + public void setTransactionFactory(TransactionFactory transactionFactory) { + this.transactionFactory = transactionFactory; + } + + /** + * NOTE: This class overrides any {@code Environment} you have set in the MyBatis config file. This is + * used only as a placeholder name. The default value is {@code SqlSessionFactoryBean.class.getSimpleName()}. + * + * @param environment the environment name + */ + public void setEnvironment(String environment) { + this.environment = environment; + } + + /** + * Set scripting language drivers. + * + * @param scriptingLanguageDrivers scripting language drivers + * @since 2.0.2 + */ + public void setScriptingLanguageDrivers(LanguageDriver... scriptingLanguageDrivers) { + this.scriptingLanguageDrivers = scriptingLanguageDrivers; + } + + /** + * Set a default scripting language driver class. + * + * @param defaultScriptingLanguageDriver A default scripting language driver class + * @since 2.0.2 + */ + public void setDefaultScriptingLanguageDriver(Class defaultScriptingLanguageDriver) { + this.defaultScriptingLanguageDriver = defaultScriptingLanguageDriver; + } + + /** + * {@inheritDoc} + */ + @Override + public void afterPropertiesSet() throws Exception { + notNull(dataSource, "Property 'dataSource' is required"); + notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required"); + state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null), + "Property 'configuration' and 'configLocation' can not specified with together"); + + this.sqlSessionFactory = buildSqlSessionFactory(); + } + + /** + * Build a {@code SqlSessionFactory} instance. + *

+ * The default implementation uses the standard MyBatis {@code XMLConfigBuilder} API to build a + * {@code SqlSessionFactory} instance based on a Reader. Since 1.3.0, it can be specified a {@link Configuration} + * instance directly(without config file). + * + * @return SqlSessionFactory + * @throws Exception if configuration is failed + */ + protected SqlSessionFactory buildSqlSessionFactory() throws Exception { + + final Configuration targetConfiguration; + +// XMLConfigBuilder xmlConfigBuilder = null; + FlexXMLConfigBuilder xmlConfigBuilder = null; + if (this.configuration != null) { + targetConfiguration = this.configuration; + if (targetConfiguration.getVariables() == null) { + targetConfiguration.setVariables(this.configurationProperties); + } else if (this.configurationProperties != null) { + targetConfiguration.getVariables().putAll(this.configurationProperties); + } + } else if (this.configLocation != null) { +// xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties); + xmlConfigBuilder = new FlexXMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties); + targetConfiguration = xmlConfigBuilder.getConfiguration(); + } else { + LOGGER.debug( + () -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration"); +// targetConfiguration = new Configuration(); + targetConfiguration = new FlexConfiguration(); + Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables); + } + + Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory); + Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory); + Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl); + + if (hasLength(this.typeAliasesPackage)) { + scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream() + .filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface()) + .filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias); + } + + if (!isEmpty(this.typeAliases)) { + Stream.of(this.typeAliases).forEach(typeAlias -> { + targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias); + LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'"); + }); + } + + if (!isEmpty(this.plugins)) { + Stream.of(this.plugins).forEach(plugin -> { + targetConfiguration.addInterceptor(plugin); + LOGGER.debug(() -> "Registered plugin: '" + plugin + "'"); + }); + } + + if (hasLength(this.typeHandlersPackage)) { + scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass()) + .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers())) + .forEach(targetConfiguration.getTypeHandlerRegistry()::register); + } + + if (!isEmpty(this.typeHandlers)) { + Stream.of(this.typeHandlers).forEach(typeHandler -> { + targetConfiguration.getTypeHandlerRegistry().register(typeHandler); + LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'"); + }); + } + + targetConfiguration.setDefaultEnumTypeHandler(defaultEnumTypeHandler); + + if (!isEmpty(this.scriptingLanguageDrivers)) { + Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> { + targetConfiguration.getLanguageRegistry().register(languageDriver); + LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'"); + }); + } + Optional.ofNullable(this.defaultScriptingLanguageDriver) + .ifPresent(targetConfiguration::setDefaultScriptingLanguage); + + if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmls + try { + targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource)); + } catch (SQLException e) { + throw new NestedIOException("Failed getting a databaseId", e); + } + } + + Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache); + + if (xmlConfigBuilder != null) { + try { + xmlConfigBuilder.parse(); + LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'"); + } catch (Exception ex) { + throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex); + } finally { + ErrorContext.instance().reset(); + } + } + + targetConfiguration.setEnvironment(new Environment(this.environment, + this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory, + this.dataSource)); + + if (this.mapperLocations != null) { + if (this.mapperLocations.length == 0) { + LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found."); + } else { + for (Resource mapperLocation : this.mapperLocations) { + if (mapperLocation == null) { + continue; + } + try { + XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), + targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments()); + xmlMapperBuilder.parse(); + } catch (Exception e) { + throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e); + } finally { + ErrorContext.instance().reset(); + } + LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'"); + } + } + } else { + LOGGER.debug(() -> "Property 'mapperLocations' was not specified."); + } + + return this.sqlSessionFactoryBuilder.build(targetConfiguration); + } + + /** + * {@inheritDoc} + */ + @Override + public SqlSessionFactory getObject() throws Exception { + if (this.sqlSessionFactory == null) { + afterPropertiesSet(); + } + + return this.sqlSessionFactory; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getObjectType() { + return this.sqlSessionFactory == null ? SqlSessionFactory.class : this.sqlSessionFactory.getClass(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isSingleton() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (failFast && event instanceof ContextRefreshedEvent) { + // fail-fast -> check all statements are completed + this.sqlSessionFactory.getConfiguration().getMappedStatementNames(); + } + } + + private Set> scanClasses(String packagePatterns, Class assignableType) throws IOException { + Set> classes = new HashSet<>(); + String[] packagePatternArray = tokenizeToStringArray(packagePatterns, + ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); + for (String packagePattern : packagePatternArray) { + Resource[] resources = RESOURCE_PATTERN_RESOLVER.getResources(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(packagePattern) + "/**/*.class"); + for (Resource resource : resources) { + try { + ClassMetadata classMetadata = METADATA_READER_FACTORY.getMetadataReader(resource).getClassMetadata(); + Class clazz = Resources.classForName(classMetadata.getClassName()); + if (assignableType == null || assignableType.isAssignableFrom(clazz)) { + classes.add(clazz); + } + } catch (Throwable e) { + LOGGER.warn(() -> "Cannot load the '" + resource + "'. Cause by " + e.toString()); + } + } + } + return classes; + } +} \ No newline at end of file diff --git a/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/SpringRowSessionManager.java b/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/SpringRowSessionManager.java new file mode 100644 index 00000000..c8253625 --- /dev/null +++ b/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/SpringRowSessionManager.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.spring; + +import com.mybatisflex.core.row.RowSessionManager; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionUtils; + +public class SpringRowSessionManager implements RowSessionManager { + + @Override + public SqlSession getSqlSession(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) { + return SqlSessionUtils.getSqlSession(sqlSessionFactory, executorType, null); + } + + + @Override + public void releaseSqlSession(SqlSession sqlSession, SqlSessionFactory sqlSessionFactory) { + SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory); + } +} diff --git a/mybatis-flex-test/mybatis-flex-native-test/pom.xml b/mybatis-flex-test/mybatis-flex-native-test/pom.xml new file mode 100644 index 00000000..257bdd5f --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-native-test/pom.xml @@ -0,0 +1,19 @@ + + + + mybatis-flex-test + com.mybatis-flex + 1.0.0-beta.1 + + 4.0.0 + + mybatis-flex-native-test + + + 8 + 8 + + + \ No newline at end of file diff --git a/mybatis-flex-test/mybatis-flex-spring-boot-test/pom.xml b/mybatis-flex-test/mybatis-flex-spring-boot-test/pom.xml new file mode 100644 index 00000000..e4b95b74 --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-boot-test/pom.xml @@ -0,0 +1,71 @@ + + + + mybatis-flex-test + com.mybatis-flex + 1.0.0-beta.1 + + 4.0.0 + + mybatis-flex-spring-boot-test + jar + + + 8 + 8 + + + + + com.mybatis-flex + mybatis-flex-spring-boot-starter + 1.0.0-beta.1 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework + spring-jdbc + + + + + com.h2database + h2 + 1.4.199 + + + + + org.yaml + snakeyaml + 1.33 + + + + + org.springframework + spring-test + test + + + + junit + junit + test + + + + + \ No newline at end of file diff --git a/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/java/com/mybatisflex/test/SampleApplication.java b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/java/com/mybatisflex/test/SampleApplication.java new file mode 100644 index 00000000..d82677e9 --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/java/com/mybatisflex/test/SampleApplication.java @@ -0,0 +1,32 @@ +package com.mybatisflex.test; + +import com.mybatisflex.test.mapper.AccountMapper; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@MapperScan("com.mybatisflex.test.mapper") +public class SampleApplication implements CommandLineRunner { + + + @Autowired + private AccountMapper accountMapper; + + + + @Override + public void run(String... args) throws Exception { + System.out.println(">>>>>>> run"); + System.out.println(this.accountMapper.selectOneById(1)); + } + + + public static void main(String[] args) { + SpringApplication.run(SampleApplication.class, args); + } + + +} diff --git a/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/java/com/mybatisflex/test/controller/AccountController.java b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/java/com/mybatisflex/test/controller/AccountController.java new file mode 100644 index 00000000..943ff088 --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/java/com/mybatisflex/test/controller/AccountController.java @@ -0,0 +1,21 @@ +package com.mybatisflex.test.controller; + +import com.mybatisflex.test.mapper.AccountMapper; +import com.mybatisflex.test.model.Account; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +//@RequestMapping("/accounts") +@RestController +public class AccountController { + + @Autowired + AccountMapper accountMapper; + + @GetMapping("/account/{id}") + Account selectOne(@PathVariable("id") Long id) { + return accountMapper.selectOneById(id); + } +} diff --git a/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/java/com/mybatisflex/test/mapper/AccountMapper.java b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/java/com/mybatisflex/test/mapper/AccountMapper.java new file mode 100644 index 00000000..fe98f15b --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/java/com/mybatisflex/test/mapper/AccountMapper.java @@ -0,0 +1,9 @@ +package com.mybatisflex.test.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.mybatisflex.test.model.Account; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AccountMapper extends BaseMapper { +} diff --git a/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/java/com/mybatisflex/test/model/Account.java b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/java/com/mybatisflex/test/model/Account.java new file mode 100644 index 00000000..fac28db0 --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/java/com/mybatisflex/test/model/Account.java @@ -0,0 +1,48 @@ +package com.mybatisflex.test.model; + +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.Table; + +import java.util.Date; + +@Table("tb_account") +public class Account { + + @Id + private Long id; + private String userName; + private Integer age; + private Date birthday; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Date getBirthday() { + return birthday; + } + + public void setBirthday(Date birthday) { + this.birthday = birthday; + } +} diff --git a/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/resources/application.yml b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/resources/application.yml new file mode 100644 index 00000000..99e72bba --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/resources/application.yml @@ -0,0 +1,12 @@ +# DataSource Config +spring: + datasource: + driver-class-name: org.h2.Driver + schema: classpath:schema.sql + username: root + password: test + sql: + init: + schema-locations: classpath:schema.sql + data-locations: classpath:data.sql + mybatis-flex: \ No newline at end of file diff --git a/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/resources/data.sql b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/resources/data.sql new file mode 100644 index 00000000..39f4cc16 --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/resources/data.sql @@ -0,0 +1,2 @@ +INSERT INTO tb_account +VALUES (1, 'Michael Yang', 18, '2020-01-11'); \ No newline at end of file diff --git a/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/resources/schema.sql b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/resources/schema.sql new file mode 100644 index 00000000..c0f2c96f --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/main/resources/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS `tb_account` +( + `id` INTEGER PRIMARY KEY, + `user_name` VARCHAR(100) NOT NULL, + `age` Integer, + `birthday` DATETIME +); \ No newline at end of file diff --git a/mybatis-flex-test/mybatis-flex-spring-test/pom.xml b/mybatis-flex-test/mybatis-flex-spring-test/pom.xml new file mode 100644 index 00000000..2cfa429e --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-test/pom.xml @@ -0,0 +1,64 @@ + + + + mybatis-flex-test + com.mybatis-flex + 1.0.0-beta.1 + + 4.0.0 + + mybatis-flex-spring-test + + + 8 + 8 + + + + + com.mybatis-flex + mybatis-flex-core + 1.0.0-beta.1 + + + + com.mybatis-flex + mybatis-flex-spring + 1.0.0-beta.1 + + + + org.springframework + spring-jdbc + + + + org.yaml + snakeyaml + 1.33 + + + + com.h2database + h2 + 1.4.199 + + + + + org.springframework + spring-test + test + + + + junit + junit + test + + + + + \ No newline at end of file diff --git a/mybatis-flex-test/mybatis-flex-spring-test/src/main/java/com/mybatisflex/test/AppConfig.java b/mybatis-flex-test/mybatis-flex-spring-test/src/main/java/com/mybatisflex/test/AppConfig.java new file mode 100644 index 00000000..f5da11d0 --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-test/src/main/java/com/mybatisflex/test/AppConfig.java @@ -0,0 +1,55 @@ +package com.mybatisflex.test; + +import com.mybatisflex.core.row.Db; +import com.mybatisflex.spring.FlexSqlSessionFactoryBean; +import com.mybatisflex.spring.SpringRowSessionManager; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.ContextStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +import javax.sql.DataSource; + +@Configuration +@MapperScan("com.mybatisflex.test.mapper") +public class AppConfig implements ApplicationListener { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("schema.sql") + .addScript("data.sql") + .build(); + } + + @Bean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { + // SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); + SqlSessionFactoryBean factoryBean = new FlexSqlSessionFactoryBean(); + factoryBean.setDataSource(dataSource); + return factoryBean.getObject(); + } + + @EventListener(classes = {ContextStartedEvent.class}) + public void handleContextStartedEvent() { + System.out.println("handleContextStartedEvent listener invoked!"); + + // 为 Db 设置默认的 SqlSession + Db.invoker().setRowSessionManager(new SpringRowSessionManager()); + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + System.out.println("onApplicationEvent"); + // 为 Db 设置默认的 SqlSession + Db.invoker().setRowSessionManager(new SpringRowSessionManager()); + } +} diff --git a/mybatis-flex-test/mybatis-flex-spring-test/src/main/java/com/mybatisflex/test/mapper/AccountMapper.java b/mybatis-flex-test/mybatis-flex-spring-test/src/main/java/com/mybatisflex/test/mapper/AccountMapper.java new file mode 100644 index 00000000..38790e2e --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-test/src/main/java/com/mybatisflex/test/mapper/AccountMapper.java @@ -0,0 +1,7 @@ +package com.mybatisflex.test.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.mybatisflex.test.model.Account; + +public interface AccountMapper extends BaseMapper { +} diff --git a/mybatis-flex-test/mybatis-flex-spring-test/src/main/java/com/mybatisflex/test/model/Account.java b/mybatis-flex-test/mybatis-flex-spring-test/src/main/java/com/mybatisflex/test/model/Account.java new file mode 100644 index 00000000..fac28db0 --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-test/src/main/java/com/mybatisflex/test/model/Account.java @@ -0,0 +1,48 @@ +package com.mybatisflex.test.model; + +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.Table; + +import java.util.Date; + +@Table("tb_account") +public class Account { + + @Id + private Long id; + private String userName; + private Integer age; + private Date birthday; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Date getBirthday() { + return birthday; + } + + public void setBirthday(Date birthday) { + this.birthday = birthday; + } +} diff --git a/mybatis-flex-test/mybatis-flex-spring-test/src/main/resources/data.sql b/mybatis-flex-test/mybatis-flex-spring-test/src/main/resources/data.sql new file mode 100644 index 00000000..39f4cc16 --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-test/src/main/resources/data.sql @@ -0,0 +1,2 @@ +INSERT INTO tb_account +VALUES (1, 'Michael Yang', 18, '2020-01-11'); \ No newline at end of file diff --git a/mybatis-flex-test/mybatis-flex-spring-test/src/main/resources/schema.sql b/mybatis-flex-test/mybatis-flex-spring-test/src/main/resources/schema.sql new file mode 100644 index 00000000..c0f2c96f --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-test/src/main/resources/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS `tb_account` +( + `id` INTEGER PRIMARY KEY, + `user_name` VARCHAR(100) NOT NULL, + `age` Integer, + `birthday` DATETIME +); \ No newline at end of file diff --git a/mybatis-flex-test/mybatis-flex-spring-test/src/test/java/com/mybatisflex/test/AccountTest.java b/mybatis-flex-test/mybatis-flex-spring-test/src/test/java/com/mybatisflex/test/AccountTest.java new file mode 100644 index 00000000..b114dbd6 --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-test/src/test/java/com/mybatisflex/test/AccountTest.java @@ -0,0 +1,32 @@ +package com.mybatisflex.test; + +import com.mybatisflex.core.row.Db; +import com.mybatisflex.core.row.Row; +import com.mybatisflex.test.mapper.AccountMapper; +import com.mybatisflex.test.model.Account; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AppConfig.class) +public class AccountTest { + + @Autowired + AccountMapper accountMapper; + + @Test + public void testSelectOne() { + Account account = accountMapper.selectOneById(1); + System.out.println(account); + } + + @Test + public void testSelectOneByRow() { + Row row = Db.selectOneById("tb_account", "id", 1); + System.out.println(row); + } + +} diff --git a/mybatis-flex-test/pom.xml b/mybatis-flex-test/pom.xml new file mode 100644 index 00000000..e82d77dd --- /dev/null +++ b/mybatis-flex-test/pom.xml @@ -0,0 +1,26 @@ + + + + parent + com.mybatis-flex + 1.0.0-beta.1 + + 4.0.0 + + mybatis-flex-test + pom + + + mybatis-flex-native-test + mybatis-flex-spring-test + mybatis-flex-spring-boot-test + + + + 8 + 8 + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..d2305bc2 --- /dev/null +++ b/pom.xml @@ -0,0 +1,279 @@ + + + 4.0.0 + + com.mybatis-flex + parent + pom + 1.0.0-beta.1 + + mybatis-flex + http://mybatis-flex.com + Mybatis-Flex is an elegant Mybatis Enhancement Framework. + + + Github Issue + https://github.com/mybatis-flex/mybatis-flex/issues + + + + The Apache Software License, Version 2.0 + http://apache.org/licenses/LICENSE-2.0.txt + + + + + yangfuhai + fuhai999@gmail.com + + developer + + +8 + + + + scm:git:https://github.com/mybatis-flex/mybatis-flex.git + scm:git:https://github.com/mybatis-flex/mybatis-flex.git + https://github.com/mybatis-flex/mybatis-flex + + + + mybatis-flex-annotation + mybatis-flex-codegen + mybatis-flex-core + mybatis-flex-spring + mybatis-flex-spring-boot-starter + mybatis-flex-test + + + + 8 + 8 + + 3.5.11 + 2.0.7 + 1.2.4 + 2.1.2 + 1.0.4 + + 5.1.49 + 4.0.3 + + 5.3.15 + 2.5.3 + + 4.13.2 + + + + + + org.mybatis + mybatis + ${mybatis.version} + + + + org.mybatis + mybatis-spring + ${mybatis-spring.version} + + + + org.mybatis.scripting + mybatis-freemarker + ${mybatis-freemarker.version} + + + + org.mybatis.scripting + mybatis-velocity + ${mybatis-velocity.version} + + + + org.mybatis.scripting + mybatis-thymeleaf + ${mybatis-thymeleaf.version} + + + + org.springframework + spring-context-support + ${spring.version} + + + + org.springframework + spring-jdbc + ${spring.version} + + + + org.springframework + spring-test + ${spring.version} + + + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot.version} + + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + + + + junit + junit + ${junit.version} + + + + mysql + mysql-connector-java + ${mysql.version} + test + + + + com.zaxxer + HikariCP + ${HikariCP.version} + test + + + + + + + + + + src/main/resources + + **/* + + + + *.properties + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + UTF-8 + + + + + + + + + + + + + + + + + + + release + + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.4 + + Mybatis-Flex + Mybatis-Flex + private + false + true + true + -Xdoclint:none + true + 8 + + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + + sign + + + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots/ + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + + \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 00000000..784b0581 --- /dev/null +++ b/readme.md @@ -0,0 +1,142 @@ + +# Mybatis-Flex 一个优雅的 Mybatis 增强框架 + +## 特征 + +- 1、很轻量,整个框架只依赖 Mybatis 再无其他任何第三方依赖 +- 2、Entity 类基本的增删改查、分页查询 +- 3、Row 通用映射支持,可以无需实体类对数据库进行增删改查 +- 4、支持多种数据库类型,自由通过方言持续扩展 +- 5、支持联合主键,以及不同的主键下的主键内容生成策略 +- 6、极其友好的 SQL 联动查询,IDE 自动提示不再担心出错 +- 7、以及更多小惊喜 + +## hello world + +**实体类** + +```java + +@Table("tb_account") +public class Account { + + @Id() + private Long id; + private String userName; + private Date birthday; + private int sex; + + //getter setter +} +``` + +**AccountMapper 类** + +```java +public interface AccountMapper extends BaseMapper { + //只需定义 Mapper 接口即可,可以无任何内容。 +} +``` + +**Hello world** + +示例 1:查询 1 条数据 +```java +class HelloWorld { + public static void main(String... args) { + + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/mybatis-flex"); + dataSource.setUsername("username"); + dataSource.setPassword("password"); + + MybatisFlexBootstrap.getInstance() + .setDatasource(dataSource) + .addMapper(AccountMapper.class) + .start(); + + + //示例1:查询 id=100 条数据 + Account account = MybatisFlexBootstrap.getInstance() + .execute(AccountMapper.class, mapper -> + mapper.selectOneById(100) + ); + } +} +``` + +示例2:查询列表 + +```java +class HelloWorld { + public static void main(String... args) { + + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/mybatis-flex"); + dataSource.setUsername("username"); + dataSource.setPassword("password"); + + MybatisFlexBootstrap.getInstance() + .setDatasource(dataSource) + .addMapper(AccountMapper.class) + .start(); + + //示例2:通过 QueryWrapper 构建条件查询数据列表 + QueryWrapper query = QueryWrapper.create() + .select() + .from(ACCOUNT) + .where(ACCOUNT.ID.ge(100)) + .and(ACCOUNT.USER_NAME.like("张").or(ACCOUNT.USER_NAME.like("李"))); + + // 执行 SQL: + // ELECT * FROM `tb_account` + // WHERE `tb_account`.`id` >= 100 + // AND (`tb_account`.`user_name` LIKE '%张%' OR `tb_account`.`user_name` LIKE '%李%' ) + List accounts = MybatisFlexBootstrap.getInstance() + .execute(AccountMapper.class, mapper -> + mapper.selectListByQuery(query) + ); + + } +} +``` + +示例3:分页查询 +```java +class HelloWorld { + public static void main(String... args) { + + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/mybatis-flex"); + dataSource.setUsername("username"); + dataSource.setPassword("password"); + + MybatisFlexBootstrap.getInstance() + .setDatasource(dataSource) + .addMapper(AccountMapper.class) + .start(); + + // 示例3:分页查询 + // 查询第 5 页,每页 10 条数据,通过 QueryWrapper 构建条件查询 + QueryWrapper query = QueryWrapper.create() + .select() + .from(ACCOUNT) + .where(ACCOUNT.ID.ge(100)) + .and(ACCOUNT.USER_NAME.like("张").or(ACCOUNT.USER_NAME.like("李"))) + .orderBy(ACCOUNT.ID.desc()); + + // 执行 SQL: + // ELECT * FROM `tb_account` + // WHERE `tb_account`.`id` >= 100 + // AND (`tb_account`.`user_name` LIKE '%张%' OR `tb_account`.`user_name` LIKE '%李%' ) + // ORDER BY `tb_account`.`id` DESC + // LIMIT 40,10 + Page accounts = MybatisFlexBootstrap.getInstance() + .execute(AccountMapper.class, mapper -> + mapper.paginate(5, 10, query) + ); + + } +} +``` +