-
Notifications
You must be signed in to change notification settings - Fork 2.1k
[Feature][seatunnel-connectors-v2/connector-jdbc]sqlserver support bulk copy write #10099
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
55d9704
2e3d9e2
239ff6c
ad494c3
b058f83
29308f2
979e283
053cf06
97b1e31
9d4895d
92d840a
bc87432
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,220 @@ | ||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||||||||||||||||||||||||||||||||
| * contributor license agreements. See the NOTICE file distributed with | ||||||||||||||||||||||||||||||||
| * this work for additional information regarding copyright ownership. | ||||||||||||||||||||||||||||||||
| * The ASF licenses this file to You 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.apache.seatunnel.connectors.seatunnel.jdbc.internal.executor; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import org.apache.seatunnel.api.table.catalog.Column; | ||||||||||||||||||||||||||||||||
| import org.apache.seatunnel.api.table.type.SeaTunnelDataType; | ||||||||||||||||||||||||||||||||
| import org.apache.seatunnel.api.table.type.SeaTunnelRow; | ||||||||||||||||||||||||||||||||
| import org.apache.seatunnel.common.exception.CommonErrorCode; | ||||||||||||||||||||||||||||||||
| import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException; | ||||||||||||||||||||||||||||||||
| import org.apache.seatunnel.connectors.seatunnel.jdbc.exception.JdbcConnectorErrorCode; | ||||||||||||||||||||||||||||||||
| import org.apache.seatunnel.connectors.seatunnel.jdbc.exception.JdbcConnectorException; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import com.microsoft.sqlserver.jdbc.ISQLServerBulkData; | ||||||||||||||||||||||||||||||||
| import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy; | ||||||||||||||||||||||||||||||||
| import com.microsoft.sqlserver.jdbc.SQLServerBulkCopyOptions; | ||||||||||||||||||||||||||||||||
| import lombok.NonNull; | ||||||||||||||||||||||||||||||||
| import lombok.SneakyThrows; | ||||||||||||||||||||||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import java.sql.Connection; | ||||||||||||||||||||||||||||||||
| import java.sql.ResultSetMetaData; | ||||||||||||||||||||||||||||||||
| import java.sql.SQLException; | ||||||||||||||||||||||||||||||||
| import java.util.ArrayList; | ||||||||||||||||||||||||||||||||
| import java.util.HashMap; | ||||||||||||||||||||||||||||||||
| import java.util.Iterator; | ||||||||||||||||||||||||||||||||
| import java.util.LinkedHashSet; | ||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||||||||||
| import java.util.Set; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Slf4j | ||||||||||||||||||||||||||||||||
| public class SqlserverBulkCopyBatchStatementExecutor | ||||||||||||||||||||||||||||||||
| implements JdbcBatchStatementExecutor<SeaTunnelRow> { | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @NonNull private final String schemaTableName; | ||||||||||||||||||||||||||||||||
| @NonNull private final List<Column> columns; | ||||||||||||||||||||||||||||||||
| @NonNull private final List<Object[]> buffer = new ArrayList<>(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private Connection connection; | ||||||||||||||||||||||||||||||||
| private ResultSetMetaData resultSetMetaData; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| public SqlserverBulkCopyBatchStatementExecutor(String schemaTableName, List<Column> columns) { | ||||||||||||||||||||||||||||||||
| this.columns = columns; | ||||||||||||||||||||||||||||||||
| this.schemaTableName = schemaTableName; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||
| public void prepareStatements(Connection connection) throws SQLException { | ||||||||||||||||||||||||||||||||
| this.connection = connection.unwrap(com.microsoft.sqlserver.jdbc.SQLServerConnection.class); | ||||||||||||||||||||||||||||||||
| this.connection.setAutoCommit(false); | ||||||||||||||||||||||||||||||||
| this.resultSetMetaData = getResultSetMetaData(this.connection, schemaTableName); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||
| public void addToBatch(SeaTunnelRow record) throws SQLException { | ||||||||||||||||||||||||||||||||
| Object[] rowData = new Object[columns.size()]; | ||||||||||||||||||||||||||||||||
| for (int i = 0; i < columns.size(); i++) { | ||||||||||||||||||||||||||||||||
| Object field = record.getField(i); | ||||||||||||||||||||||||||||||||
| SeaTunnelDataType<?> type = columns.get(i).getDataType(); | ||||||||||||||||||||||||||||||||
| switch (type.getSqlType()) { | ||||||||||||||||||||||||||||||||
| case DATE: | ||||||||||||||||||||||||||||||||
| rowData[i] = | ||||||||||||||||||||||||||||||||
| field == null | ||||||||||||||||||||||||||||||||
| ? null | ||||||||||||||||||||||||||||||||
| : java.sql.Date.valueOf((java.time.LocalDate) field); | ||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||
| case TIME: | ||||||||||||||||||||||||||||||||
| rowData[i] = | ||||||||||||||||||||||||||||||||
| field == null | ||||||||||||||||||||||||||||||||
| ? null | ||||||||||||||||||||||||||||||||
| : java.sql.Time.valueOf((java.time.LocalTime) field); | ||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||
| case TIMESTAMP: | ||||||||||||||||||||||||||||||||
| rowData[i] = | ||||||||||||||||||||||||||||||||
| field == null | ||||||||||||||||||||||||||||||||
| ? null | ||||||||||||||||||||||||||||||||
| : java.sql.Timestamp.valueOf((java.time.LocalDateTime) field); | ||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||
|
Comment on lines
+75
to
+92
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any other types need to handle? like Decimal? |
||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||
| rowData[i] = field; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| buffer.add(rowData); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||
| public void executeBatch() throws SQLException { | ||||||||||||||||||||||||||||||||
| if (!buffer.isEmpty()) { | ||||||||||||||||||||||||||||||||
| executeBatchInternal(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private void executeBatchInternal() { | ||||||||||||||||||||||||||||||||
| try (SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(connection)) { | ||||||||||||||||||||||||||||||||
| bulkCopy.setDestinationTableName(schemaTableName); | ||||||||||||||||||||||||||||||||
| // BulkCopy config | ||||||||||||||||||||||||||||||||
| SQLServerBulkCopyOptions options = new SQLServerBulkCopyOptions(); | ||||||||||||||||||||||||||||||||
| options.setTableLock(true); | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is enabling table-level locks necessary? |
||||||||||||||||||||||||||||||||
| options.setUseInternalTransaction(false); | ||||||||||||||||||||||||||||||||
| options.setCheckConstraints(false); | ||||||||||||||||||||||||||||||||
| options.setFireTriggers(false); | ||||||||||||||||||||||||||||||||
| options.setBatchSize(buffer.size()); | ||||||||||||||||||||||||||||||||
| bulkCopy.setBulkCopyOptions(options); | ||||||||||||||||||||||||||||||||
| long start = System.currentTimeMillis(); | ||||||||||||||||||||||||||||||||
| bulkCopy.writeToServer(new MemoryBulkData(resultSetMetaData, buffer)); | ||||||||||||||||||||||||||||||||
| connection.commit(); | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SeaTunnel self will handle |
||||||||||||||||||||||||||||||||
| log.info( | ||||||||||||||||||||||||||||||||
| "Bulk copied {} rows to table {}, cost {}s", | ||||||||||||||||||||||||||||||||
| buffer.size(), | ||||||||||||||||||||||||||||||||
| schemaTableName, | ||||||||||||||||||||||||||||||||
| (System.currentTimeMillis() - start) / 1000); | ||||||||||||||||||||||||||||||||
| buffer.clear(); | ||||||||||||||||||||||||||||||||
| } catch (SQLException e) { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| connection.rollback(); | ||||||||||||||||||||||||||||||||
| } catch (SQLException rollbackEx) { | ||||||||||||||||||||||||||||||||
| log.error("Failed to rollback", rollbackEx); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+128
to
+132
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There‘s no need to handle rollback, JdbcOutputFormat will handle it |
||||||||||||||||||||||||||||||||
| throw new JdbcConnectorException( | ||||||||||||||||||||||||||||||||
| JdbcConnectorErrorCode.TRANSACTION_OPERATION_FAILED, e); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||
| public void closeStatements() throws SQLException { | ||||||||||||||||||||||||||||||||
| executeBatch(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private ResultSetMetaData getResultSetMetaData(Connection connection, String schemaTableName) { | ||||||||||||||||||||||||||||||||
| final String[] split = schemaTableName.split("\\."); | ||||||||||||||||||||||||||||||||
| if (split.length != 2) { | ||||||||||||||||||||||||||||||||
| Map<String, String> params = new HashMap<>(); | ||||||||||||||||||||||||||||||||
| params.put("value", schemaTableName); | ||||||||||||||||||||||||||||||||
| throw new SeaTunnelRuntimeException(CommonErrorCode.SEATUNNEL_CONFIG_ERROR, params); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+144
to
+149
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it have to be schema.tableName? How about we allow users to only configure tableName, and in this case, the default schema will be used? |
||||||||||||||||||||||||||||||||
| String queryMeta = | ||||||||||||||||||||||||||||||||
| String.format("select * from \"%s\".\"%s\" where 1=0", split[0], split[1]); | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| return connection.createStatement().executeQuery(queryMeta).getMetaData(); | ||||||||||||||||||||||||||||||||
| } catch (SQLException e) { | ||||||||||||||||||||||||||||||||
| throw new SeaTunnelRuntimeException( | ||||||||||||||||||||||||||||||||
| JdbcConnectorErrorCode.NO_SUPPORT_OPERATION_FAILED, | ||||||||||||||||||||||||||||||||
| "get meta data fail:" + schemaTableName); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+152
to
+158
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| static class MemoryBulkData implements ISQLServerBulkData { | ||||||||||||||||||||||||||||||||
| private final ResultSetMetaData metaData; | ||||||||||||||||||||||||||||||||
| private final Iterator<Object[]> iterator; | ||||||||||||||||||||||||||||||||
| private Object[] current; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| public MemoryBulkData(ResultSetMetaData metaData, List<Object[]> rows) { | ||||||||||||||||||||||||||||||||
| this.metaData = metaData; | ||||||||||||||||||||||||||||||||
| this.iterator = rows.iterator(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @SneakyThrows | ||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||
| public Set<Integer> getColumnOrdinals() { | ||||||||||||||||||||||||||||||||
| int columnCount = metaData.getColumnCount(); | ||||||||||||||||||||||||||||||||
| Set<Integer> ordinals = new LinkedHashSet<>(); | ||||||||||||||||||||||||||||||||
| for (int i = 1; i <= columnCount; i++) { | ||||||||||||||||||||||||||||||||
| ordinals.add(i); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return ordinals; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||
| public Object[] getRowData() { | ||||||||||||||||||||||||||||||||
| return current; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||
| public boolean next() { | ||||||||||||||||||||||||||||||||
| if (iterator.hasNext()) { | ||||||||||||||||||||||||||||||||
| current = iterator.next(); | ||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @SneakyThrows | ||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||
| public String getColumnName(int column) { | ||||||||||||||||||||||||||||||||
| return metaData.getColumnName(column); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @SneakyThrows | ||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||
| public int getColumnType(int column) { | ||||||||||||||||||||||||||||||||
| return metaData.getColumnType(column); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @SneakyThrows | ||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||
| public int getPrecision(int column) { | ||||||||||||||||||||||||||||||||
| return metaData.getPrecision(column); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @SneakyThrows | ||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||
| public int getScale(int column) { | ||||||||||||||||||||||||||||||||
| return metaData.getScale(column); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You 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.apache.seatunnel.connectors.seatunnel.jdbc.sink; | ||
|
|
||
| import org.apache.seatunnel.api.configuration.ReadonlyConfig; | ||
| import org.apache.seatunnel.connectors.seatunnel.jdbc.config.JdbcSinkOptions; | ||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @Slf4j | ||
| public class JdbcSinkConfigChecker { | ||
|
|
||
| public static void check(ReadonlyConfig readonlyConfig) { | ||
| if (readonlyConfig.get(JdbcSinkOptions.USE_SQLSERVER_BULK_COPY)) { | ||
| if (readonlyConfig.get(JdbcSinkOptions.AUTO_COMMIT)) { | ||
| log.warn( | ||
| "When use_sqlserver_bulk_copy is enabled, auto_commit is true and does not take effect."); | ||
| } | ||
| if (readonlyConfig.get(JdbcSinkOptions.IS_EXACTLY_ONCE)) { | ||
| log.warn( | ||
| "When use_sqlserver_bulk_copy is enabled, is_exactly_once is true and does not take effect."); | ||
| } | ||
| if (readonlyConfig.get(JdbcSinkOptions.ENABLE_UPSERT)) { | ||
| log.warn( | ||
| "When use_sqlserver_bulk_copy is enabled, enable_upsert is true and does not take effect."); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chl-wxp This strong dependency will lead to a strong reliance, where
mssql-jdbcis imported regardless of whether it is used, which is inconsistent with the previous design.cc @davidzollo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the reminder. Such strong dependency is indeed a problem. I need to think about how to solve this problem.