All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ebuddy.cassandra.cql.dao.CqlStructuredDataSupport Maven / Gradle / Ivy

There is a newer version: 2.4.2
Show newest version
/*
 *      Copyright (C) 2013 eBuddy B.V.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package com.ebuddy.cassandra.cql.dao;

import static com.datastax.driver.core.querybuilder.QueryBuilder.batch;
import static com.datastax.driver.core.querybuilder.QueryBuilder.bindMarker;
import static com.datastax.driver.core.querybuilder.QueryBuilder.delete;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.gte;
import static com.datastax.driver.core.querybuilder.QueryBuilder.insertInto;
import static com.datastax.driver.core.querybuilder.QueryBuilder.lte;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.Validate;

import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.Statement;
import com.datastax.driver.core.querybuilder.Batch;
import com.datastax.driver.core.querybuilder.Delete;
import com.ebuddy.cassandra.BatchContext;
import com.ebuddy.cassandra.Path;
import com.ebuddy.cassandra.StructuredDataSupport;
import com.ebuddy.cassandra.TypeReference;
import com.ebuddy.cassandra.databind.CustomTypeResolverBuilder;
import com.ebuddy.cassandra.structure.Composer;
import com.ebuddy.cassandra.structure.Decomposer;
import com.ebuddy.cassandra.structure.DefaultPath;
import com.ebuddy.cassandra.structure.JacksonTypeReference;
import com.ebuddy.cassandra.structure.StructureConverter;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Implementation of StructuredDataSupport for CQL.
 *
 * To use structured data in CQL3, the following data modeling rules apply:
 * 
    *
  • There must be a designated path column and it must be the first clustering key, i.e. the next element of the * primary key after the partition key.
  • *
  • There must be a designated value column.
  • *
  • There can only be one designated path and one designated value column per table.
  • *
  • The designated path and value columns must be typed as a textual type.
  • *
* * @author Eric Zoerner [email protected] */ public class CqlStructuredDataSupport implements StructuredDataSupport { private static final String DEFAULT_VALUE_COLUMN = "value"; private static final String DEFAULT_PATH_COLUMN = "column1"; private static final String DEFAULT_PARTITION_KEY_COLUMN = "key"; private static final int MAX_CODE_POINT = 0x10FFFF; private final Session session; private final String pathColumnName; private final String valueColumnName; private final ObjectMapper writeMapper; private final ObjectMapper readMapper; private final PreparedStatement readPathQuery; private final Statement insertStatement; private final PreparedStatement readForDeleteQuery; private final String tableName; private final String partitionKeyColumnName; /** * Used for tables that are upgraded from a thrift dynamic column family that still has the default column names. * @param session a Session configured with the keyspace */ public CqlStructuredDataSupport(String tableName, Session session) { this(tableName, DEFAULT_PARTITION_KEY_COLUMN, DEFAULT_PATH_COLUMN, DEFAULT_VALUE_COLUMN, session); } /** * Construct an instance of CqlStructuredDataSupport with the specified table and column names. * @param session a Session configured with the keyspace */ public CqlStructuredDataSupport(String tableName, String partitionKeyColumnName, String pathColumnName, String valueColumnName, Session session) { Validate.notEmpty(tableName); this.session = session; this.pathColumnName = pathColumnName; this.valueColumnName = valueColumnName; writeMapper = new ObjectMapper(); writeMapper.setDefaultTyping(new CustomTypeResolverBuilder()); readMapper = new ObjectMapper(); this.tableName = tableName; this.partitionKeyColumnName = partitionKeyColumnName; readPathQuery = session.prepare(select(pathColumnName, valueColumnName) .from(tableName) .where(eq(partitionKeyColumnName, bindMarker())) .and(gte(pathColumnName, bindMarker())) .and(lte(pathColumnName, bindMarker())) .getQueryString()); readForDeleteQuery = session.prepare(select(pathColumnName) .from(tableName) .where(eq(partitionKeyColumnName, bindMarker())) .and(gte(pathColumnName, bindMarker())) .and(lte(pathColumnName, bindMarker())) .getQueryString()); insertStatement = insertInto(tableName) .value(partitionKeyColumnName, bindMarker()) .value(pathColumnName, bindMarker()) .value(valueColumnName, bindMarker()); } @Override public BatchContext beginBatch() { return new CqlBatchContext(); } @Override public void applyBatch(BatchContext batchContext) { Batch batch = validateAndGetBatch(batchContext); List bindArguments = ((CqlBatchContext)batchContext).getBindArguments(); if (bindArguments.isEmpty()) { session.execute(batch.getQueryString()); } else { session.execute(session.prepare(batch.getQueryString()).bind(bindArguments.toArray())); } ((CqlBatchContext)batchContext).reset(); } @Override public T readFromPath(K rowKey, Path path, TypeReference type) { validateArgs(rowKey, path); String start = path.toString(); // use the maximum unicode code point to terminate the range String finish = getFinishString(start); // note: prepared statements should be cached and reused by the connection pooling component.... Object[] args = {rowKey,start,finish}; ResultSet resultSet = session.execute(readPathQuery.bind(args)); Map pathMap = getPathMap(path, resultSet); if (pathMap.isEmpty()) { // not found return null; } Object structure = Composer.get().compose(pathMap); // convert object structure into POJO of type referred to by TypeReference return readMapper.convertValue(structure, new JacksonTypeReference(type)); } @Override public void writeToPath(K rowKey, Path path, Object value) { writeToPath(rowKey, path, value, null); } @Override public void writeToPath(K rowKey, Path path, Object structuredValue, BatchContext batchContext) { Batch batch = validateAndGetBatch(batchContext); validateArgs(rowKey, path); Object simplifiedStructure = writeMapper.convertValue(structuredValue, Object.class); Map pathMap = Collections.singletonMap(path, simplifiedStructure); Map objectMap = Decomposer.get().decompose(pathMap); batch = batchContext == null ? batch() : batch; List bindArguments = batchContext == null ? new ArrayList() : ((CqlBatchContext)batchContext).getBindArguments(); for (Map.Entry entry : objectMap.entrySet()) { batch.add(insertStatement); String stringValue = StructureConverter.get().toString(entry.getValue()); bindArguments.add(rowKey); bindArguments.add(entry.getKey().toString()); bindArguments.add(stringValue); } if (batchContext == null) { session.execute(session.prepare(batch.getQueryString()).bind(bindArguments.toArray())); } } @Override public void deletePath(K rowKey, Path path) { deletePath(rowKey, path, null); } @Override public void deletePath(K rowKey, Path path, BatchContext batchContext) { Batch batch = validateAndGetBatch(batchContext); validateArgs(rowKey, path); // converting from a string and back normalizes the path, e.g. makes sure ends with the delimiter character String start = path.toString(); String finish = getFinishString(start); // would like to just do a delete with a where clause, but unfortunately Cassandra can't do that in CQL (either) // with >= and <= // Since the path column is in the primary key, we need to just delete whole rows. Object[] args = {rowKey,start,finish}; ResultSet resultSet = session.execute(readForDeleteQuery.bind(args)); if (resultSet.isExhausted()) { // not found return; } Delete deleteStatement = delete().from(tableName); deleteStatement .where(eq(partitionKeyColumnName, rowKey)) .and(eq(pathColumnName, bindMarker())); batch = batchContext == null ? batch() : batch; List bindArguments = batchContext == null ? new ArrayList() : ((CqlBatchContext)batchContext).getBindArguments(); for (Row row : resultSet) { String pathToDelete = row.getString(0); batch.add(deleteStatement); bindArguments.add(pathToDelete); } if (batchContext == null) { session.execute(session.prepare(batch.getQueryString()).bind(bindArguments.toArray())); } } @Override public Path createPath(String... elements) { return DefaultPath.fromStrings(elements); } private String getFinishString(String start) { int startCodePointCount = start.codePointCount(0, start.length()); int finishCodePointCount = startCodePointCount + 1; int[] finishCodePoints = new int[finishCodePointCount]; for (int i = 0; i < startCodePointCount; i++) { finishCodePoints[i] = start.codePointAt(i); } finishCodePoints[finishCodePointCount - 1] = MAX_CODE_POINT; return new String(finishCodePoints, 0, finishCodePointCount); } private void validateArgs(K rowKey, Path path) { Validate.isTrue(!path.isEmpty(), "Path must not be empty"); Validate.notNull(rowKey, "Row key must not be empty"); } private Batch validateAndGetBatch(BatchContext batchContext) { if (batchContext == null) { return null; } if (!(batchContext instanceof CqlBatchContext)) { throw new IllegalArgumentException("batchContext is not a CQL batch context"); } return ((CqlBatchContext)batchContext).getBatch(); } private Map getPathMap(Path inputPath, Iterable resultSet) { Map pathMap = new HashMap(); for (Row row : resultSet) { String valueString = row.getString(valueColumnName); Path path = DefaultPath.fromEncodedPathString(row.getString(pathColumnName)); if (!path.startsWith(inputPath)) { throw new IllegalStateException("unexpected path found in database:" + path); } path = path.tail(inputPath.size()); Object value = StructureConverter.get().fromString(valueString); // this can be a null converted from a JSON null pathMap.put(path, value); } return pathMap; } private static class CqlBatchContext implements BatchContext { private Batch batch = batch(); private final List bindArguments = new LinkedList(); private Batch getBatch() { return batch; } private List getBindArguments() { return bindArguments; } private void reset() { batch = batch(); bindArguments.clear(); } } }