diff --git a/hutool-db/pom.xml b/hutool-db/pom.xml index b355d9d44..a34b37900 100755 --- a/hutool-db/pom.xml +++ b/hutool-db/pom.xml @@ -164,5 +164,11 @@ 2.4.12 test + + com.sap.cloud.db.jdbc + ngdbc + 2.24.7 + test + diff --git a/hutool-db/src/main/java/org/dromara/hutool/db/dialect/DialectFactory.java b/hutool-db/src/main/java/org/dromara/hutool/db/dialect/DialectFactory.java index 53324c404..74ff6d3f7 100644 --- a/hutool-db/src/main/java/org/dromara/hutool/db/dialect/DialectFactory.java +++ b/hutool-db/src/main/java/org/dromara/hutool/db/dialect/DialectFactory.java @@ -89,6 +89,9 @@ public class DialectFactory { } else if (DriverNames.DRIVER_GOLDENDB.equalsIgnoreCase(driverName)) { // MySQL兼容 return new MysqlDialect(dbConfig); + } else if (DriverNames.DRIVER_HANA.equalsIgnoreCase(driverName)) { + // SAP HANA + return new HanaDialect(dbConfig); } } // 无法识别可支持的数据库类型默认使用ANSI方言,可兼容大部分SQL语句 diff --git a/hutool-db/src/main/java/org/dromara/hutool/db/dialect/DialectName.java b/hutool-db/src/main/java/org/dromara/hutool/db/dialect/DialectName.java index f6288ad9d..4eb0ec6a2 100644 --- a/hutool-db/src/main/java/org/dromara/hutool/db/dialect/DialectName.java +++ b/hutool-db/src/main/java/org/dromara/hutool/db/dialect/DialectName.java @@ -25,7 +25,7 @@ import org.dromara.hutool.core.text.StrUtil; * @author Looly */ public enum DialectName { - ANSI, MYSQL, ORACLE, POSTGRESQL, SQLITE3, H2, SQLSERVER, SQLSERVER2012, PHOENIX, DM; + ANSI, MYSQL, ORACLE, POSTGRESQL, SQLITE3, H2, SQLSERVER, SQLSERVER2012, PHOENIX, DM, HANA; /** * 是否为指定数据库方言,检查时不分区大小写 diff --git a/hutool-db/src/main/java/org/dromara/hutool/db/dialect/impl/HanaDialect.java b/hutool-db/src/main/java/org/dromara/hutool/db/dialect/impl/HanaDialect.java new file mode 100644 index 000000000..e4a4ee4bb --- /dev/null +++ b/hutool-db/src/main/java/org/dromara/hutool/db/dialect/impl/HanaDialect.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * 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 org.dromara.hutool.db.dialect.impl; + +import org.dromara.hutool.core.text.StrUtil; +import org.dromara.hutool.db.DbException; +import org.dromara.hutool.db.Entity; +import org.dromara.hutool.db.config.DbConfig; +import org.dromara.hutool.db.dialect.DialectName; +import org.dromara.hutool.db.sql.QuoteWrapper; +import org.dromara.hutool.db.sql.SqlBuilder; +import org.dromara.hutool.db.sql.StatementUtil; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Hana数据库方言 + * + * @author daoyou.dev + */ +public class HanaDialect extends AnsiSqlDialect { + private static final long serialVersionUID = 1L; + + /** + * 构造 + * + * @param config 数据库配置对象 + */ + public HanaDialect(final DbConfig config) { + super(config); + quoteWrapper = new QuoteWrapper('"'); + } + + @Override + public String dialectName() { + return DialectName.HANA.name(); + } + + /** + * 构建用于upsert的{@link PreparedStatement}。 + * SAP HANA 使用 MERGE INTO 语法来实现 UPSERT 操作。 + *

+ * 生成 SQL 语法为: + *

{@code
+	 *     MERGE INTO demo AS target
+	 *     USING (SELECT ? AS a, ? AS b, ? AS c FROM DUMMY) AS source
+	 *     ON target.id = source.id
+	 *     WHEN MATCHED THEN
+	 *         UPDATE SET target.a = source.a, target.b = source.b, target.c = source.c
+	 *     WHEN NOT MATCHED THEN
+	 *         INSERT (a, b, c) VALUES (source.a, source.b, source.c);
+	 * }
+ * + * @param conn 数据库连接对象 + * @param entity 数据实体类(包含表名) + * @param keys 主键字段数组,通常用于确定匹配条件(联合主键) + * @return PreparedStatement + * @throws DbException SQL 执行异常 + */ + @Override + public PreparedStatement psForUpsert(final Connection conn, final Entity entity, final String... keys) throws DbException { + SqlBuilder.validateEntity(entity); + final SqlBuilder builder = SqlBuilder.of(quoteWrapper); + + final List columns = new ArrayList<>(); + + // 构建字段部分和参数占位符部分 + entity.forEach((field, value) -> { + if (StrUtil.isNotBlank(field)) { + columns.add(quoteWrapper != null ? quoteWrapper.wrap(field) : field); + builder.addParams(value); + } + }); + + String tableName = entity.getTableName(); + if (quoteWrapper != null) { + tableName = quoteWrapper.wrap(tableName); + } + + // 构建 UPSERT 语句 + builder.append("UPSERT ").append(tableName).append(" ("); + builder.append(String.join(", ", columns)); + builder.append(") VALUES ("); + builder.append(String.join(", ", Collections.nCopies(columns.size(), "?"))); + builder.append(") WITH PRIMARY KEY"); + + return StatementUtil.prepareStatement(false, this.dbConfig, conn, builder.build(), builder.getParamValueArray()); + } +} diff --git a/hutool-db/src/main/java/org/dromara/hutool/db/driver/DriverIdentifier.java b/hutool-db/src/main/java/org/dromara/hutool/db/driver/DriverIdentifier.java index 5dc31f7eb..f136fb417 100644 --- a/hutool-db/src/main/java/org/dromara/hutool/db/driver/DriverIdentifier.java +++ b/hutool-db/src/main/java/org/dromara/hutool/db/driver/DriverIdentifier.java @@ -33,7 +33,7 @@ import java.util.List; * @author Looly * @since 6.0.0 */ -public class DriverIdentifier implements DriverNames{ +public class DriverIdentifier implements DriverNames { /** * 单例驱动识别器 @@ -131,7 +131,9 @@ public class DriverIdentifier implements DriverNames{ new StartsWithDriverMatcher(DRIVER_GAUSS, "jdbc:zenith:"), new StartsWithDriverMatcher(DRIVER_OPENGAUSS, "jdbc:opengauss:"), // 中兴GoldenDB - new StartsWithDriverMatcher(DRIVER_GOLDENDB, "jdbc:goldendb:") + new StartsWithDriverMatcher(DRIVER_GOLDENDB, "jdbc:goldendb:"), + // SAP HANA + new StartsWithDriverMatcher(DRIVER_HANA, "jdbc:sap:") ); } diff --git a/hutool-db/src/main/java/org/dromara/hutool/db/driver/DriverNames.java b/hutool-db/src/main/java/org/dromara/hutool/db/driver/DriverNames.java index 9d1515502..2144e6c47 100644 --- a/hutool-db/src/main/java/org/dromara/hutool/db/driver/DriverNames.java +++ b/hutool-db/src/main/java/org/dromara/hutool/db/driver/DriverNames.java @@ -252,9 +252,12 @@ public interface DriverNames { * JDBC 驱动 Greenplum */ String DRIVER_GREENPLUM = "com.pivotal.jdbc.GreenplumDriver"; - /** * JDBC 驱动 GoldenDB */ String DRIVER_GOLDENDB = "com.goldendb.jdbc.Driver"; + /** + * JDBC 驱动 Sap Hana + */ + String DRIVER_HANA = "com.sap.db.jdbc.Driver"; } diff --git a/hutool-db/src/test/java/org/dromara/hutool/db/HanaTest.java b/hutool-db/src/test/java/org/dromara/hutool/db/HanaTest.java new file mode 100644 index 000000000..c6f9a2cc8 --- /dev/null +++ b/hutool-db/src/test/java/org/dromara/hutool/db/HanaTest.java @@ -0,0 +1,82 @@ +package org.dromara.hutool.db; + +import org.dromara.hutool.core.lang.Console; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class HanaTest { + @BeforeAll + public static void createTable() { + final Db db = Db.of("hana"); + final long count = db.count("SELECT * FROM SYS.TABLES WHERE TABLE_NAME = ? AND SCHEMA_NAME = CURRENT_SCHEMA", "user"); + if (count > 0) { + db.execute("drop table \"user\""); + } + db.execute("CREATE COLUMN TABLE \"user\" (\"id\" INT NOT NULL, \"account\" VARCHAR(255), \"name\" VARCHAR(255), \"text\" VARCHAR(255), \"test1\" VARCHAR(255), \"pass\" VARCHAR(255), PRIMARY KEY (\"id\"))"); + } + + @Test + @Disabled + public void insertTest() { + for (int id = 100; id < 200; id++) { + Db.of("hana").insert(Entity.of("user")// + .set("id", id)// + .set("name", "测试用户" + id)// + .set("text", "描述" + id)// + .set("test1", "t" + id)// + ); + } + } + + /** + * 事务测试
+ * 更新三条信息,低2条后抛出异常,正常情况下三条都应该不变 + * + * @throws SQLException SQL异常 + */ + @Test + @Disabled + public void txTest() throws SQLException { + Db.of("hana").tx(db -> { + final int update = db.update(Entity.of("user").set("text", "描述100"), Entity.of().set("id", 100)); + db.update(Entity.of("user").set("text", "描述101"), Entity.of().set("id", 101)); + if (1 == update) { + // 手动指定异常,然后测试回滚触发 + throw new RuntimeException("Error"); + } + db.update(Entity.of("user").set("text", "描述102"), Entity.of().set("id", 102)); + }); + } + + @Test + @Disabled + public void pageTest() { + final PageResult result = Db.of("hana").page(Entity.of("\"user\""), new Page(2, 10)); + for (final Entity entity : result) { + Console.log(entity.get("id")); + } + } + + @Test + @Disabled + public void getTimeStampTest() { + final List all = Db.of("hana").findAll("user"); + Console.log(all); + } + + @Test + public void upsertTest() { + final Db db = Db.of("hana"); + db.insert(Entity.of("user").set("id", 1).set("account", "ice").set("pass", "123456")); + db.upsert(Entity.of("user").set("id", 1).set("account", "daoyou").set("pass", "a123456").set("name", "道友")); + final Entity user = db.get(Entity.of("user").set("id", 1)); + System.out.println("user=======" + user.getStr("account") + "___" + user.getStr("pass")); + assertEquals("daoyou", user.getStr("account")); + } +} diff --git a/hutool-db/src/test/resources/config/db.setting b/hutool-db/src/test/resources/config/db.setting index d9d12b9bf..ab70d4a6b 100644 --- a/hutool-db/src/test/resources/config/db.setting +++ b/hutool-db/src/test/resources/config/db.setting @@ -108,3 +108,9 @@ url = jdbc:oceanbase://localhost:2881/test user = root pass = 123456 remarks = true + +[hana] +url = jdbc:sap://localhost:30015/HAP_CONN?autoReconnect=true +user = DB +pass = 123456 +remarks = true