这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

Java

Vertica JDBC 驱动程序为您提供了标准 JDBC API。如果已使用 JDBC 访问过其他数据库,您应该会觉得访问 Vertica 的方式有些熟悉。此部分介绍如何使用 JDBC 将 Java 应用程序连接到 Vertica。

先决条件

在创建 Java 客户端应用程序之前,您必须安装 JDBC 客户端驱动程序

1 - JDBC 功能支持

Vertica JDBC 驱动程序符合 JDBC 4.0 标准(但它不实施这些标准中的所有可选功能)。应用程序可以使用 DatabaseMetaData 类来确定驱动程序是否支持它要使用的特定功能。此外,此驱动程序实施 Wrapper 接口,该接口可让客户端代码发现特定于 Vertica 的 JDBC 标准类(例如,VerticaConnection 类和 VerticaStatement 类)扩展。

使用 Vertica JDBC 驱动程序时,应谨记以下一些重要事实:

  • 光标仅能向前移动,而不可滚动。无法更新结果集。

  • 一个连接在任何时候都仅支持执行单个语句。如果要同时执行多个语句,您必须打开多个连接。

  • 从客户端驱动程序 12.0.0 版开始,支持 CallableStatement

多个 SQL 语句支持

Vertica JDBC 驱动程序可以执行包含多个语句的字符串。例如:

stmt.executeUpdate("CREATE TABLE t(a INT);INSERT INTO t VALUES(10);");

Statement 界面支持执行包含多个 SQL 语句的字符串。不能将多个语句字符串与 PreparedStatement 一起使用。从主机文件系统复制文件的 COPY 语句可使用多个语句字符串。但是,客户端 COPY 语句 (COPY FROM STDIN) 不起作用。

将多批内容转换为 COPY 语句

Vertica JDBC 驱动程序可将所有批量插入转换为 Vertica COPY 语句。如果关闭 JDBC 连接的 AutoCommit 属性,JDBC 驱动程序将使用单个 COPY 语句加载连续批量插入中的数据,这样可通过减少开销来提高加载性能。有关详细信息,请参阅使用 JDBC 预定义的语句批量插入

多个 JDBC 版本支持

Vertica JDBC 驱动程序同时实施符合 JDBC 3.0 标准和 JDBC 4.0 标准的接口。驱动程序返回到应用程序的接口取决于应用程序运行于的 JVM 版本。如果应用程序正在 5.0 JVM 上运行,则驱动程序会向应用程序提供 JDBC 3.0 类。如果应用程序正在 6.0 或更高版本 JVM 上运行,则驱动程序会向应用程序提供 JDBC 4.0 类。

多重活动结果集 (MARS)

Vertica JDBC 驱动程序支持多重活动结果集 (MARS)。MARS 允许在单个连接中执行多个查询。ResultBufferSize 将查询的结果直接发送到客户端,而 MARS 先将结果存储在服务器上。完成查询和存储所有结果之后,您可向服务器发送检索请求以将行返回到客户端。

2 - 创建和配置连接

Java 应用程序必须先创建连接才能与 Vertica 交互。使用 JDBC 连接到 Vertica 与连接到大多数其他数据库相似。

导入 SQL 包

创建连接之前,您必须导入 Java SQL 包。执行此操作的一种简单方法是使用通配符导入整个包:

import java.sql.*;

您可能还需要导入 Properties 类。在将连接实例化时,您可以使用此类的实例来传递连接属性,而不必将所有内容都编码到连接字符串中:

import java.util.Properties;

如果应用程序需要在 Java 5 JVM 中运行,它将使用符合 JDBC 3.0 的较旧版本驱动程序。此驱动程序要求使用 Class.forName() 方法手动加载 Vertica JDBC 驱动程序:

// Only required for old JDBC 3.0 driver
try {
    Class.forName("com.vertica.jdbc.Driver");
} catch (ClassNotFoundException e) {
    // Could not find the driver class. Likely an issue
    // with finding the .jar file.
    System.err.println("Could not find the JDBC driver class.");
    e.printStackTrace();
    return; // Exit. Cannot do anything further.
}

应用程序可能在 Java 6 或更高版本的 JVM 中运行。如果是这样,则 JVM 会自动加载兼容 Vertica JDBC 4.0 的驱动程序,而不要求调用 Class.forName。但是,发出此调用不会对进程产生负面影响。因此,如果您希望应用程序同时与 Java 5 和 Java 6(或更高版本)JVM 兼容,应用程序仍可以调用 Class.forName

打开连接

导入 SQL 包之后,您便已准备好通过调用 DriverManager.getConnection() 方法来创建连接。您至少必须向此方法提供以下信息:

  • 数据库群集中的节点的 IP 地址或主机名。

    您可以提供 IPv4 地址、IPv6 地址或主机名。

    在 IPv4/IPv6 混合网络中,DNS 服务器配置决定了哪个 IP 版本地址最先发送。可使用 PreferredAddressFamily 选项来强制连接使用 IPv4 或 IPv6。

  • 数据库的端口号

  • 数据库的名称

  • 数据库用户帐户的用户名

  • 用户的密码(如果该用户具有密码)

前三个参数始终作为连接字符串的一部分提供,连接字符串是一个 URL,可指示 JDBC 驱动程序从何处查找数据库。连接字符串的格式为:

"jdbc:vertica://VerticaHost:portNumber/databaseName"

此连接字符串的第一部分选择了 Vertica JDBC 驱动程序,后跟数据库的位置。

可以通过以下三种方法之一向 JDBC 驱动程序提供后两个参数(用户名和密码):

  • 作为连接字符串的一部分。将以 URL 参数的相似方式对这两个参数进行编码:

    "jdbc:vertica://VerticaHost:portNumber/databaseName?user=username&password=password"
    
  • 作为单独的参数传递到 DriverManager.getConnection()

    Connection conn = DriverManager.getConnection(
            "jdbc:vertica://VerticaHost:portNumber/databaseName",
            "username", "password");
    
  • Properties 对象中:

    Properties myProp = new Properties();
    myProp.put("user", "username");
    myProp.put("password", "password");
    Connection conn = DriverManager.getConnection(
        "jdbc:vertica://VerticaHost:portNumber/databaseName", myProp);
    

对以上三种方法来说,Properties 对象是最灵活的,因为使用此对象可以轻松地将其他连接属性传递到 getConnection() 方法。有关其他连接属性的详细信息,请参阅连接属性设置和获取连接属性值

如果建立与数据库的连接时出现任何问题,getConnection() 方法将在其子类之一上引发 SQLException。若要防止异常,请将方法包含在 try-catch 块中,如以下有关建立连接的完整示例所示。

import java.sql.*;
import java.util.Properties;

public class VerySimpleVerticaJDBCExample {
    public static void main(String[] args) {
        /*
         * If your client needs to run under a Java 5 JVM, It will use the older
         * JDBC 3.0-compliant driver, which requires you manually load the
         * driver using Class.forname
         */
        /*
         * try { Class.forName("com.vertica.jdbc.Driver"); } catch
         * (ClassNotFoundException e) { // Could not find the driver class.
         * Likely an issue // with finding the .jar file.
         * System.err.println("Could not find the JDBC driver class.");
         * e.printStackTrace(); return; // Bail out. We cannot do anything
         * further. }
         */
        Properties myProp = new Properties();
        myProp.put("user", "dbadmin");
        myProp.put("password", "vertica");
        myProp.put("loginTimeout", "35");
        myProp.put("KeystorePath", "c:/keystore/keystore.jks");
     myProp.put("KeystorePassword", "keypwd");
     myProp.put("TrustStorePath", "c:/truststore/localstore.jks");
     myProp.put("TrustStorePassword", "trustpwd");
        Connection conn;
        try {
            conn = DriverManager.getConnection(
                    "jdbc:vertica://V_vmart_node0001.example.com:5433/vmart", myProp);
            System.out.println("Connected!");
            conn.close();
        } catch (SQLTransientConnectionException connException) {
            // There was a potentially temporary network error
            // Could automatically retry a number of times here, but
            // instead just report error and exit.
            System.out.print("Network connection issue: ");
            System.out.print(connException.getMessage());
            System.out.println(" Try again later!");
            return;
        } catch (SQLInvalidAuthorizationSpecException authException) {
            // Either the username or password was wrong
            System.out.print("Could not log into database: ");
            System.out.print(authException.getMessage());
            System.out.println(" Check the login credentials and try again.");
            return;
        } catch (SQLException e) {
            // Catch-all for other exceptions
            e.printStackTrace();
        }
    }
}

使用密钥库和信任库创建连接

您可以使用密钥库和信任库创建与 JDBC 客户端驱动程序的安全连接。有关 Vertica 中的安全性的详细信息,请参阅安全性和身份验证

有关如何在 Vertica 中生成(或导入外部)证书的示例和说明,请参阅生成 TLS 证书和密钥

要在 Vertica 中查看您的密钥和证书,请参阅CERTIFICATESCRYPTOGRAPHIC_KEYS

  1. 生成您自己的自签名证书或使用现有 CA(证书颁发机构)证书作为根 CA。有关此过程的信息,请参考 Schannel 文档

  2. 可选:生成或导入由根 CA 签署的中间 CA 证书。虽然中间 CA 证书不是必需的,但拥有中间 CA 证书对于测试和调试连接很有用。

  3. 为 Vertica 生成并签署(或导入)服务器证书。

  4. 使用 ALTER TLS CONFIGURATION 将 Vertica 配置为使用客户端/服务器 TLS 进行新连接。有关详细信息,请参阅配置客户端-服务器 TLS

    对于服务器模式(无客户端证书验证):

    => ALTER TLS CONFIGURATION server TLSMODE 'ENABLE';
    => ALTER TLS CONFIGURATION server CERTIFICATE server_cert;
    

    对于相互模式(根据 TLSMODE 进行不同严格性的客户端证书验证):

    => ALTER TLS CONFIGURATION server TLSMODE 'TRY_VERIFY';
    => ALTER TLS CONFIGURATION server CERTIFICATE server_cert ADD CA CERTIFICATES ca_cert;
    
  5. 或者,您可以使用 CREATE AUTHENTICATION 禁用所有非 SSL 连接。

    => CREATE AUTHENTICATION no_tls METHOD 'reject' HOST NO TLS '0.0.0.0/0';
    => CREATE AUTHENTICATION no_tls METHOD 'reject' HOST NO TLS '::/128';
    
  6. 使用签署服务器证书的同一 CA 为您的客户端生成和签署证书。

  7. 将 pem 证书链转换为单个 pkcs 12 文件。

  8. 将客户端密钥和链从 pkcs12 文件导入到密钥库 JKS 文件中。有关使用 keytool 命令界面的信息,请参考 Java 文档

    $ keytool -importkeystore -srckeystore -alias my_alias -srcstoretype PKCS12 -srcstorepass my_password -noprompt -deststorepass my_password -destkeypass my_password -destkeystore /tmp/keystore.jks
    
  9. 将 CA 导入到信任库 JKS 文件中。

    $ keytool -import -file certs/intermediate_ca.pem -alias my_alias -trustcacerts -keystore /tmp/truststore.jks -storepass my_truststore_password -noprompt
    

用法注意事项

  • 断开用户会话的连接时,任何未提交的事务会自动回退。

  • 如果数据库不符合 Vertica 许可条款,则 Vertica 会在您建立与数据库的连接时发出 SQLWarning。您可以使用 Connection.getWarnings() 方法检索此警告。有关遵守许可条款的详细信息,请参阅管理许可证

2.1 - JDBC 连接属性

可以使用连接属性来配置 JDBC 客户端应用程序和 Vertica 数据库之间的连接。属性提供有关连接的基本信息,例如,用来连接到数据库的服务器名称和端口号。您还可以使用属性来调整连接的性能以及启用日志记录。

可以采用以下方式之一设置连接属性:

  • 将属性名称和值包括在传递给方法 DriverManager.getConnection() 的连接字符串中。

  • Properties 对象中设置属性,然后将其传递给方法 DriverManager.getConnection()

  • 使用方法 VerticaConnection.setProperty()。使用此方法,您只能更改那些在创建连接后仍可更改的连接属性。

此外,一些标准 JDBC 连接属性在 Connection 界面上具有 getter 和 setter,例如 Connection.setAutoCommit()

连接属性

您只能在打开与数据库的连接之前设置下表中的属性。其中两个属性对每个连接都是必需的。

OAuth 连接属性

以下连接属性与 JDBC 中的 OAuth 相关。

超时属性

使用以下参数,您可以为每个步骤指定各种超时以及 JDBC 与 Vertica 数据库的整体连接。

下图说明了这些属性之间的关系以及它们在 JDBC 尝试连接到 Vertica 数据库时所起的作用: 关于当 JDBC 尝试连接到一个主节点和两个备份节点时超时表现行为的图表

常规属性

可以在建立连接之后设置以下属性。这些属性全都不是必需的。

日志记录属性

必须在打开连接之前设置这些控制客户端日志记录的属性。这些属性全都不是必需的,您无法在 Connection 对象已实例化之后更改所有这些属性。

Kerberos 连接参数

可以使用以下参数为使用 Kerberos 的客户端身份验证设置服务名称主体和主机名主体。

可路由连接 API 连接参数

可以使用以下参数来设置属性,以便为连接启用和配置可路由连接查找。

有关操作这些属性的信息,请参阅设置和获取连接属性值

2.2 - 设置和获取连接属性值

可以采用以下方式之一设置连接属性:

  • 将属性名称和值包括在传递给方法 DriverManager.getConnection() 的连接字符串中。

  • Properties 对象中设置属性,然后将其传递给方法 DriverManager.getConnection()

  • 使用方法 VerticaConnection.setProperty()。使用此方法,您只能更改那些在创建连接后仍可更改的连接属性。

此外,一些标准 JDBC 连接属性在 Connection 界面上具有 getter 和 setter,例如 Connection.setAutoCommit()

在连接时设置属性

创建与 Vertica 的连接时,您可以通过以下方式设置连接属性:

  • 在连接字符串中指定这些属性。

  • 修改传递给 getConnection()Properties 对象。

连接字符串属性

您可以使用用于用户名和密码的相同 URL 参数格式在连接字符串中指定连接属性。例如,以下字符串启用 TLS 连接:

"jdbc:vertica://VerticaHost:5433/db?user=UserName&password=Password&TLSmode=require"

使用 setProperty() 方法设置主机名会替代已在连接字符串中设置的主机名。如果出现这种情况,Vertica 可能无法连接到主机。例如,如果使用以上连接字符串,以下代码会替代 VerticaHost 名称:

Properties props = new Properties();
props.setProperty("dataSource", dataSourceURL);
props.setProperty("database", database);
props.setProperty("user", user);
props.setProperty("password", password);
ps.setProperty("jdbcDriver", jdbcDriver);
props.setProperty("hostName", "NonVertica_host");

但是,如果需要新连接或替代连接,您可以在主机名属性对象中输入有效的主机名。

NonVertica_host 主机名将替代连接字符串中的 VerticaHost 名称。若要避免此问题,请将 props.setProperty("hostName", "NonVertica_host"); 行注释掉:

//props.setProperty("hostName", "NonVertica_host");

属性对象

要使用传递给 getConnection() 调用的 Properties 对象设置连接属性:

  1. 导入 java.util.Properties 类以实例化 Properties 对象。

  2. 使用 put() 方法向对象添加名称-值对。

Properties myProp = new Properties();
myProp.put("user", "ExampleUser");
myProp.put("password", "password123");
myProp.put("LoginTimeout", "35");
Connection conn;
try {
    conn = DriverManager.getConnection(
        "jdbc:vertica://VerticaHost:/ExampleDB", myProp);
} catch (SQLException e) {
    e.printStackTrace();
}

在连接后获取和设置属性

与 Vertica 建立连接后,您可以使用 VerticaConnection 方法 getProperty()setProperty() 分别设置一些连接属性的值。

使用 VerticaConnection.getProperty() 方法,您可以获取部分连接属性的值。使用此方法更改与 Vertica 建立连接后可设置的属性值。

由于这些方法特定于 Vertica,因此您必须使用以下方法之一将 Connection 对象强制转换为 VerticaConnection 接口:

  • Connection 对象导入客户端应用程序。

  • 使用完全限定的引用:com.vertica.jdbc.VerticaConnection

以下示例演示了获取和设置只读属性的值。

import java.sql.*;
import java.util.Properties;
import com.vertica.jdbc.*;

public class SetConnectionProperties {
    public static void main(String[] args) {
        // Note: If your application needs to run under Java 5, you need to
        // load the JDBC driver using Class.forName() here.
        Properties myProp = new Properties();
        myProp.put("user", "ExampleUser");
        myProp.put("password", "password123");
        // Set ReadOnly to true initially
        myProp.put("ReadOnly", "true");
        Connection conn;
        try {
            conn = DriverManager.getConnection(
                            "jdbc:vertica://VerticaHost:5433/ExampleDB",
                            myProp);
            // Show state of the ReadOnly property. This was set at the
            // time the connection was created.
            System.out.println("ReadOnly state: "
                            + ((VerticaConnection) conn).getProperty(
                                            "ReadOnly"));

            // Change it and show it again
            ((VerticaConnection) conn).setProperty("ReadOnly", false);
            System.out.println("ReadOnly state is now: " +
                             ((VerticaConnection) conn).getProperty(
                                             "ReadOnly"));
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

运行以上示例后,将在标准输出上输出以下内容:

ReadOnly state: true
ReadOnly state is now: false

2.3 - 为 JDBC 客户端配置 TLS

要为 JDBC 客户端配置 TLS:

设置密钥库/信任库属性

可以通过以下方式设置密钥库和信任库属性,每种方式各有利弊:

  • 在驱动程序级别。

  • 在 JVM 级别。

驱动程序级别配置

如果您将 DbVizualizer 等工具用于多个连接,请使用 JDBC 连接属性配置密钥库和信任库。但是,这确实会在连接字符串中公开这些值:

  • KeyStorePath

  • KeyStorePassword

  • TrustStorePath

  • TrustStorePassword

例如:

Properties props = new Properties();
props.setProperty("KeyStorePath", keystorepath);
props.setProperty("KeyStorePassword", keystorepassword);
props.setProperty("TrustStorePath", truststorepath);
props.setProperty("TrustStorePassword", truststorepassword);

JVM 级别配置

在 JVM 级别设置密钥库和信任库参数会将它们从连接字符串中排除,这可能更适合安全要求较严格的环境:

  • javax.net.ssl.keyStore

  • javax.net.ssl.trustStore

  • javax.net.ssl.keyStorePassword

  • javax.net.ssl.trustStorePassword

例如:

System.setProperty("javax.net.ssl.keyStore","clientKeyStore.key");
System.setProperty("javax.net.ssl.trustStore","clientTrustStore.key");
System.setProperty("javax.net.ssl.keyStorePassword","new_keystore_password")
System.setProperty("javax.net.ssl.trustStorePassword","new_truststore_password");

设置 TLSmode 连接属性

您可以设置 TLSmode 连接属性以确定如何处理证书。默认情况下,禁用 TLSmode。

TLSmode 标识 Vertica 应用于 JDBC 连接的安全级别。必须将 Vertica 配置为处理 TLS 连接,然后才能与其建立 TLS 加密连接。有关详细信息,请参阅TLS 协议。有效值包括:

  • disable:JDBC 使用纯文本连接并且不实施任何安全措施。

  • require:JDBC 使用 TLS 连接而不验证 CA 证书。

  • verify-ca:JDBC 使用 TLS 连接并确认服务器证书已由证书颁发机构签名。此设置等效于已弃用的 ssl=true 属性。

  • verify-full:JDBC 使用 TLS 连接,确认服务器证书已由证书颁发机构签名,并验证主机名与服务器证书中提供的名称匹配。

如果设置了此属性和 SSL 属性,则此属性优先。

例如,要将 JDBC 配置为使用 TLS 连接服务器而不验证 CA 证书,您可以通过 VerticaConnection.setProperty() 方法将 TLSmode 属性设置为 'require':

Properties props = new Properties();
props.setProperty("TLSmode", "verify-full");

运行 SSL 调试实用程序

配置 TLS 后,您可以为调试实用程序运行以下命令:

$ java -Djavax.net.debug=ssl

通过调试实用程序,可以使用多个调试指示符(选项)。指示符有助于缩小返回的调试信息的范围。例如,可以指定其中一个选项来打印握手消息或会话活动。

有关该调试实用程序及其选项的信息,请参阅 Oracle 文档《JSSE 参考指南》中的“调试实用程序”。

有关解释调试信息的说明,请参阅 Oracle 文档调试 SSL/TLS 连接

2.4 - 设置和返回客户端连接标记

JDBC 客户端具有用于设置和返回客户端连接标签的方法:getClientInfo() 和 setClientInfo()。这些方法可与 SQL 函数 GET_CLIENT_LABELSET_CLIENT_LABEL 配合使用。

使用这两种方法时,请确保向 setter 和 getter 方法都传递字符串值 APPLICATIONNAME

setClientInfo() 用于创建客户端标签,而 getClientInfo() 则用于返回客户端标签:

import java.sql.*;
import java.util.Properties;

public class ClientLabelJDBC {

    public static void main(String[] args) {
        Properties myProp = new Properties();
        myProp.put("user", "dbadmin");
        myProp.put("password", "");
        myProp.put("loginTimeout", "35");
        Connection conn;
        try {
            conn = DriverManager.getConnection(
                    "jdbc:vertica://docc05.verticacorp.com:5433/doccdb", myProp);
            System.out.println("Connected!");
            conn.setClientInfo("APPLICATIONNAME", "JDBC Client - Data Load");
            System.out.println("New Conn label: " + conn.getClientInfo("APPLICATIONNAME"));
            conn.close();
        } catch (SQLTransientConnectionException connException) {
            // There was a potentially temporary network error
            // Could automatically retry a number of times here, but
            // instead just report error and exit.
            System.out.print("Network connection issue: ");
            System.out.print(connException.getMessage());
            System.out.println(" Try again later!");
            return;
        } catch (SQLInvalidAuthorizationSpecException authException) {
            // Either the username or password was wrong
            System.out.print("Could not log into database: ");
            System.out.print(authException.getMessage());
            System.out.println(" Check the login credentials and try again.");
            return;
        } catch (SQLException e) {
            // Catch-all for other exceptions
            e.printStackTrace();
        }
    }
}

运行此方法时,会将以下结果打印到标准输出:

Connected!
New Conn Label: JDBC Client - Data Load

2.5 - 设置 JDBC 会话的区域设置

可以通过以下方法设置连接的区域设置:在打开该连接时将 SET LOCALE 语句包含到 ConnSettings 属性;或者在打开该连接之后的任何时候执行 SET LOCALE 语句。更改 Connection 对象的区域设置会影响使用该对象实例化的所有 Statement 对象。

可以通过执行 SHOW LOCALE 查询来获取区域设置。以下示例演示了使用 ConnSettings 设置区域设置以及执行语句和获取区域设置:

import java.sql.*;
import java.util.Properties;

public class GetAndSetLocale {
    public static void main(String[] args) {

     // If running under a Java 5 JVM, you need to load the JDBC driver
     // using Class.forname here

     Properties myProp = new Properties();
        myProp.put("user", "ExampleUser");
        myProp.put("password", "password123");

        // Set Locale to true en_GB on connection. After the connection
        // is established, the JDBC driver runs the statements in the
        // ConnSettings property.
        myProp.put("ConnSettings", "SET LOCALE TO en_GB");
        Connection conn;
        try {
            conn = DriverManager.getConnection(
                            "jdbc:vertica://VerticaHost:5433/ExampleDB",
                            myProp);

            // Execute a query to get the locale. The results should
            // show "en_GB" as the locale, since it was set by the
            // conn settings property.
            Statement stmt = conn.createStatement();
            ResultSet rs = null;
            rs = stmt.executeQuery("SHOW LOCALE");
            System.out.print("Query reports that Locale is set to: ");
            while (rs.next()) {
                System.out.println(rs.getString(2).trim());
            }

            // Now execute a query to set locale.
            stmt.execute("SET LOCALE TO en_US");

            // Run query again to get locale.
            rs = stmt.executeQuery("SHOW LOCALE");
            System.out.print("Query now reports that Locale is set to: ");
            while (rs.next()) {
                System.out.println(rs.getString(2).trim());
            }
            // Clean up
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

运行以上示例后,将在系统控制台上显示以下内容:

Query reports that Locale is set to: en_GB (LEN)
Query now reports that Locale is set to: en_US (LEN)

注意:

  • JDBC 应用程序使用 UTF-16 字符集编码,并负责将任何非 UTF-16 编码数据转换为 UTF-16。未能转换数据会导致发生错误或不正确地存储数据。

  • 向 Vertica 服务器传递数据时,JDBC 驱动程序会将 UTF-16 数据转换为 UTF-8,并且会将 Vertica 服务器发送的数据从 UTF-8 转换为 UTF-16。

2.6 - 更改事务隔离级别

通过更改事务隔离级别,您可以选择让事务以哪种方式防止来自其他事务的干扰。默认情况下,JDBC 驱动程序会匹配 Vertica 服务器的事务隔离级别。Vertica 的默认事务隔离级别是 READ_COMMITTED,这意味着在提交由某个事务进行的任何更改之前,任何其他事务无法读取这些更改。此级别可防止事务读取由稍后回退的其他事务插入的数据。

Vertica 还支持 SERIALIZABLE 事务隔离级别。此级别可锁定表,以防止其他事务更改查询的 WHERE 子句的结果。锁定表会产生性能影响,因为一次只有一个事务能够访问该表。

事务将保留其隔离级别直至其完成,即使在事务处理期间会话的隔离级别发生更改也是如此。Vertica 内部进程(例如 Tuple Mover刷新操作)以及 DDL 操作始终以 SERIALIZABLE 隔离级别运行以确保一致性。

建立了连接之后,您可以使用 Connection 对象的 setter (setTransactionIsolation()) 和 getter (getTransactionIsolation()) 更改事务隔离级别的连接属性。事务隔离属性的值是一个整数。Connection 接口定义了一些常数,这些常数有助于以更直观的方式设置值:

以下示例演示了将事务隔离级别设置为 SERIALIZABLE。

import java.sql.*;
import java.util.Properties;

public class SetTransactionIsolation {
    public static void main(String[] args) {
        Properties myProp = new Properties();
        myProp.put("user", "ExampleUser");
        myProp.put("password", "password123");
        Connection conn;
        try {
            conn = DriverManager.getConnection(
                            "jdbc:vertica://VerticaHost:5433/ExampleDB",
                            myProp);
            // Get default transaction isolation
            System.out.println("Transaction Isolation Level: "
                            + conn.getTransactionIsolation());
            // Set transaction isolation to SERIALIZABLE
            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
            // Get the transaction isolation again
            System.out.println("Transaction Isolation Level: "
                            + conn.getTransactionIsolation());
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

运行此示例后,以下内容会输出到控制台:

Transaction Isolation Level: 2Transaction Isolation Level: 8

2.7 - JDBC 连接池

池数据源一组持续连接来减少在客户端和服务器之间重复打开网络连接的开销。与持续打开一个小型连接池并使其可供新请求使用相比,为每个请求打开新连接对服务器和客户端来说成本更高。当请求到达时,将为其分配该池中预先存在的连接之一。仅当该池中没有可用连接时,才会创建新连接。该请求完成后,连接将返回到池中并等待为其他请求提供服务。

Vertica JDBC 驱动程序支持 JDBC 4.0 标准中定义的连接池。如果要将基于 J2EE 的应用程序服务器与 Vertica 结合使用,该应用程序服务器应已有内置的数据池功能。唯一的要求是该应用程序服务器可与 Vertica 的 JDBC 驱动程序所实施的 PooledConnection 接口配合工作。应用程序服务器的池功能通常已针对该服务器设计用于处理的工作负载进行了适当优化。有关如何使用池连接的详细信息,请参阅应用程序服务器的文档。一般情况下,应在代码中以透明方式使用池连接,您只需打开连接,应用程序将处理池连接的详细信息。

如果不想使用应用程序服务器,或者应用程序服务器未提供与 Vertica 兼容的连接池,您可以使用第三方池库(例如,开源 c3p0 库或 DBCP 库)来实施连接池。

2.8 - JDBC 中的负载均衡

本机连接负载均衡

本机连接负载均衡有助于在 Vertica 数据库中的主机上分散客户端连接所带来的开销。服务器和客户端都必须启用本机连接负载均衡。如果两者者都启用了本机负载均衡,则当客户端最初连接到数据库中的主机时,此主机会从数据库中当前正在运行的主机列表中选择一个主机来处理客户端连接,并通知客户端它所选择的主机。

如果最初联系的主机没有选择自身来处理连接,则客户端会断开连接,然后打开指向第一个主机所选主机的另一个连接。指向此第二个主机的连接进程将照常进行—如果启用了 SSL,则会启动 SSL 协商,否则客户端会启动身份验证过程。有关详细信息,请参阅关于本机连接负载均衡

若要在客户端上启用本机连接负载均衡,请将 ConnectionLoadBalance 连接参数设置为 true。以下示例演示了下列操作:

  • 在启用本机连接负载均衡的情况下多次连接到数据库。

  • 从 V_MONITOR.CURRENT_SESSION 系统表中获取处理连接的节点的名称。


import java.sql.*;
import java.util.Properties;
import java.sql.*;
import java.util.Properties;

public class JDBCLoadingBalanceExample {
    public static void main(String[] args) {
        Properties myProp = new Properties();
        myProp.put("user", "dbadmin");
        myProp.put("password", "example_password123");
        myProp.put("loginTimeout", "35");
        myProp.put("ConnectionLoadBalance", "1");
        Connection conn;

        for (int x = 1; x <= 4; x++) {
            try {
                System.out.print("Connect attempt #" + x + "...");
                conn = DriverManager.getConnection(
                    "jdbc:vertica://node01.example.com:5433/vmart", myProp);
                Statement stmt = conn.createStatement();
                // Set the load balance policy to round robin before testing the database's load balancing.
                stmt.execute("SELECT SET_LOAD_BALANCE_POLICY('ROUNDROBIN');");
                // Query system to table to see what node we are connected to. Assume a single row
                // in response set.
                ResultSet rs = stmt.executeQuery("SELECT node_name FROM v_monitor.current_session;");
                rs.next();
                System.out.println("Connected to node " + rs.getString(1).trim());
                conn.close();
            } catch (SQLTransientConnectionException connException) {
                // There was a potentially temporary network error
                // Could automatically retry a number of times here, but
                // instead just report error and exit.
                System.out.print("Network connection issue: ");
                System.out.print(connException.getMessage());
                System.out.println(" Try again later!");
                return;
            } catch (SQLInvalidAuthorizationSpecException authException) {
                // Either the username or password was wrong
                System.out.print("Could not log into database: ");
                System.out.print(authException.getMessage());
                System.out.println(" Check the login credentials and try again.");
                return;
            } catch (SQLException e) {
                // Catch-all for other exceptions
                e.printStackTrace();
            }
        }
    }
}

运行上述示例得到了以下输出:


Connect attempt #1...Connected to node v_vmart_node0002
Connect attempt #2...Connected to node v_vmart_node0003
Connect attempt #3...Connected to node v_vmart_node0001
Connect attempt #4...Connected to node v_vmart_node0002

基于主机名的负载均衡

您还可以通过将单个主机名解析为多个 IP 地址来对工作负载进行负载均衡。当您为 DriverManager.getConnection() 方法指定主机名时,主机名会解析为来自每个连接的随机列出的 IP 地址。

例如,主机名 verticahost.example.cometc/hosts 中有以下条目:

192.0.2.0 verticahost.example.com
192.0.2.1 verticahost.example.com
192.0.2.2 verticahost.example.com

verticahost.example.com 指定为 DriverManager.getConnection() 的连接会随机解析为列出的 IP 地址之一。

2.9 - JDBC 连接故障转移

如果客户端应用程序尝试连接到 Vertica 群集中处于关闭状态的主机,则使用默认连接配置时连接尝试将会失败。此故障通常会向用户返回一个错误。用户必须等待主机恢复并重试连接,或者手动编辑连接设置以选择其他主机。

由于 Vertica 分析型数据库采用分布式架构,通常您不会关注哪个数据库主机处理客户端应用程序的连接。可以使用客户端驱动程序的连接故障转移功能来防止用户在无法访问连接设置中所指定主机的情况下收到连接错误。JDBC 驱动程序可通过几种方式让客户端驱动程序在无法访问连接参数中所指定的主机时自动尝试连接到其他主机:

  • 将 DNS 服务器配置为返回一个主机名的多个 IP 地址。在连接设置中使用此主机名时,客户端会尝试连接到 DNS 找到的第一个 IP 地址。如果位于该 IP 地址的主机无法访问,则客户端会尝试连接到第二个 IP,依此类推,直到其成功地连接到某个主机或试过所有 IP 地址为止。

  • 提供当您在连接参数中指定的主要主机无法访问时客户端驱动程序尝试连接的备份主机列表。

  • (仅限 JDBC)在尝试连接到下一个节点之前,使用特定于驱动程序的连接属性来管理超时。

在所有方法中,故障转移过程对于客户端应用程序是透明的(除了在选择使用列表故障转移方法时需指定备份主机列表之外)。如果主要主机无法访问,客户端驱动程序会自动尝试连接到其他主机。

故障转移仅适用于初次建立客户端连接的情况。如果连接断开,驱动程序不会自动尝试重新连接到数据库中的其他主机。

通常可选择上述两种故障转移方法中的一种。但它们可以一起使用。如果您的 DNS 服务器返回多个 IP 地址,并且您提供了备份主机列表,则客户端会首先尝试连接 DNS 服务器返回的所有 IP,然后再尝试连接备份列表中的主机。

DNS 故障转移方法可集中处理配置客户端故障转移。在向 Vertica 分析型数据库群集添加新节点时,可以选择通过编辑 DNS 服务器设置来将这些节点添加到故障转移列表中。所有使用 DNS 服务器连接到 Vertica 分析型数据库的客户端系统都会自动采用连接故障转移,而无需更改任何设置。但此方法需要对所有客户端用于连接到 Vertica 分析型数据库群集的 DNS 服务器具有管理访问权限。这在贵组织中可能无法实现。

使用备份服务器列表要比编辑 DNS 服务器设置更加容易。但此方法不能集中处理故障转移功能。如果更改了 Vertica 分析型数据库群集,您可能需要在每个客户端系统上更新应用程序设置。

使用 DNS 故障转移

若要使用 DNS 故障转移,您需要更改 DNS 服务器的设置,将单一主机名映射为 Vertica 分析型数据库群集中主机的多个 IP 地址。然后让所有客户端应用程序都使用该主机名连接到 Vertica 分析型数据库。

您可以选择让 DNS 服务器为主机名返回任何所需数量的 IP 地址。在小型群集中,您可以选择让其返回群集中所有主机的 IP 地址。但对于大型群集,应考虑选择返回一部分主机。否则可能会导致长时间延迟,因为客户端驱动程序尝试连接到数据库中处于关闭状态的每个主机会失败。

使用备份主机列表

若要启用基于备份列表的连接故障转移,需要在客户端应用程序的 BackupServerNode 参数中指定主机的至少一个 IP 地址或主机名。可以视情况在主机名或 IP 后面使用冒号和端口号。如果未提供端口号,驱动程序会默认使用标准 Vertica 端口号 (5433)。若要列出多个主机,请用逗号分隔这些主机。

以下示例演示了如何通过设置 BackupServerNode 连接参数来指定可尝试连接的其他主机。由于有意在连接字符串中使用了一个不存在的节点,因此初始连接失败。客户端驱动程序必须尝试通过备份主机来与 Vertica 建立连接。

import java.sql.*;
import java.util.Properties;

public class ConnectionFailoverExample {
    public static void main(String[] args) {
        // Assume using JDBC 4.0 driver on JVM 6+. No driver loading needed.
        Properties myProp = new Properties();
        myProp.put("user", "dbadmin");
        myProp.put("password", "vertica");
        // Set two backup hosts to be used if connecting to the first host
        // fails. All of these hosts will be tried in order until the connection
        // succeeds or all of the connections fail.
        myProp.put("BackupServerNode", "VerticaHost02,VerticaHost03");
        Connection conn;
        try {
            // The connection string is set to try to connect to a known
            // bnad host (in this case, a host that never existed).
            conn = DriverManager.getConnection(
                    "jdbc:vertica://BadVerticaHost:5433/vmart", myProp);
            System.out.println("Connected!");
            // Query system to table to see what node we are connected to.
            // Assume a single row in response set.
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(
                    "SELECT node_name FROM v_monitor.current_session;");
            rs.next();
            System.out.println("Connected to node " + rs.getString(1).trim());
            // Done with connection.
            conn.close();
        } catch (SQLException e) {
            // Catch-all for other exceptions
            e.printStackTrace();
        }
    }
}

运行以上示例后,将在系统控制台上生成类似于以下内容的输出:

Connected!
Connected to node v_vmart_node0002

请注意,系统会与备份列表中的第一个节点建立连接(节点 2)。

指定连接超时

LoginTimeout 控制 JDBC 与节点建立 TCP 连接并登录 Vertica 的超时时间。

LoginNodeTimeout 控制 JDBC 登录 Vertica 数据库的超时时间。在指定的超时之后,JDBC 尝试连接到由连接属性 BackupServerNode 或 DNS 解析确定的“下一个”节点。这在节点已启动的情况下很有用,但 Vertica 进程会出现问题。

LoginNetworkTimeout 控制 JDBC 与 Vertica 节点建立 TCP 连接的超时时间。如果您未设置此连接属性,且 JDBC 客户端尝试连接的节点已关闭,则 JDBC 客户端将“无限期”等待,但实际上使用的是系统默认的 70 秒超时。LoginNetworkTimeout 的一个典型用例是,如果当前 Vertica 节点因维护而停机并且无法修改 JDBC 应用程序的连接字符串,则让 JDBC 连接到另一个节点。

NetworkTimeout 控制 Vertica 在建立连接并登录到数据库后响应客户端请求的超时时间。

要在连接字符串中设置这些参数:

# LoginTimeout is 30 seconds, LoginNodeTimeout is 10 seconds, LoginNetworkTimeout is 2 seconds, NetworkTimeout is 0.5 seconds
Connection conn = DriverManager.getConnection("jdbc:vertica://VerticaHost:5433/verticadb?user=dbadmin&loginTimeout=30&loginNodeTimeout=10"&loginNetworkTimeout=2&networkTimeout=500");

要将这些参数设置为连接属性:


Properties myProp = new Properties();
myProp.put("user", "dbadmin");
myProp.put("loginTimeout", "30"); // overall connection timeout is 30 seconds to make sure it is not too small for failover
myProp.put("loginNodeTimeout", "10"); // JDBC waits 10 seconds before attempting to connect to the next node if the Vertica process is running but does not respond
myProp.put("loginNetworkTimeout", "2"); // node connection timeout is 2 seconds
myProp.put("networkTimeout", "500"); // after the client has logged in, Vertica has 0.5 seconds to respond to each request
Connection conn = DriverManager.getConnection("jdbc:vertica://VerticaHost:5433/verticadb", myProp);

与负载均衡的交互

启用本机连接负载均衡时,在 BackupServerNode 连接参数中指定的其他服务器将只用于指向 Vertica 主机的初始连接。如果主机将客户端重定向至数据库群集中的其他主机来处理其连接请求,则第二个连接将不使用备份节点列表。这很少出现问题,因为本机连接负载均衡知道数据库中的哪个节点目前正处于运行状态。

有关详细信息,请参阅JDBC 中的负载均衡

3 - JDBC 数据类型

JDBC 驱动程序以透明方式将大部分 Vertica 数据类型转换为相应的 Java 数据类型。在少数情况下,Vertica 数据类型无法直接转换为 Java 数据类型;本节将介绍这些例外。

3.1 - VerticaTypes 类

JDBC 不支持 Vertica 所支持的所有数据类型。Vertica JDBC 客户端驱动程序包含一个名为VerticaTypes 的附加类,可帮助您处理识别这些特定于Vertica 的数据类型。它包含可在代码中用于指定 Vertica 数据类型的常量。此类定义了两种不同类别的数据类型:

  • Vertica 的 13 种时间间隔值。此类包含每种类型的常量属性。当实例化 VerticaDayTimeIntervalVerticaYearMonthInterval 类的成员时,您可以使用这些常量来选择特定的时间间隔类型:

    // Create a day to second interval.
    VerticaDayTimeInterval dayInt = new VerticaDayTimeInterval(
            VerticaTypes.INTERVAL_DAY_TO_SECOND, 10, 0, 5, 40, 0, 0, false);
    // Create a year to month interval.
    VerticaYearMonthInterval monthInt = new VerticaYearMonthInterval(
            VerticaTypes.INTERVAL_YEAR_TO_MONTH, 10, 6, false);
    
  • Vertica UUID 数据类型。使用 VerticaTypes.UUID 的一种方法是查询表的元数据以查看列是否为 UUID。有关示例,请参阅 UUID 值

有关此类的详细信息,请参阅 JDBC 文档。

3.2 - 数值数据别名转换

Vertica 服务器支持对整数、浮点和数字类型使用数据类型别名。JDBC 驱动程序会将这些数据类型报告为基本数据类型(BIGINT、DOUBLE PRECISION 和 NUMERIC),如下所示:

如果客户端应用程序将值检索为较小数据类型,Vertica JDBC 驱动程序不会检查溢出。以下示例演示了此溢出的结果。

import java.sql.*;
import java.util.Properties;

public class JDBCDataTypes {
    public static void main(String[] args) {
        // If running under a Java 5 JVM, use you need to load the JDBC driver
        // using Class.forname here

        Properties myProp = new Properties();
        myProp.put("user", "ExampleUser");
        myProp.put("password", "password123");
        Connection conn;
        try {
            conn = DriverManager.getConnection(
                            "jdbc:vertica://VerticaHost:5433/VMart",
                             myProp);
            Statement statement = conn.createStatement();
            // Create a table that will hold a row of different types of
            // numeric data.
            statement.executeUpdate(
                    "DROP TABLE IF EXISTS test_all_types cascade");
            statement.executeUpdate("CREATE TABLE test_all_types ("
                            + "c0 INTEGER, c1 TINYINT, c2 DECIMAL, "
                            + "c3 MONEY, c4 DOUBLE PRECISION, c5 REAL)");
            // Add a row of values to it.
            statement.executeUpdate("INSERT INTO test_all_types VALUES("
                            + "111111111111, 444, 55555555555.5555, "
                            + "77777777.77,  88888888888888888.88, "
                            + "10101010.10101010101010)");
            // Query the new table to get the row back as a result set.
            ResultSet rs = statement
                            .executeQuery("SELECT * FROM test_all_types");
            // Get the metadata about the row, including its data type.
            ResultSetMetaData md = rs.getMetaData();
            // Loop should only run once...
            while (rs.next()) {
                // Print out the data type used to defined the column, followed
                // by the values retrieved using several different retrieval
                // methods.

                String[] vertTypes = new String[] {"INTEGER", "TINYINT",
                         "DECIMAL", "MONEY", "DOUBLE PRECISION", "REAL"};

                for (int x=1; x<7; x++) {
                    System.out.println("\n\nColumn " + x + " (" + vertTypes[x-1]
                            + ")");
                    System.out.println("\tgetColumnType()\t\t"
                            + md.getColumnType(x));
                    System.out.println("\tgetColumnTypeName()\t"
                            + md.getColumnTypeName(x));
                    System.out.println("\tgetShort()\t\t"
                            + rs.getShort(x));
                    System.out.println("\tgetLong()\t\t" + rs.getLong(x));
                    System.out.println("\tgetInt()\t\t" + rs.getInt(x));
                    System.out.println("\tgetByte()\t\t" + rs.getByte(x));
                }
            }
            rs.close();
            statement.executeUpdate("drop table test_all_types cascade");
            statement.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

运行以上示例后,将在控制台上输出以下内容:

Column 1 (INTEGER)
       getColumnType()        -5
    getColumnTypeName()    BIGINT
    getShort()        455
    getLong()        111111111111
    getInt()        -558038585
    getByte()        -57
Column 2 (TINYINT)
    getColumnType()        -5
    getColumnTypeName()    BIGINT
    getShort()        444
    getLong()        444
    getInt()        444
    getByte()        -68
Column 3 (DECIMAL)
    getColumnType()        2
    getColumnTypeName()    NUMERIC
    getShort()        -1
    getLong()        55555555555
    getInt()        2147483647
    getByte()        -1
Column 4 (MONEY)
    getColumnType()        2
    getColumnTypeName()    NUMERIC
    getShort()        -13455
    getLong()        77777777
    getInt()        77777777
    getByte()        113
Column 5 (DOUBLE PRECISION)
    getColumnType()        8
    getColumnTypeName()    DOUBLE PRECISION
    getShort()        -1
    getLong()        88888888888888900
    getInt()        2147483647
    getByte()        -1
Column 6 (REAL)
    getColumnType()        8
    getColumnTypeName()    DOUBLE PRECISION
    getShort()        8466
    getLong()        10101010
    getInt()        10101010
    getByte()        18

3.3 - 将时间间隔与 JDBC 配合使用

JDBC 标准不包含用于时间间隔(两个时间点之间的持续时间)的数据类型。若要处理 Vertica 的 INTERVAL 数据类型,您必须使用 JDBC 特定于数据库的对象类型。

从结果集中读取时间间隔值时,使用 ResultSet.getObject() 方法检索该值,然后将其强制转换为 Vertica 时间间隔类之一: VerticaDayTimeInterval (代表所有十种日期/时间的时间间隔)或 VerticaYearMonthInterval(代表所有三种年/月时间间隔)。

在批量插入中使用时间间隔

将多个批插入到包含时间间隔数据的表时,您必须创建 VerticaDayTimeIntervalVerticaYearMonthInterval 类的实例以用于存放要插入的数据。您可以在调用类的构造函数时设置值,或者也可以在使用 setter 之后设置值。然后,可以使用 PreparedStatement.setObject() 方法插入时间间隔值。您还可以使用 .setString() 方法,并向其传递采用 "DD ``HH:MM:SS""YY-MM" 格式的字符串。

以下示例演示了将数据插入到包含日/时间间隔和年/月时间间隔的表:

import java.sql.*;
import java.util.Properties;
// You need to import the Vertica JDBC classes to be able to instantiate
// the interval classes.
import com.vertica.jdbc.*;

public class IntervalDemo {
    public static void main(String[] args) {
        // If running under a Java 5 JVM, use you need to load the JDBC driver
        // using Class.forname here
        Properties myProp = new Properties();
        myProp.put("user", "ExampleUser");
        myProp.put("password", "password123");
        Connection conn;
        try {
            conn = DriverManager.getConnection(
                    "jdbc:vertica://VerticaHost:5433/VMart", myProp);
            // Create table for interval values
            Statement stmt = conn.createStatement();
            stmt.execute("DROP TABLE IF EXISTS interval_demo");
            stmt.executeUpdate("CREATE TABLE interval_demo("
                    + "DayInt INTERVAL DAY TO SECOND, "
                    + "MonthInt INTERVAL YEAR TO MONTH)");
            // Insert data into interval columns using
            // VerticaDayTimeInterval and VerticaYearMonthInterval
            // classes.
            PreparedStatement pstmt = conn.prepareStatement(
                    "INSERT INTO interval_demo VALUES(?,?)");
            // Create instances of the Vertica classes that represent
            // intervals.
            VerticaDayTimeInterval dayInt = new VerticaDayTimeInterval(10, 0,
                    5, 40, 0, 0, false);
            VerticaYearMonthInterval monthInt = new VerticaYearMonthInterval(
                    10, 6, false);
            // These objects can also be manipulated using setters.
            dayInt.setHour(7);
            // Add the interval values to the batch
            ((VerticaPreparedStatement) pstmt).setObject(1, dayInt);
            ((VerticaPreparedStatement) pstmt).setObject(2, monthInt);
            pstmt.addBatch();
            // Set another row from strings.
            // Set day interval in "days HH:MM:SS" format
            pstmt.setString(1, "10 10:10:10");
            // Set year to month value in "MM-YY" format
            pstmt.setString(2, "12-09");
            pstmt.addBatch();
            // Execute the batch to insert the values.
            try {
                pstmt.executeBatch();
            } catch (SQLException e) {
                System.out.println("Error message: " + e.getMessage());
            }

读取时间间隔值

可以使用 ResultSet.getObject() 方法从结果集读取时间间隔值,然后将对象转换为适当的 Vertica 对象类: VerticaDayTimeInterval (代表日期/时间的时间间隔)或 VerticaYearMonthInterval(代表年/月时间间隔)。如果您知道列包含时间间隔并且知道该时间间隔的类型,则可以轻松完成此操作。如果应用程序无法确定从中读取的结果集的数据结构,您可以测试列是否包含特定于数据库的对象类型,如果是的话,则可以确定该对象属于 VerticaDayTimeInterval 类还是 VerticaYearMonthInterval 类。

            // Retrieve the interval values inserted by previous demo.
            // Query the table to get the row back as a result set.
            ResultSet rs = stmt.executeQuery("SELECT * FROM interval_demo");
            // If you do not know the types of data contained in the result set,
            // you can read its metadata to determine the type, and use
            // additional information to determine the interval type.
            ResultSetMetaData md = rs.getMetaData();
            while (rs.next()) {
                for (int x = 1; x <= md.getColumnCount(); x++) {
                    // Get data type from metadata
                    int colDataType = md.getColumnType(x);
                    // You can get the type in a string:
                    System.out.println("Column " + x + " is a "
                            + md.getColumnTypeName(x));
                    // Normally, you'd have a switch statement here to
                    // handle all sorts of column types, but this example is
                    // simplified to just handle database-specific types
                    if (colDataType == Types.OTHER) {
                        // Column contains a database-specific type. Determine
                        // what type of interval it is. Assuming it is an
                        // interval...
                        Object columnVal = rs.getObject(x);
                        if (columnVal instanceof VerticaDayTimeInterval) {
                            // We know it is a date time interval
                            VerticaDayTimeInterval interval =
                                    (VerticaDayTimeInterval) columnVal;
                            // You can use the getters to access the interval's
                            // data
                            System.out.print("Column " + x + "'s value is ");
                            System.out.print(interval.getDay() + " Days ");
                            System.out.print(interval.getHour() + " Hours ");
                            System.out.println(interval.getMinute()
                                    + " Minutes");
                        } else if (columnVal instanceof VerticaYearMonthInterval) {
                            VerticaYearMonthInterval interval =
                                    (VerticaYearMonthInterval) columnVal;
                            System.out.print("Column " + x + "'s value is ");
                            System.out.print(interval.getYear() + " Years ");
                            System.out.println(interval.getMonth() + " Months");
                        } else {
                            System.out.println("Not an interval.");
                        }
                    }
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

该示例将在控制台上输出以下内容:

Column 1 is a INTERVAL DAY TO SECOND
Column 1's value is 10 Days 7 Hours 5 Minutes
Column 2 is a INTERVAL YEAR TO MONTH
Column 2's value is 10 Years 6 Months
Column 1 is a INTERVAL DAY TO SECOND
Column 1's value is 10 Days 10 Hours 10 Minutes
Column 2 is a INTERVAL YEAR TO MONTH
Column 2's value is 12 Years 9 Months

另一个选项是使用数据库元数据来查找包含时间间隔的列。

// Determine the interval data types by examining the database
// metadata.
DatabaseMetaData dbmd = conn.getMetaData();
ResultSet dbMeta = dbmd.getColumns(null, null, "interval_demo", null);
int colcount = 0;
while (dbMeta.next()) {

    // Get the metadata type for a column.
    int javaType = dbMeta.getInt("DATA_TYPE");

    System.out.println("Column " + ++colcount + " Type name is " +
                    dbMeta.getString("TYPE_NAME"));

    if(javaType == Types.OTHER) {
      // The SQL_DATETIME_SUB column in the metadata tells you
      // Specifically which subtype of interval you have.
      // The VerticaDayTimeInterval.isDayTimeInterval()
      // methods tells you if that value is a day time.
      //
      int intervalType = dbMeta.getInt("SQL_DATETIME_SUB");
      if(VerticaDayTimeInterval.isDayTimeInterval(intervalType)) {
           // Now you know it is one of the 10 day/time interval types.
           // When you select this column you can cast to
           // VerticaDayTimeInterval.
           // You can get more specific by checking intervalType
           // against each of the 10 constants directly, but
           // they all are represented by the same object.
           System.out.println("column " + colcount + " is a " +
                           "VerticaDayTimeInterval intervalType = "
                          + intervalType);
      } else if(VerticaYearMonthInterval.isYearMonthInterval(
                      intervalType)) {
          //now you know it is one of the 3 year/month intervals,
          //and you can select the column and cast to
          // VerticaYearMonthInterval
          System.out.println("column " + colcount + " is a " +
                          "VerticaDayTimeInterval intervalType = "
                          + intervalType);
      } else {
          System.out.println("Not an interval type.");
      }
    }
}

3.4 - UUID 值

UUID 是 Vertica 中的核心数据类型。不过,它不是核心 Java 数据类型。您必须使用 java.util.UUID 类来表示 Java 代码中的 UUID 值。JDBC 驱动程序不会将 Vertica 中的值转换为非核心 Java 数据类型。因此,您必须使用通用对象方法(例如 PreparedStatement.setObject())将 UUID 值发送到 Vertica。您还可以使用通用对象方法(例如 ResultSet.getObject())从 Vertica 检索 UUID 值。然后,将检索到的对象转换为 java.util.UUID 类的成员。

以下示例代码演示如何将 UUID 值插入 Vertica 以及从 Vertica 中检索 UUID 值。

package jdbc_uuid_example;

import java.sql.*;
import java.util.Properties;

public class VerticaUUIDExample {

    public static void main(String[] args) {

        Properties myProp = new Properties();
        myProp.put("user", "dbadmin");
        myProp.put("password", "");
        Connection conn;

        try {
            conn = DriverManager.getConnection("jdbc:vertica://doch01:5433/VMart",
                                                myProp);
            Statement stmt = conn.createStatement();

            // Create a table with a UUID column and a VARCHAR column.
            stmt.execute("DROP TABLE IF EXISTS UUID_TEST CASCADE;");
            stmt.execute("CREATE TABLE UUID_TEST (id UUID, description VARCHAR(25));");

            // Prepare a statement to insert a UUID and a string into the table.
            PreparedStatement ps = conn.prepareStatement("INSERT INTO UUID_TEST VALUES(?,?)");

            java.util.UUID uuid;  // Holds the UUID value.

            for (Integer x = 0; x < 10; x++) {
                // Generate a random uuid
                uuid = java.util.UUID.randomUUID();
                // Set the UUID value by calling setObject.
                ps.setObject(1, uuid);
                // Set the String value to indicate which UUID this is.
                ps.setString(2, "UUID #" + x);
                ps.execute();
            }

            // Query the uuid
            ResultSet rs = stmt.executeQuery("SELECT * FROM UUID_TEST ORDER BY description ASC");
            while (rs.next()) {
                // Cast the object from the result set as a UUID.
                uuid = (java.util.UUID) rs.getObject(1);
                System.out.println(rs.getString(2) + " : " +  uuid.toString());
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

以上示例生成类似如下的输出:

UUID #0 : 67b6dcb6-c28c-4965-b9f7-5c830a04664d
UUID #1 : 485d3835-2887-4233-b003-392254fa97e0
UUID #2 : 81421f51-c803-473d-8cfc-2c184582a117
UUID #3 : bec8b86a-b650-47b0-852c-8229155332d9
UUID #4 : 8ae5e3ec-d143-4ef7-8901-24f6d0483abf
UUID #5 : 669696ce-5e86-4e87-b8d0-a937f5fc18d7
UUID #6 : 19609ec9-ec56-4444-9cfe-ad2b8de537dd
UUID #7 : 97182e1d-5c7e-4da1-9922-67e804fde173
UUID #8 : c76c3a2b-a9ef-4d65-b2fb-7c637f872b3c
UUID #9 : 3cbbcd26-c177-4277-b3df-bf4d9389f69d

确定列是否具有 UUID 数据类型

JDBC 不支持 UUID 数据类型。此限制意味着您不能使用通常的 ResultSetMetaData.getColumnType() 方法来确定列的数据类型是否为 UUID。在 UUID 列上调用此方法会返回 Types.OTHER。该值也用于标识时间间隔列。可以使用两种方法来确定列是否包含 UUID:

  • 使用 ResultSetMetaData.getColumnTypeName() 获取列的数据类型的名称。对于 UUID 列,此方法将值 "Uuid" 作为String返回。

  • 查询表的元数据以获取列的 SQL 数据类型。如果此值等于 VerticaTypes.UUID,则该列的数据类型为 UUID。

下面的示例演示了全部两种方法:

            // This example assumes you already have a database connection
            // and result set from a query on a table that may contain a UUID.

            //  Get the metadata of the result set to get the column definitions
            ResultSetMetaData meta = rs.getMetaData();
            int colcount;
            int maxcol = meta.getColumnCount();

            System.out.println("Using column metadata:");
            for (colcount = 1; colcount < maxcol; colcount++) {
            // .getColumnType() always returns "OTHER" for UUID columns.
                if (meta.getColumnType(colcount) == Types.OTHER) {
                    // To determine that it is a UUID column, test the name of the column type.
                    if (meta.getColumnTypeName(colcount).equalsIgnoreCase("uuid")) {
                        // It's a UUID column
                        System.out.println("Column "+ colcount + " is UUID");
                    }
                }
            }

            // You can also query the table's metadata to find its column types and compare
            // it to the VerticaType.UUID constant to see if it is a UUID column.
            System.out.println("Using table metadata:");
            DatabaseMetaData dbmd = conn.getMetaData();
            // Get the metdata for the previously-created test table.
            ResultSet tableMeta = dbmd.getColumns(null, null, "UUID_TEST", null);
            colcount = 0;
            // Each row in the result set has metadata that describes a single column.
            while (tableMeta.next()) {
                colcount++;
                // The SQL_DATA_TYPE column holds the Vertica database data type. You compare
                // this value to the VerticvaTypes.UUID constant to see if it is a UUID.
                if (tableMeta.getInt("SQL_DATA_TYPE") == VerticaTypes.UUID) {
                    // Column is a UUID data type...
                    System.out.println("Column " + colcount + " is a UUID column.");
                }
            }

如果在运行上面的示例之后运行此示例,则会将以下内容输出到控制台:

Using column metadata:
Column 1 is UUID
Using table metadata:
Column 1 is a UUID column.

3.5 - JDBC 中的复杂类型

java.sql 查询的结果存储在 ResultSet 中。如果 ResultSet 包含复杂类型的列,您可以使用以下方法之一检索它:

  • 对于 ARRAYSETMAP 类型的列,使用 getArray(),它返回一个 java.sql.Array

  • 对于 ROW 类型的列,使用 getObject(),它返回一个 java.sql.Struct

类型转换表

java.sql.Arrayjava.sql.Struct 对象都有其自己的用于访问复杂类型数据的 API。在每种情况下,数据都以 java.lang.Object 的形式返回,其类型需要强制转换为 Java 类型。预期的确切 Java 类型取决于复杂类型定义中使用的 Vertica 类型,如下面的类型转换表所示:

ARRAY、SET 和 MAP 列

例如,以下方法运行返回某些 Vertica 类型的 ARRAY 的查询,然后在使用 getArray() 检索时这些类型由 JDBC 驱动程序强制转换为其对应 Java 类型的数组。此特定示例以 ARRAY[INT] 和 ARRAY[FLOAT] 开头,因此其类型分别强制转换为由类型转换表确定的 Long[]Double[]

  • getArrayResultSetExample() 显示了如何将 ARRAY 处理为 java.sql.ResultSet。此示例使用 getResultSet() 将底层数组作为另一个 ResultSet 返回。您可以使用此底层 ResultSet 执行以下操作:

    • 检索父 ResultSet

    • 将其视为 Object 数组或 ResultSet

  • getArrayObjectExample() 展示了如何将 ARRAY 作为原生 Java 数组进行处理。此示例使用 getArray() 将底层数组作为 Object 数组(而不是 ResultSet 数组)返回。它具有以下含义:

    • 您不能使用底层 Object 数组来检索其父数组。

    • 所有底层数组都被视为 Object 数组(而不是 ResultSet)。

package com.vertica.jdbc.test.samples;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.Array;
import java.sql.Struct;


public class ComplexTypesArraySamples
{
    /**
     * Executes a query and gets a java.sql.Array from the ResultSet. It then uses the Array#getResultSet
     * method to get a ResultSet containing the contents of the array.
     * @param conn A Connection to a Vertica database
     * @throws SQLException
     */
    public static void getArrayResultSetExample (Connection conn) throws SQLException {
        Statement stmt = conn.createStatement();

        final String queryText = "SELECT ARRAY[ARRAY[1,2,3],ARRAY[4,5,6],ARRAY[7,8,9]]::ARRAY[ARRAY[INT]] as array";
        final String targetColumnName = "array";

        System.out.println ("queryText: " + queryText);
        ResultSet rs = stmt.executeQuery(queryText);
        int targetColumnId = rs.findColumn (targetColumnName);

        while (rs.next ()) {
            Array currentSqlArray = rs.getArray (targetColumnId);
            ResultSet level1ResultSet = currentSqlArray.getResultSet();
            if (level1ResultSet != null) {
                while (level1ResultSet.next ()) {
                    // The first column of the result set holds the row index
                    int i = level1ResultSet.getInt(1) - 1;
                    Array level2SqlArray = level1ResultSet.getArray (2);
                    Object level2Object = level2SqlArray.getArray ();
                    // For this ARRAY[INT], the driver returns a Long[]
                    assert (level2Object instanceof Long[]);
                    Long [] level2Array = (Long [])level2Object;
                    System.out.println (" level1Object [" + i + "]: " + level2SqlArray.toString () + " (" + level2SqlArray.getClass() + ")");

                    for (int j = 0; j < level2Array.length; j++) {
                       System.out.println (" Value [" + i + ", " + j + "]: " + level2Array[j] + " (" + level2Array[j].getClass() + ")");
                   }
                }
            }
        }
    }

    /**
     * Executes a query and gets a java.sql.Array from the ResultSet. It then uses the Array#getArray
     * method to get the contents of the array as a Java Object [].
     * @param conn A Connection to a Vertica database
     * @throws SQLException
     */
    public static void getArrayObjectExample (Connection conn) throws SQLException {
        Statement stmt = conn.createStatement();

        final String queryText = "SELECT ARRAY[ARRAY[0.0,0.1,0.2],ARRAY[1.0,1.1,1.2],ARRAY[2.0,2.1,2.2]]::ARRAY[ARRAY[FLOAT]] as array";
        final String targetColumnName = "array";

        System.out.println ("queryText: " + queryText);
        ResultSet rs = stmt.executeQuery(queryText);
        int targetColumnId = rs.findColumn (targetColumnName);

        while (rs.next ()) {
            // Get the java.sql.Array from the result set
            Array currentSqlArray = rs.getArray (targetColumnId);
            // Get the internal Java Object implementing the array
            Object level1ArrayObject = currentSqlArray.getArray ();
            if (level1ArrayObject != null) {
                // All returned instances are Object[]
                assert (level1ArrayObject instanceof Object[]);
                Object [] level1Array = (Object [])level1ArrayObject;
                System.out.println ("Vertica driver returned a: " + level1Array.getClass());

                for (int i = 0; i < level1Array.length; i++) {
                    Object level2Object = level1Array[i];
                    // For this ARRAY[FLOAT], the driver returns a Double[]
                    assert (level2Object instanceof Double[]);
                    Double [] level2Array = (Double [])level2Object;
                    for (int j = 0; j < level2Array.length; j++) {
                         System.out.println (" Value [" + i + ", " + j + "]: " + level2Array[j] + " (" + level2Array[j].getClass() + ")");
                    }
                }
            }
        }
    }
}

getArrayResultSetExample() 的输出显示 Vertica 列类型 ARRAY[INT] 强制转换为 Long[]

queryText: SELECT ARRAY[ARRAY[1,2,3],ARRAY[4,5,6],ARRAY[7,8,9]]::ARRAY[ARRAY[INT]] as array
 level1Object [0]: [1,2,3] (class com.vertica.jdbc.jdbc42.S42Array)
 Value [0, 0]: 1 (class java.lang.Long)
 Value [0, 1]: 2 (class java.lang.Long)
 Value [0, 2]: 3 (class java.lang.Long)
 level1Object [1]: [4,5,6] (class com.vertica.jdbc.jdbc42.S42Array)
 Value [1, 0]: 4 (class java.lang.Long)
 Value [1, 1]: 5 (class java.lang.Long)
 Value [1, 2]: 6 (class java.lang.Long)
 level1Object [2]: [7,8,9] (class com.vertica.jdbc.jdbc42.S42Array)
 Value [2, 0]: 7 (class java.lang.Long)
 Value [2, 1]: 8 (class java.lang.Long)
 Value [2, 2]: 9 (class java.lang.Long)

getArrayObjectExample() 的输出显示 Vertica 列类型 ARRAY[FLOAT] 强制转换为 Double[]

queryText: SELECT ARRAY[ARRAY[0.0,0.1,0.2],ARRAY[1.0,1.1,1.2],ARRAY[2.0,2.1,2.2]]::ARRAY[ARRAY[FLOAT]] as array
Vertica driver returned a: class [Ljava.lang.Object;
 Value [0, 0]: 0.0 (class java.lang.Double)
 Value [0, 1]: 0.1 (class java.lang.Double)
 Value [0, 2]: 0.2 (class java.lang.Double)
 Value [1, 0]: 1.0 (class java.lang.Double)
 Value [1, 1]: 1.1 (class java.lang.Double)
 Value [1, 2]: 1.2 (class java.lang.Double)
 Value [2, 0]: 2.0 (class java.lang.Double)
 Value [2, 1]: 2.1 (class java.lang.Double)
 Value [2, 2]: 2.2 (class java.lang.Double)

ROW 列

如果在包含 ROW 类型的列的 java.sql.ResultSet 上调用 getObject(),会将该列检索为 java.sql.Struct,其中包含 Object[](本身可通过 getAttributes() 检索)。

Object[] 的每个元素代表该结构中的一个属性,每个属性都有对应的 Java 类型,如上面的类型转换表所示。

此示例定义具有以下属性的 ROW:


 Name    | Value        | Vertica Type | Java Type
-----------------------------------------------------------
 name    | Amy          | VARCHAR      | String
 date    | '07/10/2021' | DATE         | java.sql.Date
 id      | 5            | INT          | java.lang.Long
 current | false        | BOOLEAN      | java.lang.Boolean
package com.vertica.jdbc.test.samples;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.Array;
import java.sql.Struct;


public class ComplexTypesSamples
{
    /**
     * Executes a query and gets a java.sql.Struct from the ResultSet. It then uses the Struct#getAttributes
     * method to get the contents of the struct as a Java Object [].
     * @param conn A Connection to a Vertica database
     * @throws SQLException
     */
    public static void getRowExample (Connection conn) throws SQLException {
        Statement stmt = conn.createStatement();

        final String queryText = "SELECT ROW('Amy', '07/10/2021'::Date, 5, false) as rowExample(name, date, id, current)";
        final String targetColumnName = "rowExample";

        System.out.println ("queryText: " + queryText);
        ResultSet rs = stmt.executeQuery(queryText);
        int targetColumnId = rs.findColumn (targetColumnName);

        while (rs.next ()) {
            // Get the java.sql.Array from the result set
            Object currentObject = rs.getObject (targetColumnId);
            assert (currentObject instanceof Struct);
            Struct rowStruct = (Struct)currentObject;

            Object[] attributes = rowStruct.getAttributes();

            // attributes.length should be 4 based on the queryText
            assert (attributes.length == 4);
            assert (attributes[0] instanceof String);
            assert (attributes[1] instanceof java.sql.Date);
            assert (attributes[2] instanceof java.lang.Long);
            assert (attributes[3] instanceof java.lang.Boolean);

            System.out.println ("attributes[0]: " + attributes[0] + " (" + attributes[0].getClass().getName() +")");
            System.out.println ("attributes[1]: " + attributes[1] + " (" + attributes[1].getClass().getName() +")");
            System.out.println ("attributes[2]: " + attributes[2] + " (" + attributes[2].getClass().getName() +")");
            System.out.println ("attributes[3]: " + attributes[3] + " (" + attributes[3].getClass().getName() +")");
        }
    }
}

getRowExample() 的输出显示了每个元素的属性及其对应的 Java 类型:


queryText: SELECT ROW('Amy', '07/10/2021'::Date, 5, false) as rowExample(name, date, id, current)
attributes[0]: Amy (java.lang.String)
attributes[1]: 2021-07-10 (java.sql.Date)
attributes[2]: 5 (java.lang.Long)
attributes[3]: false (java.lang.Boolean)

3.6 - JDBC 中的日期类型

将日期转换为字符串

就本页而言,大日期是指年份超过 9999 的日期。

如果您的数据库不包含任何大日期,则可以可靠地调用 toString() 以将日期转换为字符串。

否则,如果您的数据库包含大日期,您应当使用 java.text.SimpleDateFormat 及其 format() 方法:

  1. 使用 java.text.SimpleDateFormat 定义字符串格式。yyyy 格式的字符数定义了日期中使用的最小字符数。

  2. 调用 SimpleDateFormat.format() 以将 java.sql.Date 对象转换为字符串。

示例

例如,以下方法在将 java.sql.Date 对象作为实参传递时返回一个字符串。这里,格式的年份部分 YYYY 表示该格式兼容所有在年份中至少包含四个字符的日期。

#import java.sql.Date;

private String convertDate (Date date) {
    SimpleDateFormat dateFormat = new SimpleDateFormat ("yyyy-MM-dd");
    return dateFormat.format (date);
}

4 - 通过 JDBC 执行查询

若要通过 JDBC 运行查询,请执行下列操作:

  1. 连接到 Vertica 数据库。请参阅创建和配置连接

  2. 运行查询。

用于运行查询的方法取决于要运行的查询的类型:

  • 不返回结果集的 DDL 查询。

  • 将返回结果集的 DDL 查询。

  • DML 查询。

执行 DDL(数据定义语言)查询

若要运行诸如 CREATE TABLECOPY 等 DDL 查询,请使用 Statement.execute() 方法。可以通过调用连接对象的 createStatement 方法获取此类的实例。

以下示例将创建 Statement 类的实例,并使用该实例执行 CREATE TABLECOPY 查询:

Statement stmt = conn.createStatement();
stmt.execute("CREATE TABLE address_book (Last_Name char(50) default ''," +
    "First_Name char(50),Email char(50),Phone_Number char(50))");
stmt.execute("COPY address_book FROM 'address.dat' DELIMITER ',' NULL 'null'");

执行将返回结果集的查询

可以使用 Statement 类的executeQuery 方法执行将返回结果集的查询(例如 SELECT)。若要从结果集获取数据,请根据结果集中的列的数据类型使用诸如 getIntgetStringgetDouble 等方法访问列值。使用 ResultSet.next 可以前进到数据集的下一行。

ResultSet rs = null;
rs = stmt.executeQuery("SELECT First_Name, Last_Name FROM address_book");
int x = 1;
while(rs.next()){
    System.out.println(x + ". " + rs.getString(1).trim() + " "
                       + rs.getString(2).trim());
    x++;
}

使用 executeUpdate 执行 DML(数据操作语言)查询

可以对用于更改数据库中的数据的 DML SQL 查询(例如,INSERTUPDATEDELETE)使用 executeUpdate 方法,这些查询不会返回结果集。

stmt.executeUpdate("INSERT INTO address_book " +
                   "VALUES ('Ben-Shachar', 'Tamar', 'tamarrow@example.com'," +
                   "'555-380-6466')");
stmt.executeUpdate("INSERT INTO address_book (First_Name, Email) " +
                   "VALUES ('Pete','pete@example.com')");

执行存储过程

您可以使用 CallableStatement 来创建和执行存储过程

创建存储过程

Statement st = conn.createStatement();

String createSimpleSp = "CREATE OR REPLACE PROCEDURE raiseInt(IN x INT) LANGUAGE PLvSQL AS $$ " +
"BEGIN" +
    "RAISE INFO 'x = %', x;" +
"END;" +
"$$;";

st.execute(createSimpleSp);

调用存储过程:

String spCall = "CALL raiseInt (?)";
CallableStatement stmt = conn.prepareCall(spCall);
stmt.setInt(1, 42);

存储过程尚不支持 OUT 参数。相反,您可以分别使用 RAISEgetWarnings() 返回和检索执行信息:

System.out.println(stmt.getWarnings().toString());

5 - 取消 JDBC 查询

您可以使用 Statement.cancel() 方法取消 JDBC 查询。

以下示例创建一个表 jdbccanceltest 并运行两个查询,取消第一个查询:

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.Array;
import java.sql.Struct;


public class CancelSamples
{
    /**
     * Sets up a large test table, queries its contents and cancels the query.
     * @param  conn A connection to a Vertica database
     * @throws SQLException
     */
    public static void sampleCancelTest(Connection conn) throws SQLException
    {
        setup(conn);
        try
        {
            runQueryAndCancel(conn);
            runSecondQuery(conn);
        }
        finally
        {
            cleanup(conn);
        }
    }

    // Set up table used in test.
    private static void setup(Connection conn) throws SQLException
    {
        System.out.println("Creating and loading table...");
        Statement stmt = conn.createStatement();
        String queryText = "DROP TABLE IF EXISTS jdbccanceltest";
        stmt.execute(queryText);

        queryText = "CREATE TABLE jdbccanceltest(id INTEGER, time TIMESTAMP)";
        stmt.execute(queryText);

        queryText = "INSERT INTO jdbccanceltest SELECT row_number() OVER(), slice_time "
                    + "FROM(SELECT slice_time FROM("
                    + "SELECT '2021-01-01'::timestamp s UNION ALL SELECT '2022-01-01'::timestamp s"
                    + ") sq TIMESERIES slice_time AS '1 second' OVER(ORDER BY s)) sq2";
        stmt.execute(queryText);
    }

    /**
     * Execute a long-running query and cancel it.
     * @param  conn A connection to a Vertica database
     * @throws SQLException
     */
    private static void runQueryAndCancel(Connection conn) throws SQLException
    {
        System.out.println("Running and canceling query...");
        Statement stmt = conn.createStatement();
        String queryText = "select id, time from jdbccanceltest";
        ResultSet rs = stmt.executeQuery(queryText);

        int i=0;
        stmt.cancel();
        try
        {
            while (rs.next()) ;
            i++;
        }
        catch (SQLException e)
        {
            System.out.println("Query canceled after retrieving " + i + " rows");
            System.out.println(e.getMessage());
        }
    }

    /**
     * Run a simple query to demonstrate that it can be run after
     * the previous query was canceled.
     * @param conn A connection to a Vertica database
     * @throws SQLException
     */
    private static void runSecondQuery(Connection conn) throws SQLException
    {
        String queryText = "select 1 from dual";
        Statement stmt = conn.createStatement();
        try
        {
            ResultSet rs = stmt.executeQuery(queryText);
            while (rs.next()) ;
        }
        catch (SQLException e)
        {
            System.out.println(e.getMessage());
            System.out.println("warning: no exception should have been thrown on query after cancel");
        }
    }

    /**
     * Clean up table used in test.
     * @param conn A connetion to a Vertica database
     * @throws SQLException
     */
    private static void cleanup(Connection conn) throws SQLException
    {
        String queryText = "drop table if exists jdbccanceltest";
        Statement stmt = conn.createStatement();
        stmt.execute(queryText);
    }
}

6 - 通过 JDBC 加载数据

可以使用以下任意方法通过 JDBC 接口加载数据:

  • 执行 SQL INSERT 语句以直接插入单个行。

  • 使用预定义的语句批量加载数据。

  • 使用 COPY 批量加载文件或流中的数据。

以下几节详细介绍如何使用 JDBC 加载数据。

6.1 - 使用单行插入

将数据插入到表中的最简单方法是使用 SQL INSERT 语句。可以将 Statement 类的成员实例化以使用此语句,并使用其 executeUpdate() 方法以运行 SQL 语句。

以下代码片段演示了如何创建 Statement 对象并使用该对象将数据插入到名为 address_book 的表中:

Statement stmt = conn.createStatement();
stmt.executeUpdate("INSERT INTO address_book " +
             "VALUES ('Smith', 'John', 'jsmith@example.com', " +
             "'555-123-4567')");

此方法有几个缺点:您需要将数据转换为字符串并对数据中的特殊字符进行转义。插入数据的更好方法是使用预定义的语句。请参阅使用 JDBC 预定义的语句批量插入

6.2 - 使用 JDBC 预定义的语句批量插入

可以使用预定义的 INSERT 语句(仅需设置一次即可重复调用的服务器端语句)将数据批量加载到 Vertica 中。您可以使用包含表示数据的问号占位符的 SQL 语句将 PreparedStatement 类的成员实例化。例如:

PreparedStatement pstmt = conn.prepareStatement(
                    "INSERT INTO customers(last, first, id) VALUES(?,?,?)");

然后,可以对 PreparedStatement 对象使用特定于数据类型的方法(例如 setString()setInt())来设置参数。设置参数后,调用 addBatch() 方法以将行添加到批中。整个数据批准备就绪后,调用 executeBatch() 方法以执行批量插入。

在后台,批量插入会转换为 COPY 语句。如果已禁用连接的 AutoCommit 参数,则 Vertica 会保持打开 COPY 语句并使用该语句加载后续的批,直至事务已提交或者游标已关闭或应用程序执行任何其他操作(或者使用其他 StatementPreparedStatement 对象执行任何语句)为止。使用单个 COPY 语句进行多个批量插入可以更高效地加载数据。如果要加载多个批,应禁用数据库的 AutoCommit 属性以提高效率。

执行批量插入时,可以试验各个批大小和行大小以确定可提供最佳性能的设置。

以下示例演示了使用预定义的语句批量插入数据。

import java.sql.*;
import java.util.Properties;

public class BatchInsertExample {
    public static void main(String[] args) {
        Properties myProp = new Properties();
        myProp.put("user", "ExampleUser");
        myProp.put("password", "password123");

     //Set streamingBatchInsert to True to enable streaming mode for batch inserts.
     //myProp.put("streamingBatchInsert", "True");

     Connection conn;
        try {
            conn = DriverManager.getConnection(
                            "jdbc:vertica://VerticaHost:5433/ExampleDB",
                            myProp);
            // establish connection and make a table for the data.
            Statement stmt = conn.createStatement();


            // Set AutoCommit to false to allow Vertica to reuse the same
            // COPY statement
            conn.setAutoCommit(false);


            // Drop table and recreate.
            stmt.execute("DROP TABLE IF EXISTS customers CASCADE");
            stmt.execute("CREATE TABLE customers (CustID int, Last_Name"
                            + " char(50), First_Name char(50),Email char(50), "
                            + "Phone_Number char(12))");
            // Some dummy data to insert.
            String[] firstNames = new String[] { "Anna", "Bill", "Cindy",
                            "Don", "Eric" };
            String[] lastNames = new String[] { "Allen", "Brown", "Chu",
                            "Dodd", "Estavez" };
            String[] emails = new String[] { "aang@example.com",
                            "b.brown@example.com", "cindy@example.com",
                            "d.d@example.com", "e.estavez@example.com" };
            String[] phoneNumbers = new String[] { "123-456-7890",
                            "555-444-3333", "555-867-5309",
                            "555-555-1212", "781-555-0000" };
            // Create the prepared statement
            PreparedStatement pstmt = conn.prepareStatement(
                            "INSERT INTO customers (CustID, Last_Name, " +
                            "First_Name, Email, Phone_Number)" +
                            " VALUES(?,?,?,?,?)");
            // Add rows to a batch in a loop. Each iteration adds a
            // new row.
            for (int i = 0; i < firstNames.length; i++) {
                // Add each parameter to the row.
                pstmt.setInt(1, i + 1);
                pstmt.setString(2, lastNames[i]);
                pstmt.setString(3, firstNames[i]);
                pstmt.setString(4, emails[i]);
                pstmt.setString(5, phoneNumbers[i]);
                // Add row to the batch.
                pstmt.addBatch();
            }

            try {
                // Batch is ready, execute it to insert the data
                pstmt.executeBatch();
            } catch (SQLException e) {
                System.out.println("Error message: " + e.getMessage());
                return; // Exit if there was an error
            }

            // Commit the transaction to close the COPY command
            conn.commit();


            // Print the resulting table.
            ResultSet rs = null;
            rs = stmt.executeQuery("SELECT CustID, First_Name, "
                            + "Last_Name FROM customers ORDER BY CustID");
            while (rs.next()) {
                System.out.println(rs.getInt(1) + " - "
                                + rs.getString(2).trim() + " "
                                + rs.getString(3).trim());
            }
            // Cleanup
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

运行此示例代码后的结果如下所示:

1 - Anna Allen
2 - Bill Brown
3 - Cindy Chu
4 - Don Dodd
5 - Eric Estavez

流式批量插入

默认情况下,Vertica 通过缓存每个行并在用户调用 executeBatch() 方法时插入缓存来执行批量插入。Vertica 也支持流式批量插入。流式批量插入在用户每次调用 addBatch() 时将行添加到数据库中。流式批量插入可提高数据库性能,因为它能够进行并行处理并降低内存需求。

若要启用流式批量插入,请将 streamingBatchInsert 属性设置为 True。以上代码示例包含一个行,该行启用了 streamingBatchInsert 模式。移除 // 注释标记可启用该行并激活流式批量插入。

下表介绍了各种批量插入方法及其默认批量插入模式和流式批量插入模式之间的行为差异。

注意

  • 使用 PreparedStatement.setFloat() 方法会导致出现舍入误差。如果精度很重要,请改为使用 .setDouble() 方法。

  • 预定义语句时,PreparedStatement 对象会缓存连接的 AutoCommit 属性。以后对 AutoCommit 属性进行的更改不会影响预定义的语句。

6.2.1 - 批量加载过程中的错误处理

加载各个批时,您可以确定已接受的行数和已拒绝了哪些行(有关详细信息,请参阅确定已接受的行和已拒绝的行)。如果禁用了 AutoCommit 连接设置,则插入各个批时不会发生其他错误(例如磁盘空间错误)。此行为是由单个 SQL COPY 语句对多个连续批执行加载(这样可提高加载过程的效率)导致的。只有 COPY 语句关闭时,才会提交批量数据,而且 Vertica 会报告其他类型错误。

因此,应使批量加载应用程序准备好在 COPY 语句关闭时检查错误。可以通过以下方法促使 COPY 语句关闭:

  • 通过调用 Connection.commit() 来结束批量加载事务

  • 通过使用 Statement.close() 来关闭该语句

  • 在加载中插入最后一批之前将连接的 AutoCommit 属性设置为 true

6.2.2 - 确定已接受和已拒绝行 (JDBC)

PreparedStatement.executeBatch 的返回值是一个整数数组,其中包含每个行的插入操作的成功或失败状态。值 1 表示行已被接受,而值 -3 表示行已被拒绝。如果在批处理执行期间出现了异常,您还可以使用 BatchUpdateException.getUpdateCounts() 获取该数组。

以下示例延伸了 使用 JDBC 预定义的语句批量插入 中所示的示例,以检索该数组并显示批量加载的结果。

import java.sql.*;
import java.util.Arrays;
import java.util.Properties;

public class BatchInsertErrorHandlingExample {
    public static void main(String[] args) {
        Properties myProp = new Properties();
        myProp.put("user", "ExampleUser");
        myProp.put("password", "password123");
        Connection conn;

        // establish connection and make a table for the data.
        try {
            conn = DriverManager.getConnection(
                            "jdbc:vertica://VerticaHost:5433/ExampleDB",
                            myProp);


            // Disable auto commit
            conn.setAutoCommit(false);

            // Create a statement
            Statement stmt = conn.createStatement();
            // Drop table and recreate.
            stmt.execute("DROP TABLE IF EXISTS customers CASCADE");
            stmt.execute("CREATE TABLE customers (CustID int, Last_Name"
                            + " char(50), First_Name char(50),Email char(50), "
                            + "Phone_Number char(12))");

            // Some dummy data to insert. The one row won't insert because
            // the phone number is too long for the phone column.
            String[] firstNames = new String[] { "Anna", "Bill", "Cindy",
                            "Don", "Eric" };
            String[] lastNames = new String[] { "Allen", "Brown", "Chu",
                            "Dodd", "Estavez" };
            String[] emails = new String[] { "aang@example.com",
                            "b.brown@example.com", "cindy@example.com",
                            "d.d@example.com", "e.estavez@example.com" };
            String[] phoneNumbers = new String[] { "123-456-789",
                            "555-444-3333", "555-867-53093453453",
                            "555-555-1212", "781-555-0000" };

            // Create the prepared statement
            PreparedStatement pstmt = conn.prepareStatement(
                            "INSERT INTO customers (CustID, Last_Name, " +
                            "First_Name, Email, Phone_Number)" +
                            " VALUES(?,?,?,?,?)");

            // Add rows to a batch in a loop. Each iteration adds a
            // new row.
            for (int i = 0; i < firstNames.length; i++) {
                // Add each parameter to the row.
                pstmt.setInt(1, i + 1);
                pstmt.setString(2, lastNames[i]);
                pstmt.setString(3, firstNames[i]);
                pstmt.setString(4, emails[i]);
                pstmt.setString(5, phoneNumbers[i]);
                // Add row to the batch.
                pstmt.addBatch();
            }

            // Integer array to hold the results of inserting
            // the batch. Will contain an entry for each row,
            // indicating success or failure.
            int[] batchResults = null;

            try {
                // Batch is ready, execute it to insert the data
                batchResults = pstmt.executeBatch();
            } catch (BatchUpdateException e) {
                // We expect an exception here, since one of the
                // inserted phone numbers is too wide for its column. All of the
                // rest of the rows will be inserted.
                System.out.println("Error message: " + e.getMessage());

                // Batch results isn't set due to exception, but you
                // can get it from the exception object.
                //
                // In your own code, you shouldn't assume the a batch
                // exception occurred, since exceptions can be thrown
                // by the server for a variety of reasons.
                batchResults = e.getUpdateCounts();
            }
            // You should also be prepared to catch SQLExceptions in your own
            // application code, to handle dropped connections and other general
            // problems.

            // Commit the transaction
            conn.commit();


            // Print the array holding the results of the batch insertions.
            System.out.println("Return value from inserting batch: "
                            + Arrays.toString(batchResults));
            // Print the resulting table.
            ResultSet rs = null;
            rs = stmt.executeQuery("SELECT CustID, First_Name, "
                            + "Last_Name FROM customers ORDER BY CustID");
            while (rs.next()) {
                System.out.println(rs.getInt(1) + " - "
                                + rs.getString(2).trim() + " "
                                + rs.getString(3).trim());
            }

            // Cleanup
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

运行以上示例后,将在控制台上生成以下输出:

Error message: [Vertica][VJDBC](100172) One or more rows were rejected by the server.Return value from inserting batch: [1, 1, -3, 1, 1]
1 - Anna Allen
2 - Bill Brown
4 - Don Dodd
5 - Eric Estavez

请注意,第三行插入失败,因为其电话号码对 Phone_Number 列来说太长。该批中其余的所有行(包括该错误之后的行)已正确插入。

6.2.3 - 在服务器中回退批量加载

即使一个或多个行被拒绝,批量加载也始终插入所有数据。只有某个批中导致发生错误的行不会加载。如果数据库连接的 AutoCommit 属性为 true,各个批会在完成后自动提交其事务,因此在该批完成加载后,将提交数据。

在某些情况下,您可能希望成功插入某个批中的所有数据(在发生错误时不应提交任何数据)。若要实现此目的,最佳方法是关闭数据库连接的 AutoCommit 属性以防止各个批自动提交其自身。然后,如果某个批遇到错误,您可以在捕获由插入错误导致的 BatchUpdateException 之后回退事务。

以下示例演示了在加载某个批期间发生任何错误时执行回退。

import java.sql.*;
import java.util.Arrays;
import java.util.Properties;

public class RollbackBatchOnError {
    public static void main(String[] args) {
        Properties myProp = new Properties();
        myProp.put("user", "ExampleUser");
        myProp.put("password", "password123");
        Connection conn;
        try {
            conn = DriverManager.getConnection(
                            "jdbc:vertica://VerticaHost:5433/ExampleDB",
                            myProp);
            // Disable auto-commit. This will allow you to roll back a
            // a batch load if there is an error.
            conn.setAutoCommit(false);
            // establish connection and make a table for the data.
            Statement stmt = conn.createStatement();
            // Drop table and recreate.
            stmt.execute("DROP TABLE IF EXISTS customers CASCADE");
            stmt.execute("CREATE TABLE customers (CustID int, Last_Name"
                            + " char(50), First_Name char(50),Email char(50), "
                            + "Phone_Number char(12))");

            // Some dummy data to insert. The one row won't insert because
            // the phone number is too long for the phone column.
            String[] firstNames = new String[] { "Anna", "Bill", "Cindy",
                            "Don", "Eric" };
            String[] lastNames = new String[] { "Allen", "Brown", "Chu",
                            "Dodd", "Estavez" };
            String[] emails = new String[] { "aang@example.com",
                            "b.brown@example.com", "cindy@example.com",
                            "d.d@example.com", "e.estavez@example.com" };
            String[] phoneNumbers = new String[] { "123-456-789",
                            "555-444-3333", "555-867-53094535", "555-555-1212",
                            "781-555-0000" };
            // Create the prepared statement
            PreparedStatement pstmt = conn.prepareStatement(
                            "INSERT INTO customers (CustID, Last_Name, " +
                            "First_Name, Email, Phone_Number) "+
                            "VALUES(?,?,?,?,?)");
            // Add rows to a batch in a loop. Each iteration adds a
            // new row.
            for (int i = 0; i < firstNames.length; i++) {
                // Add each parameter to the row.
                pstmt.setInt(1, i + 1);
                pstmt.setString(2, lastNames[i]);
                pstmt.setString(3, firstNames[i]);
                pstmt.setString(4, emails[i]);
                pstmt.setString(5, phoneNumbers[i]);
                // Add row to the batch.
                pstmt.addBatch();
            }
            // Integer array to hold the results of inserting
            // the batch. Will contain an entry for each row,
            // indicating success or failure.
            int[] batchResults = null;
            try {
                // Batch is ready, execute it to insert the data
                batchResults = pstmt.executeBatch();
                // If we reach here, we inserted the batch without errors.
                // Commit it.
                System.out.println("Batch insert successful. Committing.");
                conn.commit();
            } catch (BatchUpdateException e) {
                    System.out.println("Error message: " + e.getMessage());
                    // Batch results isn't set due to exception, but you
                    // can get it from the exception object.
                    batchResults =  e.getUpdateCounts();
                    // Roll back the batch transaction.
                    System.out.println("Rolling back batch insertion");
                    conn.rollback();
            }
            catch  (SQLException e) {
                // General SQL errors, such as connection issues, throw
                // SQLExceptions. Your application should do something more
                // than just print a stack trace,
                e.printStackTrace();
            }
            System.out.println("Return value from inserting batch: "
                            + Arrays.toString(batchResults));
            System.out.println("Customers table contains:");


            // Print the resulting table.
            ResultSet rs = null;
            rs = stmt.executeQuery("SELECT CustID, First_Name, "
                            + "Last_Name FROM customers ORDER BY CustID");
            while (rs.next()) {
                System.out.println(rs.getInt(1) + " - "
                                + rs.getString(2).trim() + " "
                                + rs.getString(3).trim());
            }

            // Cleanup
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

运行以上示例后,将在系统控制台上输出以下内容:

Error message: [Vertica][VJDBC](100172) One or more rows were rejected by the server.Rolling back batch insertion
Return value from inserting batch: [1, 1, -3, 1, 1]
Customers table contains:

返回值指示是否已成功插入每个行。值 1 表示已插入该行并且未出现任何问题,而值 -3 表示插入该行失败。

客户表为空,因为已回退该批量插入,原因是第三列导致发生错误。

6.3 - 使用 COPY 语句进行批量加载

一次将大量数据加载(批量加载)到 Vertica 的最快方法之一是使用 COPY 语句。此语句可将存储在 Vertica 主机上的文件中的数据(或数据流中的数据)加载到数据库中的表。可以向 COPY 语句传递定义了以下设置的参数:文件中的数据的格式、加载数据时对数据的转换方式、对错误的处理方式以及加载数据的方式。有关详细信息,请参阅 COPY 文档。

只有 超级用户可以使用 COPY 语句复制存储在主机上的文件,因此您必须使用超级用户帐户连接到数据库。如果要以非超级用户身份批量加载数据,您可以使用 COPY 从主机上的流(例如 STDIN)而非文件加载数据,或者也可以从客户端以流式传输数据(请参阅通过 JDBC 进行流式数据传输)。您还可以执行使用预定义的语句的批量插入(此为标准方式),这种方式在后台使用 COPY 语句来加载数据。

以下示例演示了通过 JDBC 使用 COPY 语句将名为 customers.txt 的文件加载到新的数据库表。该文件必须存储在应用程序连接到的数据库主机(在此示例中是名为 VerticaHost 的主机)上。

import java.sql.*;
import java.util.Properties;
import com.vertica.jdbc.*;

public class COPYFromFile {
    public static void main(String[] args) {
        Properties myProp = new Properties();
        myProp.put("user", "ExampleAdmin"); // Must be superuser
        myProp.put("password", "password123");
        Connection conn;
        try {
            conn = DriverManager.getConnection(
                            "jdbc:vertica://VerticaHost:5433/ExampleDB",myProp);
            // Disable AutoCommit
            conn.setAutoCommit(false);
            Statement stmt = conn.createStatement();
            // Create a table to hold data.
            stmt.execute("DROP TABLE IF EXISTS customers;");
            stmt.execute("CREATE TABLE IF NOT EXISTS customers (Last_Name char(50) "
                            + "NOT NULL, First_Name char(50),Email char(50), "
                            + "Phone_Number char(15))");

             // Use the COPY command to load data. Use ENFORCELENGTH to reject
            // strings too wide for their columns.
            boolean result = stmt.execute("COPY customers FROM "
                            + " '/data/customers.txt' ENFORCELENGTH");

            // Determine if execution returned a count value, or a full result
            // set.
            if (result) {
                System.out.println("Got result set");
            } else {
                // Count will usually return the count of rows inserted.
                System.out.println("Got count");
                int rowCount = stmt.getUpdateCount();
                System.out.println("Number of accepted rows = " + rowCount);
            }


            // Commit the data load
            conn.commit();
        } catch (SQLException e) {
            System.out.print("Error: ");
            System.out.println(e.toString());
        }
    }
}

运行此示例后,将在系统控制台上输出以下内容(假设 customers.txt 文件两百万个有效的行):

Number of accepted rows = 2000000

6.4 - 通过 JDBC 进行流式数据传输

以下两个选项可用于将客户端上的文件中的数据以流式传输到 Vertica 数据库:

  • 使用 VerticaCopyStream 类按照面向对象的方式流式传输数据 - 有关该类的详细信息,请参阅 JDBC 文档

  • 执行 COPY LOCAL SQL 语句以进行流式数据传输

此部分中的主题介绍了使用这些选项的方法。

6.4.1 - 使用 VerticaCopyStream

使用 VerticaCopyStream 类可以将数据从客户端系统流式传输到 Vertica 数据库。它允许您直接使用 COPY,而无需先将数据复制到数据库群集中的主机。使用 COPY 命令从主机加载数据时,需要拥有超级用户权限才能访问主机的文件系统。用于从流加载数据的 COPY 语句不需要超级用户权限,因此客户端可以使用对目标表拥有 INSERT 权限的任何用户帐户进行连接。

若要将流复制到数据库,请执行下列操作:

  1. 禁用数据库连接的 AutoCommit 连接参数。

  2. VerticaCopyStreamObject 实例化,并至少向其传递数据库连接对象和包含用于加载数据的 COPY 语句的字符串。该语句必须将 STDIN 中的数据复制到表。您可以使用适用于数据加载的任何参数。

  3. 调用 VerticaCopyStreamObject.start(),以启动 COPY 语句并开始以流式传输已添加到 VerticaCopyStreamObject 的任何流中的数据。

  4. 调用 VerticaCopyStreamObject.addStream(),以将其他流添加到要发送到数据库的流的列表。然后,可以调用 VerticaCopyStreamObject.execute() 以将它们以流式传输到服务器。

  5. (可选)调用 VerticaCopyStreamObject.getRejects() 以从上一次 .execute() 调用获取拒绝的行的列表。对 .execute().finish() 的每次调用会重置拒绝列表。

  6. 完成添加流后,调用 VerticaCopyStreamObject.finish() 以将其余任何流发送到数据库并关闭 COPY 语句。

  7. 调用 Connection.commit() 以提交已加载的数据。

获取拒绝的行

VerticaCopyStreamObject.getRejects() 方法将返回一个列表,其中包含上一次 .execute() 方法调用之后拒绝的行的数量。对 .execute() 的每次调用会清除拒绝的行的列表,因此您需要在每次调用 .execute() 之后调用 .getRejects()。由于 .start().finish() 也会调用 .execute() 以将任何挂起的流发送到服务器,因此您还应在调用这些方法之后调用 .getRejects()

以下示例演示了将存储在客户端系统上的五个文本文件的内容加载到表中。

import java.io.File;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import com.vertica.jdbc.VerticaConnection;
import com.vertica.jdbc.VerticaCopyStream;

public class CopyMultipleStreamsExample {
    public static void main(String[] args) {
        // Note: If running on Java 5, you need to call Class.forName
        // to manually load the JDBC driver.
        // Set up the properties of the connection
        Properties myProp = new Properties();
        myProp.put("user", "ExampleUser"); // Must be superuser
        myProp.put("password", "password123");
        // When performing bulk loads, you should always disable the
        // connection's AutoCommit property to ensure the loads happen as
        // efficiently as possible by reusing the same COPY command and
        // transaction.
        myProp.put("AutoCommit", "false");
        Connection conn;
        try {
            conn = DriverManager.getConnection(
                          "jdbc:vertica://VerticaHost:5433/ExampleDB", myProp);
            Statement stmt = conn.createStatement();

            // Create a table to receive the data
            stmt.execute("DROP TABLE IF EXISTS customers");
            stmt.execute("CREATE TABLE customers (Last_Name char(50), "
                            + "First_Name char(50),Email char(50), "
                            + "Phone_Number char(15))");

            // Prepare the query to insert from a stream. This query must use
            // the COPY statement to load data from STDIN. Unlike copying from
            // a file on the host, you do not need superuser privileges to
            // copy a stream. All your user account needs is INSERT privileges
            // on the target table.
            String copyQuery = "COPY customers FROM STDIN "
                            + "DELIMITER '|' ENFORCELENGTH";

            // Create an instance of the stream class. Pass in the
            // connection and the query string.
            VerticaCopyStream stream = new VerticaCopyStream(
                            (VerticaConnection) conn, copyQuery);

            // Keep running count of the number of rejects
            int totalRejects = 0;

            // start() starts the stream process, and opens the COPY command.
            stream.start();

            // If you added streams to VerticaCopyStream before calling start(),
            // You should check for rejects here (see below). The start() method
            // calls execute() to send any pre-queued streams to the server
            // once the COPY statement has been created.

            // Simple for loop to load 5 text files named customers-1.txt to
            // customers-5.txt
            for (int loadNum = 1; loadNum <= 5; loadNum++) {
                // Prepare the input file stream. Read from a local file.
                String filename = "C:\\Data\\customers-" + loadNum + ".txt";
                System.out.println("\n\nLoading file: " + filename);
                File inputFile = new File(filename);
                FileInputStream inputStream = new FileInputStream(inputFile);

                // Add stream to the VerticaCopyStream
                stream.addStream(inputStream);

                // call execute() to load the newly added stream. You could
                // add many streams and call execute once to load them all.
                // Which method you choose depends mainly on whether you want
                // the ability to check the number of rejections as the load
                // progresses so you can stop if the number of rejects gets too
                // high. Also, high numbers of InputStreams could create a
                // resource issue on your client system.
                stream.execute();

                // Show any rejects from this execution of the stream load
                // getRejects() returns a List containing the
                // row numbers of rejected rows.
                List<Long> rejects = stream.getRejects();

                // The size of the list gives you the number of rejected rows.
                int numRejects = rejects.size();
                totalRejects += numRejects;
                System.out.println("Number of rows rejected in load #"
                                + loadNum + ": " + numRejects);

                // List all of the rows that were rejected.
                Iterator<Long> rejit = rejects.iterator();
                long linecount = 0;
                while (rejit.hasNext()) {
                    System.out.print("Rejected row #" + ++linecount);
                    System.out.println(" is row " + rejit.next());
                }
            }
            // Finish closes the COPY command. It returns the number of
            // rows inserted.
            long results = stream.finish();
            System.out.println("Finish returned " + results);

            // If you added any streams that hadn't been executed(),
            // you should also check for rejects here, since finish()
            // calls execute() to

            // You can also get the number of rows inserted using
            // getRowCount().
            System.out.println("Number of rows accepted: "
                            + stream.getRowCount());
            System.out.println("Total number of rows rejected: " + totalRejects);

            // Commit the loaded data
            conn.commit();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

对部分示例数据运行以上示例后,将生成以下输出:


Loading file: C:\Data\customers-1.txtNumber of rows rejected in load #1: 3
Rejected row #1 is row 3
Rejected row #2 is row 7
Rejected row #3 is row 51
Loading file: C:\Data\customers-2.txt
Number of rows rejected in load #2: 5Rejected row #1 is row 4143
Rejected row #2 is row 6132
Rejected row #3 is row 9998
Rejected row #4 is row 10000
Rejected row #5 is row 10050
Loading file: C:\Data\customers-3.txt
Number of rows rejected in load #3: 9
Rejected row #1 is row 14142
Rejected row #2 is row 16131
Rejected row #3 is row 19999
Rejected row #4 is row 20001
Rejected row #5 is row 20005
Rejected row #6 is row 20049
Rejected row #7 is row 20056
Rejected row #8 is row 20144
Rejected row #9 is row 20236
Loading file: C:\Data\customers-4.txt
Number of rows rejected in load #4: 8
Rejected row #1 is row 23774
Rejected row #2 is row 24141
Rejected row #3 is row 25906
Rejected row #4 is row 26130
Rejected row #5 is row 27317
Rejected row #6 is row 28121
Rejected row #7 is row 29321
Rejected row #8 is row 29998
Loading file: C:\Data\customers-5.txt
Number of rows rejected in load #5: 1
Rejected row #1 is row 39997
Finish returned 39995
Number of rows accepted: 39995
Total number of rows rejected: 26

6.4.2 - 将 COPY LOCAL 与 JDBC 配合使用

若要将 COPY LOCAL 与 JDBC 结合使用,只需执行 COPY LOCAL 语句并指定客户端系统上源文件的路径即可。此方法比使用 VerticaCopyStream 类更简单(有关该类的详细信息,请参阅 JDBC 文档)。但是,如果有许多文件要复制到数据库,或者如果数据来自某个文件以外的其他源(例如,通过网络连接进行流式传输),您可能倾向于使用 VerticaCopyStream

可以在多语句查询中使用 COPY LOCAL。但是,您应当始终将其作为查询中的第一条语句。不应当在同一个查询中多次使用它。

以下示例代码演示了使用 COPY LOCAL 将文件从客户端复制到数据库。除了在 COPY 语句中使用 LOCAL 选项,并且数据文件的路径位于客户端系统而非服务器上,此示例与使用 COPY 语句进行批量加载中所示的代码相同。

import java.sql.*;
import java.util.Properties;

public class COPYLocal {
    public static void main(String[] args) {
        // Note: If using Java 5, you must call Class.forName to load the
        // JDBC driver.
        Properties myProp = new Properties();
        myProp.put("user", "ExampleUser"); // Do not need to superuser
        myProp.put("password", "password123");
        Connection conn;
        try {
            conn = DriverManager.getConnection(
                            "jdbc:vertica://VerticaHost:5433/ExampleDB",myProp);
            // Disable AutoCommit
            conn.setAutoCommit(false);
            Statement stmt = conn.createStatement();
            // Create a table to hold data.
            stmt.execute("DROP TABLE IF EXISTS customers;");
            stmt.execute("CREATE TABLE IF NOT EXISTS customers (Last_Name char(50) "
                            + "NOT NULL, First_Name char(50),Email char(50), "
                            + "Phone_Number char(15))");

            // Use the COPY command to load data. Load directly into ROS, since
            // this load could be over 100MB. Use ENFORCELENGTH to reject
            // strings too wide for their columns.
            boolean result = stmt.execute("COPY customers FROM LOCAL "
                            + " 'C:\\Data\\customers.txt' DIRECT ENFORCELENGTH");

            // Determine if execution returned a count value, or a full result
            // set.
            if (result) {
                System.out.println("Got result set");
            } else {
                // Count will usually return the count of rows inserted.
                System.out.println("Got count");
                int rowCount = stmt.getUpdateCount();
                System.out.println("Number of accepted rows = " + rowCount);
            }

            conn.close();
        } catch (SQLException e) {
            System.out.print("Error: ");
            System.out.println(e.toString());
        }
    }
}

运行此代码后的结果如下所示。在此示例中,customers.txt 文件包含 10000 行,其中七个行被拒绝,因为这些行所包含的数据的宽度太大而无法适应其数据库列。

Got countNumber of accepted rows = 9993

7 - 处理错误

当 Vertica JDBC 驱动程序遇到错误时,它会引发 SQLException 或其子类之一。所引发的特定子类取决于已发生的错误的类型。大部分 JDBC 方法调用会导致发生多种不同类型的错误,JDBC 驱动程序将引发特定的 SQLException 子类以响应这些错误。客户端应用程序可以根据 JDBC 驱动程序已引发的特定异常选择如何对错误做出响应。

SQLException 子类的层次结构已进行了排列,可帮助客户端应用程序确定采取哪些措施以对错误条件做出响应。例如:

  • 当错误原因可能是临时条件(例如,超时错误 (SQLTimeoutException) 或连接问题 (SQLTransientConnectionIssue))时,JDBC 驱动程序将引发 SQLTransientException 子类。客户端应用程序可以选择重试操作而不进行任何尝试以纠正错误,因为该错误可能不会再次发生。

  • 当客户端需要采取某种措施以便能够重试操作时,JDBC 驱动程序将引发 SQLNonTransientException 子类。例如,执行包含 SQL 语法错误的语句会导致 JDBC 驱动程序引发 SQLSyntaxErrorExceptionSQLNonTransientException 的子类)。通常,客户端应用程序只需要将这些错误报告回给用户并让用户解决错误。例如,如果用户向应用程序提供的 SQL 语句触发了 SQLSyntaxErrorException,则应用程序会提示用户修复 SQL 错误。

有关由 JDBC 驱动程序引发的 Java 异常的列表,请参阅 SQLState 和 Java 异常类的映射

7.1 - SQLState 和 Java 异常类的映射

8 - 将 JDBC 查询直接路由到单个节点

JDBC 驱动程序能够使用名为可路由连接的特殊连接将查询直接路由到单个节点。此功能适用于大量的“短”请求,此类请求将返回全部存在于单个节点上的少量结果。此功能的常见使用场景是对由唯一键标识的数据执行大量查找。与分布式查询相比,可路由查询通常提供更低延迟并使用更少系统资源。但是,必须以某种方式对要查询的数据进行分段,以使 JDBC 客户端能够确定数据驻留在哪个节点。

Vertica 典型分析查询

典型分析查询需要对群集中所有节点上的数据执行密集计算,并且通过让所有节点参与查询的规划和执行来获益。

Vertica 可路由查询 API 查询

对于返回单个或几个数据行的大量查询,在包含数据的单个节点上执行查询更高效。

为有效地将请求路由到单个节点,客户端必须确定数据所驻留在的特定节点。为使客户端能够确定正确节点,必须按一个或多个列对表进行分段。例如,如果按主键 (PK) 列对表进行分段,则客户端可以基于主键确定数据驻留在哪个节点,并直接连接到该节点以快速完成请求。

可路由查询 API 提供以下两个用于执行可路由查询的类:VerticaRoutableExecutor 和 VGet。VerticaRoutableExecutor 提供更富有表现力的基于 SQL 的 API,而 VGet 提供结构更完善的 API 用于编程访问。

  • 通过 VerticaRoutableExecutor 类,您可以将传统 SQL 与减少的功能集结合使用,以在单个节点上查询数据。

    对于联接,表必须按存在于要联接的每个表中的键列进行联接,并且这些表必须按该键进行分段。但是,此限制对未分段的表不适用,这种表始终可以联接(因为未分段的表中的所有数据在所有节点上均可用)。

  • VGet 类不使用传统 SQL 语法。相反,此类使用您通过定义谓词而构建的数据结构,还使用谓词表达式以及输出和输出表达式。此类适用于对单个表执行键/值类型查找。

    用于查询表的数据结构必须为该表的投影中定义的每个已分段的列提供一个谓词。您至少必须为每个已分段的列提供一个具有常量值的谓词。例如,如果表仅按 id 列进行分段,请提供具有值 12234 的 id。您还可以为表中其他未分段的列指定附加谓词。谓词的作用类似于 SQL WHERE 子句,多个谓词/谓词表达式通过 SQL AND 修饰符一起应用。必须为谓词定义常量值。谓词表达式可用于细化查询,并且可以包含任意 SQL 表达式(例如,小于和大于等)以用于表中未分段的列。

Vertica JDBC 文档中提供了适用于 JDBC 驱动程序中所有类和方法的 Java 文档。

8.1 - 创建与可路由查询 API 一起使用的表和投影

对于可路由查询,客户端必须确定适用于获取数据的节点。客户端通常以下方法执行此操作:比较对表可用的所有投影,然后确定可用于查找包含数据的单个节点的最佳投影。您必须在至少一个表上创建按键列分段的投影,以充分利用可路由查询 API。联接到此表的其他表必须具有未分段投影,或者必须具有按下文所属进行分段的投影。

创建与可路由查询结合使用的表

若要创建可以与可路由查询 API 结合使用的表,请按均匀分布的列对表进行分段(使用哈希算法)。通常,可以按主键进行分段。为提高查找速度,请按用作分段依据的相同列对投影进行排序。例如,若要创建非常适用于可路由查询的表,请执行下列操作:


CREATE TABLE users (
id INT NOT NULL PRIMARY KEY,
username VARCHAR(32),
email VARCHAR(64),
business_unit VARCHAR(16))
ORDER BY id
SEGMENTED BY HASH(id)
ALL NODES;

此表基于 id 列进行分段(并按 id 进行排序以提高查找速度)。若要使用可路由查询 API 为此表构建查询,您只需要为 id 列提供单个谓词,当您查询该列时,将返回单个行。

不过,您可以向分段子句添加多个列。例如:

CREATE TABLE users2 (
    id INT NOT NULL PRIMARY KEY,
    username VARCHAR(32),
    email VARCHAR(64),
    business_unit VARCHAR(16))
ORDER BY id, business_unit
SEGMENTED BY HASH(id, business_unit)
ALL NODES;

在本例中,您需要在查询 users2 表时提供两个谓词,因为它按以下两列分段:idbusiness_unit。但是,如果您在执行查询时知道 idbusiness_unit,则同时按这两个列进行分段会有帮助,因为这样可使客户端更容易确定该投影是可用于确定正确节点的最佳投影。

针对单节点 JOIN 设计表

如果打算在可路由查询期间使用 VerticaRoutableExecutor 类并联接表,则您必须按同一个分段键对要联接的所有表进行分段。通常,此键是要联接的所有表上的主键/外键。例如,customer_key 可能是客户维度表中的主键,并且同一个键是销售事实数据表中的外键。使用这些表的 VerticaRoutableExecutor 查询的投影必须使用哈希算法按每个表中的客户键进行分段。

如果要与小型维度表(例如日期维度)联接,则使这些表保持未分段可能是合适做法,以使 date_dimension 数据存在于所有节点上。请务必注意,在联接未分段的表时,您仍必须在 createRoutableExecutor() 调用中指定已分段的表。

验证表的现有投影

如果已使用哈希算法对表进行分段(例如按 ID 列),则您可以通过使用 Vertica 函数 GET_PROJECTIONS 查看该表的投影,确定需要使用哪些谓词来查询该表。例如:

=> SELECT GET_PROJECTIONS ('users');
...
Projection Name: [Segmented] [Seg Cols] [# of Buddies] [Buddy Projections] [Safe] [UptoDate] [Stats]
----------------------------------------------------------------------------------------------------
public.users_b1 [Segmented: Yes] [Seg Cols: "public.users.id"] [K: 1] [public.users_b0] [Safe: Yes] [UptoDate: Yes] [Stats: RowCounts]
public.users_b0 [Segmented: Yes] [Seg Cols: "public.users.id"] [K: 1] [public.users_b1] [Safe: Yes] [UptoDate: Yes] [Stats: RowCounts]

对于每个投影,仅指定 public.users.id 列,表示您的查询谓词应包含此列。

如果表按多个列(例如 idbusiness_unit)进行分段,则您需要将这两个列作为谓词提供给可路由查询。

8.2 - 为可路由查询创建连接

JDBC 可路由查询 API 提供了 VerticaRoutableConnection 接口( JDBC 文档中提供了详细信息),此接口可用于连接到群集以及启用可路由查询。除了普通 VerticaConnection 所提供的功能之外,此接口还提供高级路由功能。VerticaRoutableConnection 提供对 VerticaRoutableExecutor 类和 VGet 类的访问。请分别参阅使用 VerticaRoutableExecutor 类为可路由查询定义查询使用 VGet 类为可路由查询定义查询

可以通过将 EnableRoutableQueries JDBC 连接属性设置为 true 来启用对此类的访问。

VerticaRoutableConnection 维护内部连接池和表元数据缓存(由连接的 createRoutableExecutor()/prepareGet() 方法所生成的所有 VerticaRoutableExecutor/VGet 对象共享)。此接口还是一个完全开发的独立 JDBC 连接,并且支持 VerticaConnection 所支持的所有功能。当此连接关闭时,由此 VerticaRoutableConnection 管理的所有池连接和所有子对象也会关闭。连接池和元数据仅由子可路由查询操作使用。

例如:

您可以使用 JDBC 数据源创建连接:


com.vertica.jdbc.DataSource jdbcSettings = new com.vertica.jdbc.DataSource();
jdbcSettings.setDatabase("exampleDB");
jdbcSettings.setHost("v_vmart_node0001.example.com");
jdbcSettings.setUserID("dbadmin");
jdbcSettings.setPassword("password");
jdbcSettings.setEnableRoutableQueries(true);
jdbcSettings.setPort((short) 5433);

VerticaRoutableConnection conn;
conn = (VerticaRoutableConnection)jdbcSettings.getConnection();

您还可以使用连接字符串和 DriverManager.getConnection() 方法创建连接:

String connectionString = "jdbc:vertica://v_vmart_node0001.example.com:5433/exampleDB?user=dbadmin&password=&EnableRoutableQueries=true";
VerticaRoutableConnection conn = (VerticaRoutableConnection) DriverManager.getConnection(connectionString);

以上两种方法生成相同的 conn 连接对象。

除了由可路由查询 API 添加到 Vertica JDBC 连接类的 setEnableRoutableQueries 属性之外,该 API 还添加了其他属性。完整列表如下。

  • EnableRoutableQueries:启用可路由查询查找功能。默认值为 false。

  • FailOnMultiNodePlans:如果计划需要多个节点,并且 FailOnMultiNodePlans 设置为 true,查询将失败。如果此属性设置为 false,则会生成警告,并且查询会继续执行。但是,延迟会显著提高,因为可路由查询必须先确定数据是否位于多个节点上,然后使用传统执行(在所有节点上)来运行普通查询。默认值为 true。请注意,仅使用谓词和常量值的简单调用不会出现此失败。

  • MetadataCacheLifetime:保留投影元数据的时间(以秒为单位)。API 缓存有关投影的元数据以用于查询(例如投影)。后续的查询使用该缓存来缩短响应时间。默认值为 300 秒。

  • MaxPooledConnections:保留在 VerticaRoutableConnection 内部池中的最大连接数(群集范围)。默认值 20。

  • MaxPooledConnectionsPerNode:保留在 VerticaRoutableConnection 内部池中的最大连接数(每节点)。默认值 5。

8.3 - 使用 VerticaRoutableExecutor 类为可路由查询定义查询

使用 VerticaRoutableExecutor 类从单个节点直接访问表数据。 VerticaRoutableExecutor 仅在包含查询所需的所有数据的节点上直接查询 Vertica,从而避免产生与 Vertica 查询执行关联的分布式规划和执行成本。您可以使用 VerticaRoutableExecutor 联接表或使用 GROUP BY 子句,因为使用 VGet 无法执行这些操作。

使用 VerticaRoutableExecutor 类时,适用以下规则:

  • 如果要联接表,您必须按联接谓词中引用的相同列集对要联接的所有表进行分段(使用哈希算法),除非要联接的表未分段。
  • 联接 WHERE 子句中的多个条件必须使用 AND 联接在一起。在 WHERE 子句中使用 OR 会导致查询退化成多节点计划。如果数据存在于同一节点上,则可以在联接条件 outside 列上指定 OR、IN 列表或范围条件。
  • 每个请求只能执行单个语句。不允许使用链接的 SQL 语句。
  • 在由驱动程序生成的子查询中使用您的查询有助于确定该查询是否可以在单个节点上执行。因此,不能在语句的结尾包含分号,也不能包含使用双破折号的 SQL 注释 (--),因为这些会导致由驱动程序生成的查询失败。

可以通过对连接对象调用 createRoutableExecutor 方法来创建 VerticaRoutableExecutor。

createRoutableExecutor( schema‑name, table‑name )

例如:


VerticaRoutableConnection conn;
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("customer_key", 1);
try {
    conn = (VerticaRoutableConnection)
        jdbcSettings.getConnection();
     String table = "customers";
     VerticaRoutableExecutor q = conn.createRoutableExecutor(null, table);
     ...
}...

如果 schema‑name 设置为空,则会使用搜索路径来查找表。

VerticaRoutableExecutor 方法

VerticaRoutableExecutor 包含以下方法:

  • [execute](#execute()

  • [关闭](#close())

  • getWarnings

有关此类的详细信息,请参阅 JDBC 文档。

执行

execute( query-string, { column, value | map } )

运行查询。

需要满足以下要求:

  • 要执行的查询必须使用符合 VerticaRoutableExecutor 类规则的常规 SQL。例如,假如数据存在于单个节点上,则您可以添加限制和排序或使用聚合函数。

  • JDBC 客户端使用/映射实参来确定应在哪个节点上执行查询。查询的内容必须使用您在列/值或映射实参中提供的相同值。

  • 不能使用以下数据类型作为列值: * INTERVAL * TIMETZ * TIMESTAMPTZ

    此外,如果表在具有以下数据类型的任何列上分段,则无法使用可路由查询 API 查询此表:

将查询发送到服务器之前,驱动程序不会验证查询的语法。如果表达式不正确,则查询会失败。

关闭

close()

通过释放由此 VerticaRoutableExecutor 使用的资源来关闭此 VerticaRoutableExecutor。它不会关闭与 Vertica 的父 JDBC 连接。

getWarnings

getWarnings()

检索对此 VerticaRoutableExecutor 的调用所报告的第一个警告。其他警告均为链式,可使用 JDBC 方法 getNextWarning() 访问。

示例

以下示例显示如何使用 VerticaRoutableExecutor 执行同时使用 JOIN 子句和带有 GROUP BY 子句的聚合函数的查询。此示例还显示如何创建客户表和销售表,并对这些表进行分段,以便可以使用 VerticaRoutableExecutor 类联接这些表。此示例使用 VMart 架构中的 date_dimension 表来显示如何联接未分段的表上的数据。

  1. 创建 customers 表以存储客户详细信息,然后创建在表的 customer_key 列上分段的投影:

    
    => CREATE TABLE customers (customer_key INT, customer_name VARCHAR(128), customer_email VARCHAR(128));
    => CREATE PROJECTION cust_proj_b0 AS SELECT * FROM customers SEGMENTED BY HASH (customer_key) ALL NODES;
    => CREATE PROJECTION cust_proj_b1 AS SELECT * FROM customers SEGMENTED BY HASH (customer_key) ALL NODES OFFSET 1;
    => CREATE PROJECTION cust_proj_b2 AS SELECT * FROM customers SEGMENTED BY HASH (customer_key) ALL NODES OFFSET 2;
    => SELECT start_refresh();
    
  2. 创建 sales 表,然后创建在其 customer_key 列上分段的投影。由于 customersales 表按同一个键进行分段,因此您稍后可以使用 VerticaRoutableExecutor 可路由查询查找来联接这两个表。

    
    => CREATE TABLE sales (sale_key INT, customer_key INT, date_key INT, sales_amount FLOAT);
    => CREATE PROJECTION sales_proj_b0 AS SELECT * FROM sales SEGMENTED BY HASH (customer_key) ALL NODES;
    => CREATE PROJECTION sales_proj_b1 AS SELECT * FROM sales SEGMENTED BY HASH (customer_key) ALL NODES OFFSET 1;
    => CREATE PROJECTION sales_proj_b2 AS SELECT * FROM sales SEGMENTED BY HASH (customer_key) ALL NODES OFFSET 2;
    => SELECT start_refresh();
    
  3. 添加一些示例数据:

    => INSERT INTO customers VALUES (1, 'Fred', 'fred@example.com');
    => INSERT INTO customers VALUES (2, 'Sue', 'Sue@example.com');
    => INSERT INTO customers VALUES (3, 'Dave', 'Dave@example.com');
    => INSERT INTO customers VALUES (4, 'Ann', 'Ann@example.com');
    => INSERT INTO customers VALUES (5, 'Jamie', 'Jamie@example.com');
    => COMMIT;
    
    => INSERT INTO sales VALUES(1, 1, 1, '100.00');
    => INSERT INTO sales VALUES(2, 2, 2, '200.00');
    => INSERT INTO sales VALUES(3, 3, 3, '300.00');
    => INSERT INTO sales VALUES(4, 4, 4, '400.00');
    => INSERT INTO sales VALUES(5, 5, 5, '400.00');
    => INSERT INTO sales VALUES(6, 1, 15, '500.00');
    => INSERT INTO sales VALUES(7, 1, 15, '400.00');
    => INSERT INTO sales VALUES(8, 1, 35, '300.00');
    => INSERT INTO sales VALUES(9, 1, 35, '200.00');
    => COMMIT;
    
  4. 创建 VMart date_dimension 表的未分段投影以在此示例中使用。调用元函数 START_REFRESH 来取消现有数据的分段:

    => CREATE PROJECTION date_dim AS SELECT * FROM date_dimension UNSEGMENTED ALL NODES;
    => SELECT start_refresh();
    

现在,您可以使用 customersalesdate_dimension 数据创建如下可路由查询查找:使用联接和 group by 子句查询客户表并返回给定客户每天的总购买量:


import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import com.vertica.jdbc.kv.*;

public class verticaKV_doc {
    public static void main(String[] args) {
        com.vertica.jdbc.DataSource jdbcSettings
            = new com.vertica.jdbc.DataSource();
        jdbcSettings.setDatabase("VMart");
        jdbcSettings.setHost("vertica.example.com");
        jdbcSettings.setUserID("dbadmin");
        jdbcSettings.setPassword("password");
        jdbcSettings.setEnableRoutableQueries(true);
        jdbcSettings.setFailOnMultiNodePlans(true);
        jdbcSettings.setPort((short) 5433);
        VerticaRoutableConnection conn;
                Map<String, Object> map = new HashMap<String, Object>();
                map.put("customer_key", 1);
        try {
            conn = (VerticaRoutableConnection)
                jdbcSettings.getConnection();
            String table = "customers";
            VerticaRoutableExecutor q = conn.createRoutableExecutor(null, table);
            String query = "select d.date, SUM(s.sales_amount) as Total ";
                query += " from customers as c";
                query += " join sales as s ";
                query += " on s.customer_key = c.customer_key ";
                query += " join date_dimension as d ";
                query += " on d.date_key = s.date_key ";
                query += " where c.customer_key = " + map.get("customer_key");
                query += " group by (d.date) order by Total DESC";
            ResultSet rs = q.execute(query, map);
            while(rs.next()) {
                System.out.print("Date: " + rs.getString("date") + ":  ");
                System.out.println("Amount: " + rs.getString("Total"));
            }
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

示例代码生成如下输出:

Date: 2012-01-15:  Amount: 900.0
Date: 2012-02-04:  Amount: 500.0
Date: 2012-01-01:  Amount: 100.0

8.4 - 使用 VGet 类为可路由查询定义查询

如果不需要联接表或使用 group by 子句,您可以使用 VGet 类从单个节点直接访问表数据。与 VerticaRoutableExecutor 一样,VGet 直接查询包含查询所需的数据的 Vertica 节点,从而避免产生与正常的 Vertica 执行关联的分布式规划和执行成本。但是,VGet 不使用 SQL。相反,您可以定义谓词和值以对单个表执行键/值类型查找。VGet 非常适用于对单个表执行键/值类型查找。

可以通过对连接对象调用 prepareGet 方法来创建 VGet。

prepareGet( schema‑name, { table‑name | projection‑name } )

例如:


VerticaRoutableConnection conn;
try {
    conn = (VerticaRoutableConnection)
        jdbcSettings.getConnection();
        System.out.println("Connected.");
        VGet get = conn.prepareGet("public", "users");
              ...
}...

VGet 操作跨越多个 JDBC 连接(和多个 Vertica 会话),并且不遵循父连接的事务语义。如果需要在多个执行之间保持一致性,您可以使用父 VerticaRoutableConnection 的一致读取 API 来保证所有操作在同一时期进行。

VGet 是线程安全的,但会同步所有方法,因此共享 VGet 实例的线程永远不会并行运行。为了提高并行度,每个线程应具有各自的 VGet 实例。对同一个表执行操作的不同 VGet 实例共享池连接和元数据,以便实现较高的并行度。

VGet 方法

VGet 包含以下方法:

默认情况下,VGet 提取可满足多个谓词的逻辑 AND 的所有行的所有列,这些谓词通过 addPredicate 方法传递。您可以使用以下方法进一步自定义 get 操作:addOutputColumnaddOutputExpressionaddPredicateExpressionaddSortColumnsetLimit

addPredicate

addPredicate(string, object)

向查询添加谓词列和常量值。必须为用作表的分段依据的每个列包含一个谓词。谓词充当查询 WHERE 子句。多个 addPredicate 方法调用由 AND 修饰符联接。每次调用并执行后,VGet 都会保留此值。要移除它,请使用 clearPredicates

不能使用以下数据类型作为列值。同样,如果表在具有以下数据类型的任何列上分段,则不能使用可路由查询 API 查询此表:

addPredicateExpression

addPredicateExpression(string)

接受对表列执行操作的任意 SQL 表达式作为查询的输入。谓词表达式和谓词由 AND 修饰符联接。可以在谓词表达式中使用已分段的列,但您必须同时使用 addPredicate 将这些谓词表达式指定为常规谓词。每次调用并执行后,VGet 都会保留此值。要移除它,请使用 clearPredicates

驱动程序将表达式发送至服务器之前不会验证表达式的语法。如果表达式不正确,则查询会失败。

addOutputColumn

addOutputColumn(string)

添加要包含到输出的列。默认情况下,查询作为 SELECT * 运行,并且您不需要定义任何用于返回数据的输出列。如果要添加输出列,则您必须添加要返回的所有列。每次调用并执行后,VGet 都会保留此值。要移除它,请使用 clearOutputs

addOutputExpression

addOutputExpression(string)

接受对表列执行操作的任意 SQL 表达式作为输出。每次调用并执行后,VGet 都会保留此值。要移除它,请使用 ClearOutputs。

存在以下限制:

  • 驱动程序将表达式发送至服务器之前不会验证表达式的语法。如果表达式不正确,则查询会失败。

  • addOutputExpression 在查询 Flex 表时不受支持。如果尝试对 Flex 表查询使用 addOutputExpression,则会引发 SQLFeatureNotSupportedException

addSortColumn

addSortColumn(string, SortOrder)

向输出列添加排序顺序。输出列既可以是默认查询 (SELECT *) 所返回的一个列,也可以是 addSortColumn 或 addOutputExpress 中定义的列之一。可以定义多个排序列。

setLimit

setLimit(int)

对返回的结果数设置限制。值为 0 的限制表示无限制。

clearPredicates

clearPredicates()

移除由 addPredicateaddPredicateExpression 添加的谓词。

clearOutputs

clearOutputs()

移除由 addOutputColumnaddOutputExpression 添加的输出。

clearSortColumns

clearSortColumns()

移除先前由 addSortColumn 添加的排序列。

执行

execute()

运行查询。必须格外小心,以确保谓词列存在于 VGet 所使用的表和投影上,并确保表达式不需要在多个节点上执行。如果表达式由于十分复杂而需要在多个节点上执行,execute 将在 FailOnMultiNodePlans 连接属性设置为 true 时引发 SQLException。

关闭

close()

通过释放由此 VGet 使用的资源来关闭此 VGet。它不会关闭与 Vertica 的父 JDBC 连接。

getWarnings

getWarnings()

检索对此 VGet 的调用所报告的第一个警告。其他警告均为链式,可使用 JDBC 方法 getNextWarning 进行访问。

示例

以下代码查询在创建与可路由查询 API 一起使用的表和投影中定义的 users 表。该表定义了一个使用哈希算法进行分段的 id 列。

import java.sql.*;
import com.vertica.jdbc.kv.*;

public class verticaKV2 {
    public static void main(String[] args) {
        com.vertica.jdbc.DataSource jdbcSettings
            = new com.vertica.jdbc.DataSource();
        jdbcSettings.setDatabase("exampleDB");
        jdbcSettings.setHost("v_vmart_node0001.example.com");
        jdbcSettings.setUserID("dbadmin");
        jdbcSettings.setPassword("password");
        jdbcSettings.setEnableRoutableQueries(true);
        jdbcSettings.setPort((short) 5433);

        VerticaRoutableConnection conn;
        try {
            conn = (VerticaRoutableConnection)
                jdbcSettings.getConnection();
                System.out.println("Connected.");
            VGet get = conn.prepareGet("public", "users");
            get.addPredicate("id", 5);
            ResultSet rs = get.execute();
            rs.next();
            System.out.println("ID: " +
                rs.getString("id"));
            System.out.println("Username: "
                + rs.getString("username"));
            System.out.println("Email: "
                + rs.getString("email"));
            System.out.println("Closing Connection.");
            conn.close();
        } catch (SQLException e) {
            System.out.println("Error! Stacktrace:");
            e.printStackTrace();
        }
    }
}

该代码将生成以下输出:

Connected.
ID: 5
Username: userE
Email: usere@example.com
Closing Connection.

8.5 - 可路由查询性能和故障排除

本主题详细介绍性能注意事项和使用可路由查询 API 时可能会遇到的常见问题。

将资源池与可路由查询结合使用

各个可路由查询将快速得到处理,因为它们直接访问单个节点并返回一个或几个数据行。但在默认情况下,Vertica 资源池对 execution parallelism 参数使用 AUTO 设置。如果将此参数设置为 AUTO,则设置由可用的 CPU 核心数决定,并通常导致对资源池中的查询进行多线程执行。在服务器上创建并行线程会降低效率,因为可路由查询操作返回数据的速度太快,并且可路由查询操作仅使用单个线程查找行。若要防止服务器打开不需要的处理线程,应为可路由查询客户端创特定的资源池。请考虑用于可路由查询的资源池的以下设置:

  • 将执行并行度设置为 1,以强制执行单线程查询。此设置能够提高可路由查询的性能。

  • 使用 CPU 相关性将资源池限制为某个特定 CPU 或 CPU 集。此设置不但能够确保可路由查询具有可用资源,而且能够防止可路由查询对其他常规查询的系统性能造成显著影响。

  • 如果不为资源池设置 CPU 相关性,可以考虑将资源池的最大并发值设置为既能确保可路由查询性能良好又不会对常规查询的性能造成负面影响的设置。

可路由查询连接的性能注意事项

由于 VerticaRoutableConnection 将打开内部连接池,因此必须根据群集大小和同时客户端连接的数量适当地配置 MaxPooledConnectionsMaxPooledConnectionsPerNode。如果要使用 VerticaRoutableConnection 重载群集,则您无法影响正常的数据库连接。

与发起程序节点的初始连接可发现群集中的所有其他节点。发送 VerticaRoutableExecutor 或 VGet 查询之前,不会打开内部池连接。连接对象中的所有 VerticaRoutableExecutor/VGet 均使用来自内部池的连接,并受到 MaxPooledConnections 设置的限制。连接将保持打开,直至在已达到连接限制时关闭这些连接以便在其他位置打开新连接为止。

可路由查询故障排除

可路由查询问题通常分为以下两个类别:

  • 提供的谓词数量不足够。

  • 查询必须跨越多个节点。

谓词要求

必须提供与表(使用哈希算法进行分段)的列数相同的谓词数量。要确定分段列,请调用 Vertica 函数 GET_PROJECTIONS。必须为 Seg Cols 字段中显示的每个列提供一个谓词。

对于 VGet,这意味着您必须使用 addPredicate() 以添加每个列。对于 VerticaRoutableExecutor,这意味着您必须在发送到 execute() 的映射中提供所有谓词和值。

多节点故障

无法定义正确数量的谓词,但仍会发生故障,因为数据包含在多个节点上。发生此故障的原因是投影的数据未以某种方式进行分段,以使要查询的数据包含在单个节点上。请为连接启用日志记录并查看日志,以验证所使用的投影。如果客户端未选择正确的投影,则会通过在 create/prepare 语句中指定投影而非表来尝试直接查询投影,例如:

  • 使用 VerticaRoutableExecutor:

    conn.createRoutableExecutor(schema, table/projection);
    
  • 使用 VGet:

    conn.prepareGet('schema','table/projection')
    

此外,在 vsql 中使用 EXPLAIN 命令有助于确定查询是否可以在单个节点中运行。EXPLAIN 可以帮助了解为什么查询在单个或多个节点中运行。

8.6 - 使用 VHash 对数据进行预分段

VHash 类是 Vertica 哈希函数的实施,可与 JDBC 客户端应用程序结合使用。

使用 Vertica 中的哈希分段,您可以基于内置的哈希函数对投影进行分段。内置的哈希函数可使数据平均分布到群集中的部分或全部节点,从而提供最佳的查询执行。

假设您有数百万个值行分布在几千个 CSV 文件之中。假设您已创建了一个使用哈希算法进行分段的表。将值加载到数据库之前,您可能想要确定应将特定值加载到哪个节点。因此,使用 VHash 会特别有帮助,因为此类允许您在加载数据之前对数据进行预分段。

以下示例显示了对名为“testFile.csv”的文件的第一列使用哈希算法的 VHash 类。该文件中第一列的名称为 meterId

使用 VHash 对数据进行分段

以下示例演示了如何从本地文件系统读取 testFile.csv 文件并对 meteterId 列运行哈希函数。然后,您可以使用投影中的数据库元数据基于 meterId 的哈希值对该文件中的各个列进行预分段。

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.io.IOException;
import java.sql.*;

import com.vertica.jdbc.kv.VHash;

public class VerticaKVDoc {

    final Map<String, FileOutputStream> files;
    final Map<String, List<Long>> nodeToHashList;
    String segmentationMetadata;
    List<String> lines;

    public static void main(String[] args) throws Exception {
        try {
            Class.forName("com.vertica.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            System.err.println("Could not find the JDBC driver class.");
            e.printStackTrace();
            return;
        }

        Properties myProp = new Properties();
        myProp.put("user", "username");
        myProp.put("password", "password");

        VerticaKVDoc ex = new VerticaKVDoc();

        // Read in the data from a CSV file.
        ex.readLinesFromFile("C:\\testFile.csv");

        try (Connection conn = DriverManager.getConnection(
                "jdbc:vertica://VerticaHost:portNumber/databaseName", myProp)) {

        // Compute the hashes and create FileOutputStreams.
        ex.prepareForHashing(conn);

        }

        // Write to files.
        ex.writeLinesToFiles();
    }

    public VerticaKVDoc() {
        files = new HashMap<String, FileOutputStream>();
        nodeToHashList = new HashMap<String, List<Long>>();
    }

    public void prepareForHashing(Connection conn) throws SQLException,
            FileNotFoundException {

        // Send a query to Vertica to return the projection segments.
        try (ResultSet rs = conn.createStatement().executeQuery(
                "SELECT get_projection_segments('public.projectionName')")) {
            rs.next();
            segmentationMetadata = rs.getString(1);
        }

        // Initialize the data files.
        try (ResultSet rs = conn.createStatement().executeQuery(
                "SELECT node_name FROM nodes")) {
            while (rs.next()) {
                String node = rs.getString(1);
                files.put(node, new FileOutputStream(node + ".csv"));
            }
        }
    }

    public void writeLinesToFiles() throws UnsupportedEncodingException,
            IOException {
        for (String line : lines) {

            long hashedValue = VHash.hashLong(getMeterIdFromLine(line));

            // Write the row data to that node's data file.
            String node = VHash.getNodeFor(segmentationMetadata, hashedValue);

            FileOutputStream fos = files.get(node);
            fos.write(line.getBytes("UTF-8"));
        }
    }

    private long getMeterIdFromLine(String line) {

        // In our file, "meterId" is the name of the first column in the file.
        return Long.parseLong(line.split(",")[0]);
    }

    public void readLinesFromFile(String filename) throws IOException {
        lines = new ArrayList<String>();
        String line;
        try (BufferedReader reader = new BufferedReader(
                new FileReader(filename))) {
            while ((line = reader.readLine()) != null) {
                lines.add(line);
            }
        }
    }

}