From 19906f2bd7ee01cae6560b90655d83f96edd73ce Mon Sep 17 00:00:00 2001 From: ZhouXY108 Date: Wed, 19 Jul 2023 22:19:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=8E=20plusone-commons=20=E4=B8=AD?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E5=87=BA=E6=9D=A5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 38 +++ .idea/.gitignore | 3 + .idea/encodings.xml | 7 + .idea/inspectionProfiles/Project_Default.xml | 6 + .idea/misc.xml | 14 + .idea/vcs.xml | 6 + pom.xml | 88 ++++++ src/main/java/xyz/zhouxy/jdbc/DbRecord.java | 75 +++++ src/main/java/xyz/zhouxy/jdbc/ResultMap.java | 28 ++ .../xyz/zhouxy/jdbc/SimpleJdbcTemplate.java | 256 ++++++++++++++++++ .../zhouxy/jdbc/SimpleJdbcTemplateTests.java | 85 ++++++ 11 files changed, 606 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/encodings.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 pom.xml create mode 100644 src/main/java/xyz/zhouxy/jdbc/DbRecord.java create mode 100644 src/main/java/xyz/zhouxy/jdbc/ResultMap.java create mode 100644 src/main/java/xyz/zhouxy/jdbc/SimpleJdbcTemplate.java create mode 100644 src/test/java/xyz/zhouxy/jdbc/SimpleJdbcTemplateTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..359bb53 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..d5ce676 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..132404b --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8526a62 --- /dev/null +++ b/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + xyz.zhouxy.jdbc + simple-jdbc + 0.1.0-SNAPSHOT + + + 8 + 8 + UTF-8 + + + + + xyz.zhouxy.plusone + plusone-commons + 0.1.0-SNAPSHOT + + + + org.junit.jupiter + junit-jupiter-api + 5.9.2 + test + + + + ch.qos.logback + logback-classic + 1.2.11 + test + + + + com.zaxxer + HikariCP + 4.0.3 + + + org.slf4j + slf4j-api + + + test + + + org.postgresql + postgresql + 42.3.8 + + + org.checkerframework + checker-qual + + + test + + + + + + aliyun + https://maven.aliyun.com/repository/public + + true + + + false + + + + + + aliyun-plugin + https://maven.aliyun.com/repository/public + + true + + + false + + + + diff --git a/src/main/java/xyz/zhouxy/jdbc/DbRecord.java b/src/main/java/xyz/zhouxy/jdbc/DbRecord.java new file mode 100644 index 0000000..bf84482 --- /dev/null +++ b/src/main/java/xyz/zhouxy/jdbc/DbRecord.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022-2023 the original author or authors. + * + * 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 + * + * https://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 xyz.zhouxy.jdbc; + +import com.google.common.annotations.Beta; +import com.google.common.base.Preconditions; +import org.apache.commons.lang3.StringUtils; +import xyz.zhouxy.plusone.commons.collection.AbstractMapWrapper; +import xyz.zhouxy.plusone.commons.util.OptionalUtil; + +import java.util.*; + +@Beta +public class DbRecord extends AbstractMapWrapper { + + public DbRecord() { + super(new HashMap<>(), k -> Preconditions.checkArgument(StringUtils.isNotBlank(k), "Key must has text."), null); + } + + public DbRecord(Map map) { + super(map, k -> Preconditions.checkArgument(StringUtils.isNotBlank(k), "Key must has text."), null); + } + + public Optional getValueAsString(String key) { + return this.getAndConvert(key); + } + + public List getValueAsList(String key) { + return this.>getAndConvert(key) + .map(l -> (l instanceof List) ? (List) l : new ArrayList<>(l)) + .orElse(Collections.emptyList()); + } + + public Set getValueAsSet(String key) { + return this.>getAndConvert(key) + .map(l -> (l instanceof Set) ? (Set) l : new HashSet<>(l)) + .orElse(Collections.emptySet()); + } + + public OptionalInt getValueAsInt(String key) { + return OptionalUtil.toOptionalInt(this.getAndConvert(key)); + } + + public OptionalLong getValueAsLong(String key) { + return OptionalUtil.toOptionalLong(this.getAndConvert(key)); + } + + public OptionalDouble getValueAsDouble(String key) { + return OptionalUtil.toOptionalDouble(this.getAndConvert(key)); + } + + @Override + protected DbRecord getSelf() { + return this; + } + + @Override + public String toString() { + return "xyz.zhouxy.plusone.commons.jdbc.DbRecord@" + super.toString(); + } +} diff --git a/src/main/java/xyz/zhouxy/jdbc/ResultMap.java b/src/main/java/xyz/zhouxy/jdbc/ResultMap.java new file mode 100644 index 0000000..532a3e3 --- /dev/null +++ b/src/main/java/xyz/zhouxy/jdbc/ResultMap.java @@ -0,0 +1,28 @@ +/* + * Copyright 2022-2023 the original author or authors. + * + * 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 + * + * https://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 xyz.zhouxy.jdbc; + +import com.google.common.annotations.Beta; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Beta +@FunctionalInterface +public interface ResultMap { + T map(ResultSet rs, int rowNumber) throws SQLException; +} diff --git a/src/main/java/xyz/zhouxy/jdbc/SimpleJdbcTemplate.java b/src/main/java/xyz/zhouxy/jdbc/SimpleJdbcTemplate.java new file mode 100644 index 0000000..461b952 --- /dev/null +++ b/src/main/java/xyz/zhouxy/jdbc/SimpleJdbcTemplate.java @@ -0,0 +1,256 @@ +/* + * Copyright 2022-2023 the original author or authors. + * + * 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 + * + * https://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 xyz.zhouxy.jdbc; + +import com.google.common.annotations.Beta; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import org.apache.commons.lang3.ArrayUtils; +import xyz.zhouxy.plusone.commons.util.MoreArrays; +import xyz.zhouxy.plusone.commons.util.MoreCollections; +import xyz.zhouxy.plusone.commons.util.OptionalUtil; + +import java.math.BigDecimal; +import java.sql.*; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Beta +public class SimpleJdbcTemplate { + + public static JdbcExecutor connect(final Connection conn) { + return new JdbcExecutor(conn); + } + + public static String paramsToString(Object[] params) { + return Arrays.toString(params); + } + + public static String paramsToString(final Collection params) { + if (params == null) { + return "null"; + } + if (params.isEmpty()) { + return "[]"; + } + int iMax = params.size() - 1; + StringBuilder b = new StringBuilder(); + b.append('['); + int i = 0; + for (Object[] p : params) { + b.append(Arrays.toString(p)); + if (i == iMax) { + return b.append(']').toString(); + } + b.append(','); + i++; + } + return b.append(']').toString(); + } + + private SimpleJdbcTemplate() { + throw new IllegalStateException("Utility class"); + } + + public static class JdbcExecutor { + + private final Connection conn; + + public JdbcExecutor(Connection conn) { + this.conn = conn; + } + + public List query(String sql, Object[] params, ResultMap resultMap) throws SQLException { + try (PreparedStatement stmt = this.conn.prepareStatement(sql)) { + if (params != null && params.length > 0) { + for (int i = 0; i < params.length; i++) { + stmt.setObject(i + 1, params[i]); + } + } + try (ResultSet rs = stmt.executeQuery()) { + List result = new ArrayList<>(); + int rowNumber = 0; + while (rs.next()) { + T e = resultMap.map(rs, rowNumber++); + result.add(e); + } + return result; + } + } + } + + public Optional queryFirst(String sql, Object[] params, ResultMap resultMap) throws SQLException { + List list = query(sql, params, resultMap); + return (list.isEmpty()) ? Optional.empty() : Optional.ofNullable(list.get(0)); + } + + public static final ResultMap> mapResultMap = (rs, rowNumber) -> { + Map result = new HashMap<>(); + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + for (int i = 1; i <= columnCount; i++) { + String colName = metaData.getColumnName(i); + result.put(colName, rs.getObject(colName)); + } + return result; + }; + + public List> query(String sql, Object... params) throws SQLException { + return query(sql, params, mapResultMap); + } + + public Optional> queryFirst(String sql, Object... params) throws SQLException { + return queryFirst(sql, params, mapResultMap); + } + + public static final ResultMap recordResultMap = (rs, rowNumber) -> { + DbRecord result = new DbRecord(); + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + for (int i = 1; i <= columnCount; i++) { + String colName = metaData.getColumnName(i); + result.put(colName, rs.getObject(colName)); + } + return result; + }; + + public List queryToRecordList(String sql, Object... params) throws SQLException { + return query(sql, params, recordResultMap); + } + + public Optional queryFirstRecord(String sql, Object... params) throws SQLException { + return queryFirst(sql, params, recordResultMap); + } + + public Optional queryToString(String sql, Object... params) throws SQLException { + return queryFirst(sql, params, (ResultSet rs, int rowNumber) -> rs.getString(1)); + } + + public OptionalInt queryToInt(String sql, Object... params) throws SQLException { + Optional result = queryFirst(sql, params, (ResultSet rs, int rowNumber) -> rs.getInt(1)); + return OptionalUtil.toOptionalInt(result); + } + + public OptionalLong queryToLong(String sql, Object... params) throws SQLException { + Optional result = queryFirst(sql, params, (ResultSet rs, int rowNumber) -> rs.getLong(1)); + return OptionalUtil.toOptionalLong(result); + } + + public OptionalDouble queryToDouble(String sql, Object... params) throws SQLException { + Optional result = queryFirst(sql, params, (ResultSet rs, int rowNumber) -> rs.getDouble(1)); + return OptionalUtil.toOptionalDouble(result); + } + + public Optional queryToBigDecimal(String sql, Object... params) throws SQLException { + return queryFirst(sql, params, (ResultSet rs, int rowNumber) -> rs.getBigDecimal(1)); + } + + public int update(String sql, Object... params) throws SQLException { + try (PreparedStatement stmt = this.conn.prepareStatement(sql)) { + if (params != null && params.length > 0) { + for (int i = 0; i < params.length; i++) { + stmt.setObject(i + 1, params[i]); + } + } + return stmt.executeUpdate(); + } + } + + public int[] batchUpdate(String sql, Collection params, int batchSize) throws SQLException { + int executeCount = params.size() / batchSize; + executeCount = (params.size() % batchSize == 0) ? executeCount : (executeCount + 1); + List result = Lists.newArrayListWithCapacity(executeCount); + + try (PreparedStatement stmt = this.conn.prepareStatement(sql)) { + int i = 0; + for (Object[] ps : params) { + i++; + for (int j = 0; j < ps.length; j++) { + stmt.setObject(j + 1, ps[j]); + } + stmt.addBatch(); + if (i % batchSize == 0 || i >= params.size()) { + int[] n = stmt.executeBatch(); + result.add(n); + stmt.clearBatch(); + } + } + return MoreArrays.concatIntArray(result); + } + } + + public void tx(final IAtom atom) throws SQLException, T { + Preconditions.checkNotNull(atom, "Atom can not be null."); + try { + this.conn.setAutoCommit(false); + atom.execute(); + conn.commit(); + conn.setAutoCommit(true); + } catch (Exception e) { + conn.rollback(); + conn.setAutoCommit(true); + throw e; + } + } + + @FunctionalInterface + public interface IAtom { + @SuppressWarnings("all") + void execute() throws SQLException, T; + } + } + + public static class ParamBuilder { + + public static Object[] buildParams(final Object... params) { + if (ArrayUtils.isEmpty(params)) { + return ArrayUtils.EMPTY_OBJECT_ARRAY; + } + return Arrays.stream(params) + .map(param -> { + if (param instanceof Optional) { + return OptionalUtil.orElseNull((Optional) param); + } + if (param instanceof OptionalInt) { + return OptionalUtil.toInteger(((OptionalInt) param)); + } + if (param instanceof OptionalLong) { + return OptionalUtil.toLong(((OptionalLong) param)); + } + if (param instanceof OptionalDouble) { + return OptionalUtil.toDouble(((OptionalDouble) param)); + } + return param; + }) + .toArray(); + } + + public static List buildBatchParams(final Collection c, final Function function) { + Preconditions.checkNotNull(c, "The collection can not be null."); + Preconditions.checkNotNull(function, "The function can not be null."); + if (MoreCollections.isEmpty(c)) { + return Collections.emptyList(); + } + return c.stream().map(function).collect(Collectors.toList()); + } + + private ParamBuilder() { + throw new IllegalStateException("Utility class"); + } + } +} diff --git a/src/test/java/xyz/zhouxy/jdbc/SimpleJdbcTemplateTests.java b/src/test/java/xyz/zhouxy/jdbc/SimpleJdbcTemplateTests.java new file mode 100644 index 0000000..be860c7 --- /dev/null +++ b/src/test/java/xyz/zhouxy/jdbc/SimpleJdbcTemplateTests.java @@ -0,0 +1,85 @@ +package xyz.zhouxy.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static xyz.zhouxy.jdbc.SimpleJdbcTemplate.ParamBuilder.*; +import static xyz.zhouxy.plusone.commons.sql.JdbcSql.IN; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import xyz.zhouxy.plusone.commons.sql.SQL; + +class SimpleJdbcTemplateTests { + + private static final Logger log = LoggerFactory.getLogger(SimpleJdbcTemplateTests.class); + + final DataSource dataSource; + + String[] cStruct = { + "id", + "created_by", + "create_time", + "updated_by", + "update_time", + "status" + }; + + SimpleJdbcTemplateTests() { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl("jdbc:postgresql://localhost:5432/plusone"); + config.setUsername("postgres"); + config.setPassword("zhouxy108"); + this.dataSource = new HikariDataSource(config); + } + + @Test + void testQuery() throws SQLException { + try (Connection conn = this.dataSource.getConnection()) { + Object[] params = buildParams("501533", "501554", "544599"); + String sql = SQL.newJdbcSql() + .SELECT("*") + .FROM("test_table") + .WHERE(IN("id", params)) + .toString(); + log.info(sql); + List rs = SimpleJdbcTemplate.connect(conn) + .queryToRecordList(sql, params); + assertNotNull(rs); + for (DbRecord baseEntity : rs) { + // log.info("id: {}", baseEntity.getValueAsString("id")); + log.info(baseEntity.toString()); + assertEquals(Optional.empty(), baseEntity.getValueAsString("updated_by")); + } + } + } + + @Test + void testTransaction() { + try (Connection conn = dataSource.getConnection()) { + SimpleJdbcTemplate.connect(conn) + .tx(() -> { + SimpleJdbcTemplate.connect(conn) + .update("INSERT INTO base_table (created_by, create_time, status) VALUES (?, now(), 0)", 585757); + Optional> first = SimpleJdbcTemplate.connect(conn) + .queryFirst("SELECT * FROM base_table WHERE created_by = ?", 585757); + log.info("first: {}", first); + throw new NullPointerException(); + }); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } +}