Since the GA release of N1QL, we get a lot of questions about moving content from a SQL database to Couchbase. There are many different ways to do so. Today, I have chosen what is probably the simplest. I will transform each row of each table in a JsonDocument and store it in Couchbase. I will do my test with Postgres and their sample dataset inspired by MySQL Sakila sample. I will use Java, but the guidelines presented here are applicable to other languages.
Connecting to a running SQL database
Since I am using Java, I will implement Spring Boot and their JDBC package, which handles the db connection for me. All I have to do is set up the right dependencies and properties to configure the JdbcTemplate. This object makes running a SQL query a breeze.
Dependencies
To make sure you have everything configured neatly and automatically you need the following dependencies:
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" } |
I am testing with Postgres but you could add any other driver supported by Spring JDBC. The spring-boot-starter-data-jpa will allow me to inject the preconfigured JdbcTemplate.
Configuration
To make sure the Spring framework finds your database, add the following properties to your configuration file (for example, src/main/resources/application.properties).
1 2 3 4 5 6 7 8 9 10 |
spring.jpa.データベース=POSTGRESQL spring.datasource.プラットフォーム=postgres spring.jpa.ショー-sql=真の spring.jpa.hibernate.ddl-auto=作成する-drop spring.データベース.driverClassName=オルグ.postgresql.Driver spring.datasource.url=jdbc:postgresql://192.168.99.100:5432/dvdrental spring.datasource.ユーザー名=postgres spring.datasource.パスワード=パスワード |
Of course you would need to fine-tune this according to the database you are using. Here I am using Postgres running on 192.168.99.100 with default port 5432. The name of the database I want to use is dvdrental.
Code
If everything is configured correctly you should be able to inject the JdbcTemplate and start querying your SQL DB.
1 2 3 4 5 6 7 8 9 10 |
@Autowired JdbcTemplate jdbcTemplate; @オーバーライド 公開 ボイド doStuff() スロー 例外 { ストリング sql = "SELECT id FROM table"; Long アイドル = jdbcTemplate.queryForObject(sql, Long.クラス); } |
Connecting to Couchbase
My goal is to move content from a SQL database to Couchbase, so we also need a Couchbase connection.
Dependencies
Working with Couchbase on your Java project requires you to add the following dependency:
1 2 3 4 5 |
dependencies { compile "com.couchbase.client:java-client:2.2.3" } |
This will give you access to the Couchbase Java SDK.
Configuration
A basic Couchbase configuration requires basically three properties: one server IP address, a bucket name, and a bucket password. Doing this in a Spring Boot fashion would look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@Configuration 公開 クラス データベース { @Value("${hostname}") プライベート ストリング hostname; @Value("${bucket}") プライベート ストリング バケット; @Value("${password}") プライベート ストリング パスワード; 公開 @Bean クラスター クラスタ() { 戻る CouchbaseCluster.作成する(hostname); } 公開 @Bean バケット バケット() { 戻る クラスタ().オープンバケット(バケット, パスワード); } } |
The properties hostname, bucket, and password can be added directly to your application properties file.
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 password パスワード: |
Code
With Couchbase, the equivalent granularity level of a database would be a bucket, which is where you store documents. With the previous configuration you can simply inject a bucket and start playing around.
1 2 3 4 5 6 7 8 9 |
@Autowired バケット バケット; @オーバーライド 公開 ボイド doStuff() スロー 例外 { JsonDocument ドク = バケット.得る("key"); } |
Tables
At this point you have a connection to a SQL database and Couchbase. Now we can start moving things around. The easiest way to move data is to consider each row of each table as a document.
Getting the SQL schema
Let’s start by getting the schema of the database automatically using the JdbcTemplate. The interesting object here is DatabaseMetaData, which can give us the complete structure of the database. The API is not the easiest to use, but at least it’s documented.
I will map the result of the DatabaseMetaData query to a list of Table and Column. I have created the following Java class to do so:
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 |
公開 クラス Table { プライベート ストリング 名称; プライベート リスト<Column> 列 = 新しい 配列リスト<Column>(); プライベート ストリング primaryKey; 公開 Table(ストリング tableName) { これ.名称 = tableName; } 公開 ボイド setPrimaryKey(ストリング primaryKey) { これ.primaryKey = primaryKey; } 公開 ボイド addColumn(ストリング 名称, イント タイプ) { 列.追加(新しい Column(名称, タイプ)); } 公開 ストリング getName() { 戻る 名称; } 公開 リスト<Column> getColumns() { 戻る 列; } 公開 ストリング getPrimaryKey() { 戻る primaryKey; } 公開 JsonObject toJsonObject() { JsonObject obj = JsonObject.作成する(); JsonArray jsonColumns = JsonArray.作成する(); にとって (Column col : 列) { jsonColumns.追加(col.toJsonObject()); } obj.置く("tableName", 名称); obj.置く("primaryKey", primaryKey); obj.置く("columns", jsonColumns); 戻る obj; } } 公開 クラス Column { プライベート ストリング 名称; プライベート イント タイプ; 公開 Column(ストリング 名称, イント タイプ) { これ.名称 = 名称; これ.タイプ = タイプ; } 公開 ストリング getName() { 戻る 名称; } 公開 イント getType() { 戻る タイプ; } 公開 JsonObject toJsonObject() { JsonObject obj = JsonObject.作成する(); obj.置く(名前, 名称); obj.置く(「タイプ, タイプ); 戻る obj; } } |
It’s definitely not the most exciting code to write, but at the end you get a JSON representation of your SQL database tables.
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 |
公開 ボイド getDatabaseSchema() スロー 例外 { // get Database Medatadata objects to retrieve Tables schema DatabaseMetaData databaseMetadata = jdbcTemplate.getDataSource().getConnection().getMetaData(); リスト<ストリング> tableNames = 新しい 配列リスト<ストリング>(); // Get tables names ResultSet 結果 = databaseMetadata.getTables(catalog, schemaPattern, tableNamePattern, types); 同時に (結果.次のページ()) { ストリング tablename = 結果.ゲットストリング(3); ストリング tableType = 結果.ゲットストリング(4); // make sure we only import table(as oppose to Views, counter etc...) もし (!tablename.isEmpty() && "TABLE".equals(tableType)) { tableNames.追加(tablename); ログ.デバッグ("Will import table " + tablename); } } // Map the tables schema to Table objects 地図<ストリング, Table> tables = 新しい ハッシュマップ<ストリング, Table>(); JsonObject tablesSchema = JsonObject.作成する(); にとって (ストリング tableName : tableNames) { 結果 = databaseMetadata.getColumns(catalog, schemaPattern, tableName, columnNamePattern); Table テーブル = 新しい Table(tableName); 同時に (結果.次のページ()) { ストリング columnName = 結果.ゲットストリング(4); // Maps to JDBCType enum イント columnType = 結果.getInt(5); テーブル.addColumn(columnName, columnType); } 結果 = databaseMetadata.getPrimaryKeys(catalog, schemaPattern, tableName); 同時に (結果.次のページ()) { ストリング columnName = 結果.ゲットストリング(4); テーブル.setPrimaryKey(columnName); } tables.置く(tableName, テーブル); tablesSchema.置く(tableName, テーブル.toJsonObject()); } JsonDocument schemaDoc = JsonDocument.作成する(tablesSchemaId, tablesSchema); JsonDocument ドク = バケット.アップサート(schemaDoc); } |
Content
Here’s the fun part. This is where we start mapping a table row to a JsonDocument. The previous section puts us in a state where we can retrieve the name of all the tables. From one table name, we can create a SQL query that returns every row of the table.
Spring has a mechanism that allows you to define a RowMapper. For each row returned by the query, you can return the object you want. Since I am using Couchbase, I want a JsonDocument.
Following is an implementation example. This RowMapper needs a Table object previously defined; therefore, we have to implement the mapRow method. There are several things we need to do here.
The first task is to create a unique key. As rows are scoped by tables, some id can be exactly the same for rows in different tables. But documents are scoped by bucket, so we need to create a unique document key that reflects the row id and the table name. To keep track of where the document comes from, I will also add a _tableName field for the table name.
Then, the exciting step comes from the type mapping. JSON supports fewer types than a SQL database, so we have some conversion to do here. This is what the getJsonTypedValue method does. It makes sure most JDBC type can be converted to a native JSON type (String, number, boolean, array, object, null). At the end, we have a JsonDocument that can be saved in 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 |
公開 クラス JSONRowMapper 用具 RowMapper<ドキュメント> { Table テーブル; 公開 JSONRowMapper(Table テーブル) { これ.テーブル = テーブル; } 公開 JsonDocument mapRow(ResultSet rs, イント rowNum) スロー SQLException { ストリング アイドル = テーブル.getName() + "::" + rs.ゲットストリング(テーブル.getPrimaryKey()); JsonObject obj = JsonObject.作成する(); obj.置く("_tableName", テーブル.getName()); にとって (Column col : テーブル.getColumns()) { 対象 価値 = getJsonTypedValue(col.タイプ, rs.getObject(col.名称), col.名称); obj.置く(col.名称, 価値); } 戻る JsonDocument.作成する(アイドル, obj); } 公開 対象 getJsonTypedValue(イント タイプ, 対象 価値, ストリング 名称) スロー SQLException { もし (価値 == ヌル) { 戻る ヌル; } JDBCType current = JDBCType.バリューオブ(タイプ); switch (current) { case TIMESTAMP: Timestamp タイムスタンプ = (Timestamp) 価値; 戻る タイムスタンプ.getTime(); case TIMESTAMP_WITH_TIMEZONE: Timestamp ts = (Timestamp) 価値; JsonObject tsWithTz = JsonObject.作成する(); tsWithTz.置く(「タイムスタンプ, ts.getTime()); tsWithTz.置く("timezone", ts.getTimezoneOffset()); 戻る tsWithTz; case 日付: 日付 sqlDate = (日付) 価値; 戻る sqlDate.getTime(); case DECIMAL: case NUMERIC: BigDecimal bigDecimal = (BigDecimal) 価値; 戻る bigDecimal.doubleValue(); case ARRAY: 配列 array = (配列) 価値; 対象[] objects = (対象[]) array.getArray(); 戻る JsonArray.より(objects); case BINARY: case BLOB: case LONGVARBINARY: 戻る Base64.getEncoder().encodeToString((バイト[]) 価値); case OTHER: case JAVA_OBJECT: // database specific, default to String value 戻る 価値.文字列(); デフォルト: 戻る 価値; } } } |
With that RowMapper it makes things really easy. We can loop through the table’s name, run the query, and save the results in Couchbase. Doing this in a synchronous fashion would look like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
にとって (ストリング tableName : tableNames) { ストリング sql = "select * from " + tableName + ";"; リスト<JsonDocument> rs = jdbcTemplate.クエリー(sql, 新しい JSONRowMapper(tables.得る(tableName))); もし (!rs.isEmpty()) { にとって (JsonDocument ドク : rs) { バケット.アップサート(ドク); } } } バケット.アップサート(schemaDoc); |
But I prefer the async version:
1 2 3 4 5 6 7 8 |
Observable.より(tableNames).flatMap(s -> { ストリング sql = ストリング.フォーマット("Select * from %s;", s); 戻る Observable.より(jdbcTemplate.クエリー(sql, 新しい JSONRowMapper(tables.得る(s)))); }) // start by a jsonDocument containing the tables to be imported. .スタートウィズ(schemaDoc).flatmap(ドク -> asyncBucket.アップサート(ドク)); |
Here I am not using the full potential of Rx; take a look at this function that writes a doc to Couchbase and handles timeout and error management.
For convenience, I have packaged all steps implemented and previously shown in a single project. All you have to do is make sure your properties file is configured right and run the importer:
1 2 3 |
$ ./ビン/カウチベース-ジャワ-importer myConfiguration.プロパティ |
を見てみよう。 README file for more information.
結論
Today we have learn how to move SQL content to Couchbase, but there is still some work to do. Next time I will tell you how to move the SQL business logic to the application layer.