flink jdbc connector支持clickhouse

1、业务背景

业务需求把数据写入clickhouse,同时还需要支持主键更新。目前使用的开源flink1.11版本是不支持clickhouse的,项目中使用的是flink sql 所以需要对源代码进行改造,支持jdbc的方式把实时数据写入clickhouse集群。

package org.apache.flink.connector.jdbc.dialect;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * Default JDBC dialects.
 */
public final class JdbcDialects {

	private static final List<JdbcDialect> DIALECTS = Arrays.asList(
		new DerbyDialect(),
		new MySQLDialect(),
		new PostgresDialect()
	);

	/**
	 * Fetch the JdbcDialect class corresponding to a given database url.
	 */
	public static Optional<JdbcDialect> get(String url) {
		for (JdbcDialect dialect : DIALECTS) {
			if (dialect.canHandle(url)) {
				return Optional.of(dialect);
			}
		}
		return Optional.empty();
	}
}

2、自定义JdbcDialect,参考MySQLDialect来实现

package org.apache.flink.connector.jdbc.dialect;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.apache.flink.connector.jdbc.internal.converter.ClickhouseRowConverter;
import org.apache.flink.connector.jdbc.internal.converter.JdbcRowConverter;
import org.apache.flink.table.types.logical.LogicalTypeRoot;
import org.apache.flink.table.types.logical.RowType;

public class ClickhouseDialect extends AbstractDialect {
	
	 	private static final long serialVersionUID = 1L;

		// Define MAX/MIN precision of TIMESTAMP type according to Mysql docs:
		// https://dev.mysql.com/doc/refman/8.0/en/fractional-seconds.html
		private static final int MAX_TIMESTAMP_PRECISION = 6;
		private static final int MIN_TIMESTAMP_PRECISION = 1;

		// Define MAX/MIN precision of DECIMAL type according to Mysql docs:
		// https://dev.mysql.com/doc/refman/8.0/en/fixed-point-types.html
		private static final int MAX_DECIMAL_PRECISION = 65;
		private static final int MIN_DECIMAL_PRECISION = 1;

	   @Override
	   public boolean canHandle(String url) {
	       return url.startsWith("jdbc:clickhouse:");
	   }

	   @Override
	   public JdbcRowConverter getRowConverter(
	           RowType rowType) {
	       return new ClickhouseRowConverter(rowType);
	   }

	   @Override
	   public Optional<String> defaultDriverName() {
	       return Optional.of("ru.yandex.clickhouse.ClickHouseDriver");
	   }

	   @Override
	   public String quoteIdentifier(String identifier) {
	       return identifier;
	   }

	   @Override
	   public int maxDecimalPrecision() {
	       return MAX_DECIMAL_PRECISION;
	   }

	   @Override
	   public int minDecimalPrecision() {
	       return MIN_DECIMAL_PRECISION;
	   }

	   @Override
	   public int maxTimestampPrecision() {
	       return MAX_TIMESTAMP_PRECISION;
	   }

	   @Override
	   public int minTimestampPrecision() {
	       return MIN_TIMESTAMP_PRECISION;
	   }

	   @Override
	   public List<LogicalTypeRoot> unsupportedTypes() {
	       return Arrays.asList(
	               LogicalTypeRoot.BINARY,
	               LogicalTypeRoot.TIMESTAMP_WITH_LOCAL_TIME_ZONE,
	               LogicalTypeRoot.TIMESTAMP_WITH_TIME_ZONE,
	               LogicalTypeRoot.INTERVAL_YEAR_MONTH,
	               LogicalTypeRoot.INTERVAL_DAY_TIME,
	               LogicalTypeRoot.ARRAY,
	               LogicalTypeRoot.MULTISET,
	               LogicalTypeRoot.MAP,
	               LogicalTypeRoot.ROW,
	               LogicalTypeRoot.DISTINCT_TYPE,
	               LogicalTypeRoot.STRUCTURED_TYPE,
	               LogicalTypeRoot.NULL,
	               LogicalTypeRoot.RAW,
	               LogicalTypeRoot.SYMBOL,
	               LogicalTypeRoot.UNRESOLVED
	       );
	   }

	   @Override
	   public String dialectName() {
	       return "clickhouse";
	   }
}

3、自定义实现JdbcRowConverter,参考MySQLRowConverter

package org.apache.flink.connector.jdbc.internal.converter;

import org.apache.flink.table.types.logical.RowType;

public class ClickhouseRowConverter extends AbstractJdbcRowConverter{
	
	public ClickhouseRowConverter(RowType rowType) {
		super(rowType);
	}

	private static final long serialVersionUID = 1L;

    @Override
    public String converterName() {
        return "clickhouse";
    }
}

4、现在我们把创建好的ClickhouseDialect 放入到JdbcDialects 代码中

/*
 * 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.flink.connector.jdbc.dialect;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * Default JDBC dialects.
 */
public final class JdbcDialects {

	private static final List<JdbcDialect> DIALECTS = Arrays.asList(
		new DerbyDialect(),
		new MySQLDialect(),
		new PostgresDialect(),
		new ClickhouseDialect()
	);

	/**
	 * Fetch the JdbcDialect class corresponding to a given database url.
	 */
	public static Optional<JdbcDialect> get(String url) {
		for (JdbcDialect dialect : DIALECTS) {
			if (dialect.canHandle(url)) {
				return Optional.of(dialect);
			}
		}
		return Optional.empty();
	}
}

这样就完成了,flink-jdbc-connector支持clickhouse的改造。

5、业务sql

create table tableA(
    client_time string,
    user_id string,
    client_ip string,
    session_id string,
    query string,
    dayno string,
) COMMENT 'tableA'
WITH (
    'connector' = 'kafka',
    'topic' = 'kafka_topic',
    'properties.bootstrap.servers' = 'kafka_servers',
    'properties.security.protocol' = 'SASL_PLAINTEXT',
    'properties.sasl.mechanism' = 'SCRAM-SHA-256',
    'properties.sasl.jaas.config' = 'org.apache.kafka.common.security.scram.ScramLoginModule required username="xxx" password="xxx";',
    'properties.group.id' = 'kafka_groupUd',
    'scan.startup.mode'='timestamp',
    'scan.startup.timestamp-millis'='1638962488170',
    'format' = 'csv',
    'csv.ignore-parse-errors' = 'true',
    'csv.allow-comments' = 'true',
    'csv.field-delimiter' = U&'\0009'
);

create table tableB(
    client_time		string,
    user_id		string,
    session_id string,
    query		string,
    dayno		string,
    primary key (session_id) NOT ENFORCED
) COMMENT 'session_id维度汇总数据'
WITH (
    'connector.type' = 'jdbc',
    'connector.url' = 'jdbc:clickhouse://host:ip/database',
    'connector.table' =  'clickhouse_table' ,
    'connector.username' = 'xxx',
    'connector.password' = 'xxx',
    'connector.write.flush.max-rows' = '5000',
    'connector.write.flush.interval' = '5'
);

insert into  tableB
select
    client_time,
    user_id,
    session_id,
    query,
    dayno,
from(
    select
        LISTAGG(client_time,',') client_time,
        max(user_id) user_id,
        session_id,
        LISTAGG(query,',') query,
        min(dayno) dayno,
        count(1) cnt
    from tableA
    where REGEXP(dayno,'[0-9]{4}-[0-9]{2}-[0-9]{2}') and session_id is not null and session_id <> ''
    group by session_id
)x where cnt <= 10;

上面的sql代码是支持持续更新的,所以会生成update语句,但是clickhouse不支持update语句,要求写入insert语句,根据报错信息,找对应的代码进行改造,只执行insert语句,业务就成功运行了。

/*
 * 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.flink.connector.jdbc.internal.executor;

import org.apache.flink.annotation.Internal;
import org.apache.flink.connector.jdbc.JdbcStatementBuilder;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.clickhouse.ClickHousePreparedStatementImpl;

import javax.annotation.Nonnull;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import static org.apache.flink.util.Preconditions.checkNotNull;

/**
 * {@link JdbcBatchStatementExecutor} that provides upsert semantics by updating row if it exists and inserting otherwise.
 * Used in Table API.
 */
@Internal
public final class InsertOrUpdateJdbcExecutor<R, K, V> implements JdbcBatchStatementExecutor<R> {

	private static final Logger LOG = LoggerFactory.getLogger(InsertOrUpdateJdbcExecutor.class);

	private final String existSQL;
	private final String insertSQL;
	private final String updateSQL;

	private final JdbcStatementBuilder<K> existSetter;
	private final JdbcStatementBuilder<V> insertSetter;
	private final JdbcStatementBuilder<V> updateSetter;

	private final Function<R, K> keyExtractor;
	private final Function<R, V> valueMapper;

	private final Map<K, V> batch;

	private transient PreparedStatement existStatement;
	private transient PreparedStatement insertStatement;
	private transient PreparedStatement updateStatement;

	public InsertOrUpdateJdbcExecutor(
			@Nonnull String existSQL,
			@Nonnull String insertSQL,
			@Nonnull String updateSQL,
			@Nonnull JdbcStatementBuilder<K> existSetter,
			@Nonnull JdbcStatementBuilder<V> insertSetter,
			@Nonnull JdbcStatementBuilder<V> updateSetter,
			@Nonnull Function<R, K> keyExtractor,
			@Nonnull Function<R, V> valueExtractor) {
		this.existSQL = checkNotNull(existSQL);
		this.insertSQL = checkNotNull(insertSQL);
		this.updateSQL = checkNotNull(updateSQL);
		this.existSetter = checkNotNull(existSetter);
		this.insertSetter = checkNotNull(insertSetter);
		this.updateSetter = checkNotNull(updateSetter);
		this.keyExtractor = checkNotNull(keyExtractor);
		this.valueMapper = checkNotNull(valueExtractor);
		this.batch = new HashMap<>();
	}

	@Override
	public void prepareStatements(Connection connection) throws SQLException {
		existStatement = connection.prepareStatement(existSQL);
		insertStatement = connection.prepareStatement(insertSQL);
		updateStatement = connection.prepareStatement(updateSQL);
	}

	@Override
	public void addToBatch(R record) {
		batch.put(keyExtractor.apply(record), valueMapper.apply(record));
	}

	@Override
	public void executeBatch() throws SQLException {
		if (!batch.isEmpty()) {
			for (Map.Entry<K, V> entry : batch.entrySet()) {
				processOneRowInBatch(entry.getKey(), entry.getValue());
			}
			
			if(updateStatement instanceof ClickHousePreparedStatementImpl) {
				insertStatement.executeBatch();
			} else {
				updateStatement.executeBatch();
				insertStatement.executeBatch();
			}
			
			batch.clear();
		}
	}

	private void processOneRowInBatch(K pk, V row) throws SQLException {
		
		
		if(updateStatement instanceof ClickHousePreparedStatementImpl) {
			insertSetter.accept(insertStatement, row);
			insertStatement.addBatch();
		} else if (exist(pk)) {
			updateSetter.accept(updateStatement, row);
			updateStatement.addBatch();
		} else {
			insertSetter.accept(insertStatement, row);
			insertStatement.addBatch();
		}
	}

	private boolean exist(K pk) throws SQLException {
		existSetter.accept(existStatement, pk);
		try (ResultSet resultSet = existStatement.executeQuery()) {
			return resultSet.next();
		}
	}

	@Override
	public void closeStatements() throws SQLException {
		for (PreparedStatement s : Arrays.asList(existStatement, insertStatement, updateStatement)) {
			if (s != null) {
				s.close();
			}
		}
	}
}

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: FlinkJDBC连接器是一个用于在Flink流处理作业中与外部数据库进行交互的重要组件。这个连接器可以连接到多种数据库,其中包括Oracle数据库。 Oracle是一个流行的关系型数据库管理系统,广泛用于企业级应用和大规模数据存储。FlinkJDBC连接器通过使用Oracle JDBC驱动程序,在Flink作业中无缝集成了对Oracle数据库的访问和查询功能。 FlinkJDBC连接器支持Oracle数据库的各种操作,包括数据读取、数据写入和数据更新。使用FlinkJDBC连接器,可以轻松地从Oracle数据库中读取数据,进行流处理和分析,然后将结果写回到数据库中。 在Flink作业中使用JDBC连接器与Oracle数据库进行交互是相对简单的。只需要在Flink的作业配置文件中指定Oracle数据库的连接信息和表信息,并指定要执行的SQL语句或查询语句。FlinkJDBC连接器会根据配置信息建立与Oracle数据库的连接,并执行指定的操作。 总而言之,FlinkJDBC连接器是一个强大的工具,可以将Flink的流处理和分析能力与Oracle数据库的数据存储和查询功能结合起来。使用FlinkJDBC连接器,可以轻松地实现与Oracle数据库的集成,并实现复杂的数据处理和分析任务。 ### 回答2: Flink JDBC Connector 是 Apache Flink 框架的一个重要组件,它用于将 Flink 应用程序与关系型数据库进行连接和交互。而Oracle是一种功能强大且广泛使用的商业关系型数据库管理系统,所以可以肯定地说,Flink JDBC Connector支持连接和操作 Oracle 数据库的。 使用 Flink JDBC Connector 可以方便地在 Flink 应用程序中读取和写入 Oracle 数据库的数据。对于读取数据,可以通过指定查询语句来从 Oracle 数据库中提取数据,并将其转换为 Flink DataStream 或 Table 进行进一步处理和分析。对于写入数据,可以将 Flink 应用程序的计算结果直接插入到 Oracle 数据库中的指定表中。 Flink JDBC Connector 提供了与 Oracle 数据库交互所需的 JDBC 驱动程序,并具有处理数据库连接管理、事务管理等方面的能力。另外,Flink JDBC Connector支持将查询结果批量写入或者批量读取,以提高数据处理的效率和性能。 在使用 Flink JDBC Connector 连接 Oracle 数据库时,我们需要配置连接参数,包括数据库的 URL、用户名、密码等,并根据需要指定要执行的 SQL 查询语句或插入数据的表名。通过合理配置这些参数,Flink 应用程序可以轻松地与 Oracle 数据库进行数据交互。 总之,Flink JDBC Connector支持连接和操作 Oracle 数据库的,它为 Flink 应用程序提供了与 Oracle 数据库交互的便利性和灵活性,使得我们可以方便地在大数据处理中使用 Oracle 数据库。 ### 回答3: 是的,Flink JDBC连接器支持与Oracle数据库的集成。Flink提供了适用于Oracle的JDBC连接器,可以通过该连接器将Flink与Oracle数据库连接起来,并在Flink作业中读取和写入Oracle数据库的数据。 使用Flink JDBC连接器与Oracle集成非常简单。首先,我们需要在Flink作业中设置Oracle数据库的连接URL、用户名和密码等连接参数。然后,可以通过FlinkJDBCSourceFunction从Oracle数据库中读取数据,并将其转换为流数据进行进一步的处理和计算。另外,也可以使用FlinkJDBCSinkFunction将流数据写入到Oracle数据库中。 在与Oracle集成时,FlinkJDBC连接器提供了对Oracle特定的数据类型和功能的支持。它可以处理Oracle的数值、字符串、日期、时间戳等常见数据类型,并且支持Oracle的事务和批处理操作。此外,Flink还提供了对Oracle的连接池管理和数据分片等功能,以提高性能和可伸缩性。 总之,Flink JDBC连接器可以很方便地与Oracle数据库集成,实现在Flink作业中读取和写入Oracle数据库的功能。无论是实时数据流处理还是批处理作业,都可以通过Flink与Oracle进行无缝集成,以实现各种数据处理和分析的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值