N1QL의 GA 릴리스 이후 SQL 데이터베이스에서 Couchbase로 콘텐츠를 옮기는 방법에 대한 질문을 많이 받습니다. 그렇게 하는 방법에는 여러 가지가 있습니다. 오늘은 가장 간단한 방법을 선택했습니다. 각 테이블의 각 행을 JsonDocument로 변환하여 Couchbase에 저장하겠습니다. 저는 Postgres와 그 샘플 데이터 세트 MySQL에서 영감을 얻은 사킬라 샘플. Java를 사용하겠지만 여기에 제시된 가이드라인은 다른 언어에도 적용할 수 있습니다.
실행 중인 SQL 데이터베이스에 연결하기
저는 Java를 사용하고 있으므로 Spring Boot와 데이터베이스 연결을 처리하는 JDBC 패키지를 구현할 것입니다. 제가 해야 할 일은 올바른 종속성과 속성을 설정하여 JdbcTemplate. 이 개체를 사용하면 SQL 쿼리를 쉽게 실행할 수 있습니다.
종속성
모든 것을 깔끔하게 자동으로 구성하려면 다음과 같은 종속성이 필요합니다:
|
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" } |
저는 Postgres로 테스트하고 있지만 Spring JDBC에서 지원하는 다른 드라이버를 추가할 수 있습니다. 스프링 부팅 스타터 데이터-jpa를 사용하면 미리 구성된 JdbcTemplate을 주입할 수 있습니다.
구성
Spring 프레임워크가 데이터베이스를 찾도록 하려면 구성 파일에 다음 속성을 추가하세요(예: 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 |
물론 사용 중인 데이터베이스에 따라 이를 미세 조정해야 합니다. 여기서는 기본 포트 5432로 192.168.99.100에서 실행되는 Postgres를 사용하고 있습니다. 사용하려는 데이터베이스의 이름은 dvdrental입니다.
코드
모든 것이 올바르게 구성되었다면 JdbcTemplate을 삽입하고 SQL DB 쿼리를 시작할 수 있어야 합니다.
|
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); } |
Couchbase에 연결
제 목표는 SQL 데이터베이스에서 Couchbase로 콘텐츠를 이동하는 것이므로 Couchbase 연결도 필요합니다.
종속성
Java 프로젝트에서 Couchbase를 사용하려면 다음 종속성을 추가해야 합니다:
|
1 2 3 4 5 |
dependencies { compile "com.couchbase.client:java-client:2.2.3" } |
이렇게 하면 카우치베이스에 액세스할 수 있습니다. Java SDK.
구성
기본 Couchbase 구성에는 기본적으로 서버 IP 주소, 버킷 이름, 버킷 비밀번호의 세 가지 속성이 필요합니다. Spring Boot 방식으로 이 작업을 수행하면 다음과 같습니다:
|
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); } } |
속성 호스트 이름, 버킷 및 비밀번호는 애플리케이션 속성 파일에 직접 추가할 수 있습니다.
|
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: |
코드
카우치베이스에서 데이터베이스와 동등한 수준의 세분화 수준은 문서를 저장하는 버킷이 될 것입니다. 이전 구성을 사용하면 간단히 버킷을 주입하고 놀기 시작할 수 있습니다.
|
1 2 3 4 5 6 7 8 9 |
@Autowired Bucket bucket; @Override public void doStuff() throws Exception { JsonDocument doc = bucket.get("key"); } |
테이블
이 시점에서 SQL 데이터베이스와 Couchbase에 연결이 완료되었습니다. 이제 데이터를 이동하기 시작할 수 있습니다. 데이터를 이동하는 가장 쉬운 방법은 각 테이블의 각 행을 하나의 문서로 간주하는 것입니다.
SQL 스키마 가져오기
JdbcTemplate을 사용하여 데이터베이스의 스키마를 자동으로 가져오는 것부터 시작해 보겠습니다. 여기서 흥미로운 객체는 데이터베이스 메타데이터를 사용하면 데이터베이스의 전체 구조를 파악할 수 있습니다. 이 API는 사용하기 가장 쉬운 것은 아니지만 적어도 문서화되어 있습니다.
데이터베이스 메타데이터 쿼리 결과를 테이블 및 열 목록에 매핑하겠습니다. 이를 위해 다음과 같은 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; } } |
작성하기 가장 흥미로운 코드는 아니지만, 마지막에는 SQL 데이터베이스 테이블의 JSON 표현을 얻을 수 있습니다.
|
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); } |
콘텐츠
재미있는 부분은 여기입니다. 여기서부터 테이블 행을 JsonDocument에 매핑하기 시작합니다. 이전 섹션에서는 모든 테이블의 이름을 검색할 수 있는 상태가 되었습니다. 하나의 테이블 이름에서 테이블의 모든 행을 반환하는 SQL 쿼리를 만들 수 있습니다.
Spring에는 RowMapper를 정의할 수 있는 메커니즘이 있습니다. 쿼리에서 반환되는 각 행에 대해 원하는 객체를 반환할 수 있습니다. 저는 카우치베이스를 사용하고 있기 때문에, 저는 JsonDocument.
다음은 구현 예시입니다. 이 RowMapper에는 이전에 정의된 Table 객체가 필요하므로 mapRow 메서드를 구현해야 합니다. 여기서 해야 할 일이 몇 가지 있습니다.
첫 번째 작업은 고유 키를 만드는 것입니다. 행은 테이블별로 범위가 지정되므로 일부 ID는 다른 테이블의 행에 대해 정확히 동일할 수 있습니다. 하지만 문서는 버킷별로 범위가 지정되므로 행 ID와 테이블 이름을 반영하는 고유한 문서 키를 만들어야 합니다. 문서의 출처를 추적하기 위해 테이블 이름에 _tableName 필드도 추가하겠습니다.
그 다음 흥미로운 단계는 유형 매핑입니다. JSON은 SQL 데이터베이스보다 더 적은 수의 유형을 지원하므로 여기서 몇 가지 변환 작업을 수행해야 합니다. 이것이 바로 getJsonTypedValue 메서드가 하는 일입니다. 이 메서드는 대부분의 JDBC 유형을 기본 JSON 유형(문자열, 숫자, 부울, 배열, 객체, null)으로 변환할 수 있는지 확인합니다. 마지막에는 Couchbase에 저장할 수 있는 JsonDocument가 생성됩니다.
|
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; } } } |
RowMapper를 사용하면 작업이 정말 쉬워집니다. 테이블의 이름을 반복하고 쿼리를 실행한 다음 결과를 Couchbase에 저장할 수 있습니다. 이 작업을 동기식으로 수행하면 다음과 같이 보일 것입니다:
|
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); |
하지만 저는 비동기 버전을 선호합니다:
|
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)); |
여기서는 Rx의 잠재력을 최대한 활용하지 않고 있습니다. 이 함수 는 Couchbase에 문서를 작성하고 시간 초과 및 오류 관리를 처리합니다.
편의를 위해 구현된 모든 단계를 패키지로 묶어 이전에 보여드렸습니다. 단일 프로젝트. 속성 파일이 올바르게 구성되었는지 확인하고 임포터를 실행하기만 하면 됩니다:
|
1 2 3 |
$ ./bin/couchbase-java-importer myConfiguration.properties |
다음 내용을 살펴보십시오. README 파일에서 자세한 내용을 확인하세요.
결론
오늘은 SQL 콘텐츠를 Couchbase로 이동하는 방법을 배웠지만 아직 해야 할 일이 남아있습니다. 다음 시간에는 SQL 비즈니스 로직을 애플리케이션 계층으로 이동하는 방법을 알려드리겠습니다.