Write to and read from BigQuery#

Assumes the artifacts and credentials from the Quickstart index. The whole file, since it is the one worth copying verbatim; the other connectors’ pages show only the job.

Write a stream to a table#


package example;

import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

import com.google.cloud.bigquery.Field;
import com.google.cloud.bigquery.Schema;
import com.google.cloud.bigquery.StandardSQLTypeName;
import io.github.flink.gcp.connector.bigquery.sink.BigQuerySink;
import io.github.flink.gcp.connector.bigquery.sink.TableDestination;
import io.github.flink.gcp.connector.bigquery.sink.serializer.json.JsonDocumentSerializationSchema;

public class BigQueryQuickstartWrite {

    public static void main(String[] args) throws Exception {
        // JSON carries no schema, so this one is supplied rather than derived. Serializers for
        // input that does carry a schema — protobuf messages and Avro records — derive it.
        Schema schema =
                Schema.of(
                        Field.of("order_id", StandardSQLTypeName.STRING),
                        Field.of("amount", StandardSQLTypeName.INT64));

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
        // Not optional. Every sink here is at-least-once *only* with checkpointing: the checkpoint
        // is what makes Flink flush what the Google client libraries are still holding, so without
        // it those records are lost on failure.
        env.enableCheckpointing(60_000);

        env.fromData("{\"order_id\":\"a-1\",\"amount\":10}", "{\"order_id\":\"a-2\",\"amount\":20}")
                .sinkTo(
                        BigQuerySink.<String>builder()
                                .table(TableDestination.of("my-project", "my_dataset", "orders"))
                                .serializer(JsonDocumentSerializationSchema.of(schema))
                                .build());

        env.execute("bigquery-quickstart");
    }
}

The dataset must exist; the table need not, because the default create disposition is CREATE_IF_NEEDED and the schema above is what it is created from. The default write method is STORAGE_API_AT_LEAST_ONCE, which makes rows queryable within seconds — the other two, and when to reach for them, are under exactly-once.

Schema here is the BigQuery REST client’s type, so a schema read back from the destination table with BigQuery.getTable(...) can be passed straight in. The traps in the JSON conversion — a bare number in a TIMESTAMP column is epoch microseconds, a JSON column takes text rather than an object, a BYTES column takes an array of byte values rather than base64 — are on the BigQuery connector page, along with the protobuf and Avro serializers for input that is not JSON.

Read a table#

The other direction is a bounded source over the Storage Read API. It finishes when the table has been read, so it works both as a batch input and as the dimension side of a join in a streaming job.

Schema readerSchema =
        new Schema.Parser()
                .parse(
                        "{\"type\":\"record\",\"name\":\"Person\",\"fields\":["
                                + "{\"name\":\"id\",\"type\":\"long\"},"
                                + "{\"name\":\"name\",\"type\":\"string\"}]}");

Source<GenericRecord, ?, ?> source =
        BigQuerySource.<GenericRecord>builder()
                .table(TableDestination.of("my-project", "my_dataset", "people"))
                .deserializer(BigQueryRowDeserializationSchema.genericRecord(readerSchema))
                .selectedFields("id", "name")
                .rowRestriction("id > 1000")
                .build();

env.fromSource(source, WatermarkStrategy.noWatermarks(), "BigQuery")
        .map(row -> row.get("name").toString())
        .print();

Schema here is Avro’s, not the REST client’s: rows arrive as Avro, and the schema you pass is what they are read into — naming the columns you want with their natural types is enough, since Avro’s schema resolution maps the table’s schema onto it. Reading GenericRecords needs flink-avro on the job’s classpath.

selectedFields and rowRestriction are applied by BigQuery before anything is sent, which matters for more than speed: a read through this API is charged for the bytes it scans, unlike the free FILE_LOADS write path, and a column you do not select is a column BigQuery does not scan.

Next#

Choose the direction and API you need next: