Desde a versão GA do N1QL, recebemos muitas perguntas sobre a transferência de conteúdo de um banco de dados SQL para o Couchbase. Há muitas maneiras diferentes de fazer isso. Hoje, escolhi a que provavelmente é a mais simples. Transformarei cada linha de cada tabela em um JsonDocument e o armazenarei no Couchbase. Farei meu teste com o Postgres e seu conjunto de dados de amostra inspirado no MySQL Amostra de Sakila. Usarei Java, mas as diretrizes apresentadas aqui são aplicáveis a outras linguagens.
Conexão a um banco de dados SQL em execução
Como estou usando Java, implementarei o Spring Boot e seu pacote JDBC, que cuida da conexão com o banco de dados para mim. Tudo o que preciso fazer é definir as dependências e propriedades corretas para configurar o JdbcTemplate. Esse objeto facilita a execução de uma consulta SQL.
Dependências
Para garantir que tudo esteja configurado de forma organizada e automática, você precisa das seguintes dependências:
|
1 2 3 4 5 6 7 |
dependencies { compile "org.springframework.boot:spring-boot-starter", "org.springframework.boot:spring-boot-starter-data-jpa", "org.postgresql:postgresql:9.4-1206-jdbc4" } |
Estou testando com o Postgres, mas você pode adicionar qualquer outro driver compatível com o Spring JDBC. O spring-boot-starter-data-jpa permitirá que eu injete o JdbcTemplate pré-configurado.
Configuração
Para garantir que a estrutura do Spring encontre seu banco de dados, adicione as seguintes propriedades ao seu arquivo de configuração (por exemplo, src/main/resources/application.properties).
|
1 2 3 4 5 6 7 8 9 10 |
spring.jpa.database=POSTGRESQL spring.datasource.platform=postgres spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=create-drop spring.database.driverClassName=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://192.168.99.100:5432/dvdrental spring.datasource.username=postgres spring.datasource.password=password |
É claro que você precisará ajustar isso de acordo com o banco de dados que estiver usando. Aqui estou usando o Postgres em execução em 192.168.99.100 com a porta padrão 5432. O nome do banco de dados que quero usar é dvdrental.
Código
Se tudo estiver configurado corretamente, você poderá injetar o JdbcTemplate e começar a consultar o banco de dados SQL.
|
1 2 3 4 5 6 7 8 9 10 |
@Autowired JdbcTemplate jdbcTemplate; @Override public void doStuff() throws Exception { String sql = "SELECT id FROM table"; Long id = jdbcTemplate.queryForObject(sql, Long.class); } |
Conexão com o Couchbase
Meu objetivo é mover o conteúdo de um banco de dados SQL para o Couchbase, portanto, também precisamos de uma conexão com o Couchbase.
Dependências
Para trabalhar com o Couchbase em seu projeto Java, é necessário adicionar a seguinte dependência:
|
1 2 3 4 5 |
dependencies { compile "com.couchbase.client:java-client:2.2.3" } |
Isso lhe dará acesso ao Couchbase Java SDK.
Configuração
Uma configuração básica do Couchbase requer basicamente três propriedades: um endereço IP do servidor, um nome de bucket e uma senha de bucket. Fazer isso no Spring Boot seria parecido com o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@Configuration public class Database { @Value("${hostname}") private String hostname; @Value("${bucket}") private String bucket; @Value("${password}") private String password; public @Bean Cluster cluster() { return CouchbaseCluster.create(hostname); } public @Bean Bucket bucket() { return cluster().openBucket(bucket, password); } } |
As propriedades nome do host, bucket e senha podem ser adicionadas diretamente ao arquivo de propriedades do aplicativo.
|
1 2 3 4 5 6 7 8 |
# Hostnames, comma separated list of Couchbase node IP or hostname hostnames: localhost,127.0.0.1 # Bucket name bucket: default # Bucket password password: |
Código
Com o Couchbase, o nível de granularidade equivalente de um banco de dados seria um bucket, que é onde você armazena documentos. Com a configuração anterior, você pode simplesmente injetar um bucket e começar a brincar.
|
1 2 3 4 5 6 7 8 9 |
@Autowired Bucket bucket; @Override public void doStuff() throws Exception { JsonDocument doc = bucket.get("key"); } |
Tabelas
Neste ponto, você tem uma conexão com um banco de dados SQL e com o Couchbase. Agora podemos começar a mover as coisas. A maneira mais fácil de mover dados é considerar cada linha de cada tabela como um documento.
Obtendo o esquema SQL
Vamos começar obtendo o esquema do banco de dados automaticamente usando o JdbcTemplate. O objeto interessante aqui é DatabaseMetaDataque pode nos fornecer a estrutura completa do banco de dados. A API não é a mais fácil de usar, mas pelo menos está documentada.
Vou mapear o resultado da consulta DatabaseMetaData para uma lista de tabelas e colunas. Para isso, criei a seguinte classe Java:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
public class Table { private String name; private List<Column> columns = new ArrayList<Column>(); private String primaryKey; public Table(String tableName) { this.name = tableName; } public void setPrimaryKey(String primaryKey) { this.primaryKey = primaryKey; } public void addColumn(String name, int type) { columns.add(new Column(name, type)); } public String getName() { return name; } public List<Column> getColumns() { return columns; } public String getPrimaryKey() { return primaryKey; } public JsonObject toJsonObject() { JsonObject obj = JsonObject.create(); JsonArray jsonColumns = JsonArray.create(); for (Column col : columns) { jsonColumns.add(col.toJsonObject()); } obj.put("tableName", name); obj.put("primaryKey", primaryKey); obj.put("columns", jsonColumns); return obj; } } public class Column { private String name; private int type; public Column(String name, int type) { this.name = name; this.type = type; } public String getName() { return name; } public int getType() { return type; } public JsonObject toJsonObject() { JsonObject obj = JsonObject.create(); obj.put("name", name); obj.put("type", type); return obj; } } |
Definitivamente, não é o código mais empolgante de se escrever, mas no final você obtém uma representação JSON das tabelas do seu banco de dados SQL.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public void getDatabaseSchema() throws Exception { // get Database Medatadata objects to retrieve Tables schema DatabaseMetaData databaseMetadata = jdbcTemplate.getDataSource().getConnection().getMetaData(); List<String> tableNames = new ArrayList<String>(); // Get tables names ResultSet result = databaseMetadata.getTables(catalog, schemaPattern, tableNamePattern, types); while (result.next()) { String tablename = result.getString(3); String tableType = result.getString(4); // make sure we only import table(as oppose to Views, counter etc...) if (!tablename.isEmpty() && "TABLE".equals(tableType)) { tableNames.add(tablename); log.debug("Will import table " + tablename); } } // Map the tables schema to Table objects Map<String, Table> tables = new HashMap<String, Table>(); JsonObject tablesSchema = JsonObject.create(); for (String tableName : tableNames) { result = databaseMetadata.getColumns(catalog, schemaPattern, tableName, columnNamePattern); Table table = new Table(tableName); while (result.next()) { String columnName = result.getString(4); // Maps to JDBCType enum int columnType = result.getInt(5); table.addColumn(columnName, columnType); } result = databaseMetadata.getPrimaryKeys(catalog, schemaPattern, tableName); while (result.next()) { String columnName = result.getString(4); table.setPrimaryKey(columnName); } tables.put(tableName, table); tablesSchema.put(tableName, table.toJsonObject()); } JsonDocument schemaDoc = JsonDocument.create(tablesSchemaId, tablesSchema); JsonDocument doc = bucket.upsert(schemaDoc); } |
Conteúdo
Aqui está a parte divertida. É aqui que começamos a mapear uma linha de tabela para um JsonDocument. A seção anterior nos coloca em um estado em que podemos recuperar o nome de todas as tabelas. A partir de um nome de tabela, podemos criar uma consulta SQL que retorna todas as linhas da tabela.
O Spring tem um mecanismo que permite que você defina um RowMapper. Para cada linha retornada pela consulta, você pode retornar o objeto que desejar. Como estou usando o Couchbase, quero um JsonDocument.
A seguir, um exemplo de implementação. Esse RowMapper precisa de um objeto Table previamente definido; portanto, temos de implementar o método mapRow. Há várias coisas que precisamos fazer aqui.
A primeira tarefa é criar uma chave exclusiva. Como o escopo das linhas é definido por tabelas, alguns IDs podem ser exatamente os mesmos para linhas em tabelas diferentes. Mas os documentos têm escopo por bucket, portanto, precisamos criar uma chave de documento exclusiva que reflita o ID da linha e o nome da tabela. Para manter o controle da origem do documento, também adicionarei um campo _tableName para o nome da tabela.
Em seguida, a etapa interessante vem do mapeamento de tipos. O JSON é compatível com menos tipos do que um banco de dados SQL, portanto, temos que fazer algumas conversões aqui. É isso que o método getJsonTypedValue faz. Ele garante que a maioria dos tipos JDBC possa ser convertida em um tipo JSON nativo (String, número, booleano, matriz, objeto, nulo). No final, temos um JsonDocument que pode ser salvo no Couchbase.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
public class JSONRowMapper implements RowMapper<Document> { Table table; public JSONRowMapper(Table table) { this.table = table; } public JsonDocument mapRow(ResultSet rs, int rowNum) throws SQLException { String id = table.getName() + "::" + rs.getString(table.getPrimaryKey()); JsonObject obj = JsonObject.create(); obj.put("_tableName", table.getName()); for (Column col : table.getColumns()) { Object value = getJsonTypedValue(col.type, rs.getObject(col.name), col.name); obj.put(col.name, value); } return JsonDocument.create(id, obj); } public Object getJsonTypedValue(int type, Object value, String name) throws SQLException { if (value == null) { return null; } JDBCType current = JDBCType.valueOf(type); switch (current) { case TIMESTAMP: Timestamp timestamp = (Timestamp) value; return timestamp.getTime(); case TIMESTAMP_WITH_TIMEZONE: Timestamp ts = (Timestamp) value; JsonObject tsWithTz = JsonObject.create(); tsWithTz.put("timestamp", ts.getTime()); tsWithTz.put("timezone", ts.getTimezoneOffset()); return tsWithTz; case DATE: Date sqlDate = (Date) value; return sqlDate.getTime(); case DECIMAL: case NUMERIC: BigDecimal bigDecimal = (BigDecimal) value; return bigDecimal.doubleValue(); case ARRAY: Array array = (Array) value; Object[] objects = (Object[]) array.getArray(); return JsonArray.from(objects); case BINARY: case BLOB: case LONGVARBINARY: return Base64.getEncoder().encodeToString((byte[]) value); case OTHER: case JAVA_OBJECT: // database specific, default to String value return value.toString(); default: return value; } } } |
Com esse RowMapper, as coisas ficam muito fáceis. Podemos fazer um loop pelo nome da tabela, executar a consulta e salvar os resultados no Couchbase. Fazer isso de forma síncrona seria assim:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
for (String tableName : tableNames) { String sql = "select * from " + tableName + ";"; List<JsonDocument> rs = jdbcTemplate.query(sql, new JSONRowMapper(tables.get(tableName))); if (!rs.isEmpty()) { for (JsonDocument doc : rs) { bucket.upsert(doc); } } } bucket.upsert(schemaDoc); |
Mas eu prefiro a versão assíncrona:
|
1 2 3 4 5 6 7 8 |
Observable.from(tableNames).flatMap(s -> { String sql = String.format("Select * from %s;", s); return Observable.from(jdbcTemplate.query(sql, new JSONRowMapper(tables.get(s)))); }) // start by a jsonDocument containing the tables to be imported. .startWith(schemaDoc).flatmap(doc -> asyncBucket.upsert(doc)); |
Aqui não estou usando todo o potencial do Rx; dê uma olhada em esta função que grava um documento no Couchbase e lida com o tempo limite e o gerenciamento de erros.
Por conveniência, agrupei todas as etapas implementadas e mostradas anteriormente em um arquivo projeto único. Tudo o que você precisa fazer é verificar se o arquivo de propriedades está configurado corretamente e executar o importador:
|
1 2 3 |
$ ./bin/couchbase-java-importer myConfiguration.properties |
Dê uma olhada no LEIAME para obter mais informações.
Conclusão
Hoje, aprendemos a mover o conteúdo SQL para o Couchbase, mas ainda há algum trabalho a ser feito. Na próxima vez, falarei sobre como mover a lógica de negócios do SQL para a camada de aplicativos.