1. Introduction
Micronaut BigQuery library helps to simplify usage of BigQuery in Micronaut application.
The main use case is running simple queries and updates.
There is a micronaut-bigquery-mock
artefact which allows you to run
against an arbitrary database instead of BigQuery.
Groovy language has a first-class support. You can use GString
to define staments which are
automatically converted into named SQL statements.
2. Installation
The libraries are available in JCenter repository:
repositories {
jcenter()
}
dependencies {
// the main library
compile 'com.agorapulse:micronaut-bigquery:0.3.1-micronaut-3.0'
// the mock library for testing
testCompile 'com.agorapulse:micronaut-bigqery-mock:0.3.1-micronaut-3.0'
// required for testing using Testcontainers (see examples bellow)
testCompile 'io.micronaut.sql:micronaut-jdbc-tomcat'
testCompile 'org.postgresql:postgresql:42.2.9'
testCompile 'org.testcontainers:spock:1.14.3'
testCompile 'org.testcontainers:postgresql:1.14.3'
}
2.1. Connectiong to Google Cloud
To get started with the library locally you need to setup GOOGLE_APPLICATION_CREDENTIALS
environment variable
pointing to the JSON file obtained from Google Cloud Console. Follow the Quickstart: Using client libraries tutorial
to get one.
To use the JSON locally for running the application you can add following snippet into your build.gradle
file:
run {
environment 'GOOGLE_APPLICATION_CREDENTIALS', file('path/to/credentials.json').absolutePath
}
If you are using the library within Google Cloud then BigQuery connection will be configured for you once the BigQuery API is enabled and configured for the project.
3. Usage
The library provides BigQuery
and BigQueryService
beans into the application context.
BigQueryService
has a couple of useful methods to execute SQL against BigQuery
data warehouse - insert
, query
and execute
.
In a following examples we’re be reffering to a class Person
:
@Introspected (1)
public class Person {
private long id;
private boolean enabled = true;
private Instant created = Instant.now();
private double score = 1.0;
private String firstName;
private String lastName;
private String email;
private Role role;
// getters, setters, equals, hash code
}
1 | classes used for database mapping must be annotated with @Introspected (if you plan to use insert method) |
The example bellow are defined inside a person service class:
@Singleton
public class JavaPersonService {
private final String schema;
private final String table;
private final BigQueryService bq;
public JavaPersonService(
@Value("${person.schema:persons}") String schema,
@Value("${person.table:persons}") String table,
BigQueryService bq
) {
this.schema = schema;
this.table = table;
this.bq = bq;
}
}
@Singleton
class GroovyPersonService {
private final String schema
private final String table
private final BigQueryService bq
GroovyPersonService(
@Value('${person.schema:persons}') String schema,
@Value('${person.table:persons}') String table,
BigQueryService bq
) {
this.schema = schema
this.table = table
this.bq = bq
}
}
3.1. Inserting New Rows
Inserting new rows is very easy. You only need to suppy the object and the name of the data set and table.
public Person createPerson(String firstName, String lastName, String email, Role role) {
Person person = new Person();
person.setId(System.currentTimeMillis());
person.setFirstName(firstName);
person.setLastName(lastName);
person.setEmail(email);
person.setRole(role);
return bq.insert(person, schema, table);
}
Person createPerson(String firstName, String lastName, String email, Role role) {
return bq.insert(new Person(
id: System.currentTimeMillis(),
firstName: firstName,
lastName: lastName,
role: role,
email: email
), schema, table)
}
Alternatively you can use execute
method to have a full control over the SQL statement.
3.2. Running Queries
If you want to retrieve single item you can run querySingle
method which returns an Optional
:
public Optional<Person> get(long id) {
return bq.querySingle(
Collections.singletonMap("id", id), (1)
String.format("select * from %s.%s where id = @id", schema, table), (2)
JavaPersonService::buildPerson (3)
);
}
1 | define the named parameter map |
2 | BigQuery uses @ prefix for the named parameters, : won’t be accepted |
3 | buildPerson method reference constructs the Person object |
Optional<Person> get(long id) {
return bq.querySingle("select * from ${schema}.${table} where id = $id") { (1)
return buildPerson(it) (2)
}
}
1 | use GString to define the query, only variables after first occurence of where , on , set , values are considered named parameters so it is safe to use variables for data set and table |
2 | buildPerson called from within the closure |
If you want to retrieve single more items you can run query
method which returns Flowable
:
public Flowable<Person> findByLastName(String lastName) {
return bq.query(
Collections.singletonMap("last_name", lastName),
String.format("select * from %s.%s where last_name = @last_name", schema, table),
JavaPersonService::buildPerson
);
}
Flowable<Person> findByLastName(String lastName) {
return bq.query("select * from ${schema}.${table} where last_name = $lastName") {
return buildPerson(it)
}
}
This is how the buildPerson
method looks like:
private static Person buildPerson(RowResult result) {
Person person = new Person();
person.setId(result.getLongValue("id"));
person.setFirstName(result.getStringValue("first_name"));
person.setLastName(result.getStringValue("last_name"));
person.setEmail(result.getStringValue("email"));
person.setRole(result.getEnumValue("role", Role.class));
person.setScore(result.getDoubleValue("score"));
person.setCreated(result.getTimestampValue("created"));
person.setEnabled(result.getBooleanValue("enabled"));
return person;
}
private static Person buildPerson(RowResult result) {
return new Person(
id: result.getLongValue('id'),
firstName: result.getStringValue('first_name'),
lastName: result.getStringValue('last_name'),
email: result.getStringValue('email'),
role: result.getEnumValue('role', Role),
score: result.getDoubleValue('score'),
created: result.getTimestampValue('created'),
enabled: result.getBooleanValue('enabled')
)
}
3.3. Executing Write Operations
You can execute write operations using execute
method. This method does not return any values but if there is a problem
with your statement it will throw an exception.
This is an example of updating a role of the Person
:
public void updateRole(long id, Role role) {
Map<String, Object> parameters = new HashMap<>();
parameters.put("id", id);
parameters.put("role", role);
bq.execute(
parameters, (1)
String.format("update %s.%s set role = @role where id = @id", schema, table)(2)
);
}
1 | define the named parameter map |
2 | BigQuery uses @ prefix for the named parameters, : won’t be accepted |
void updateRole(long id, Role role) {
bq.execute """
update ${schema}.${table}
set
role = $role
where
id = $id
"""
}
This is an example of deleting a Person
:
public void deletePerson(long id) {
bq.execute(
Collections.singletonMap("id", id), (1)
String.format("delete from %s.%s where id = @id", schema, table) (2)
);
}
1 | define the named parameter map |
2 | BigQuery uses @ prefix for the named parameters, : won’t be accepted |
void deletePerson(long id) {
bq.execute "delete from ${schema}.${table} where id = $id"
}
3.4. Testing
Once micronaut-bigquery-mock
is on the classpath the BigQueryService
is replaced with pure SQL implementation.
You can use Testcontainers to setup a test database which emulates BigQuery:
@Testcontainers
class PersonServiceSpec {
private static final String TABLE_DEFINITION = '''
CREATE SCHEMA persons;
CREATE TABLE persons.persons (
id bigserial primary key,
first_name character varying(256),
last_name character varying(256),
email character varying(256),
role character varying(10),
score numeric,
enabled boolean,
created timestamp
);
'''
private static final String DRIVER = 'org.postgresql.Driver'
@Shared PostgreSQLContainer container = new PostgreSQLContainer() (1)
@AutoCleanup ApplicationContext context
void setupSpec() {
Sql sql = Sql.newInstance(
container.jdbcUrl,
container.username,
container.password,
DRIVER
)
sql.execute(TABLE_DEFINITION) (2)
}
void setup() {
context = ApplicationContext.builder( (3)
'datasources.default.url': container.jdbcUrl,
'datasources.default.driverClassName': DRIVER,
'datasources.default.username': container.username,
'datasources.default.password': container.password,
).build()
context.start()
}
void 'test person service'() {
given:
PersonService service = context.getBean(serviceType) (4)
// tests
}
}
1 | Using PostgreSQL running using Testcontainers to emulate BigQuery |
2 | Create the table inside the test database |
3 | Start Micronaut context with a default datasource pointing to the test database |
4 | Get the instance of the service under test from the application context |