Статьи

Пользовательские типы данных Cassandra

В своем блоге « Подключение к Cassandra из Java» я упомянул, что одним из преимуществ реализации Java-разработчиков Cassandra на Java является возможность создания пользовательских типов данных Cassandra . В этом посте я расскажу, как это сделать более подробно.

Cassandra имеет множество встроенных типов данных , но есть ситуации, в которых можно добавить пользовательский тип. Пользовательские типы данных Cassandra реализованы в Java путем расширения класса org.apache.cassandra.db.marshal.AbstractType . Класс, который расширяет это, должен в конечном итоге реализовать три метода со следующими сигнатурами:

1
public ByteBuffer fromString(final String) throws MarshalException
1
public TypeSerializer getSerializer()
1
public int compare(Object, Object)

Пример реализации AbstractType в этом посте показан в следующем листинге кода.

UnitedStatesState.java — расширяет абстрактный тип

01
02
03
04
05
06
07
08
09
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
package dustin.examples.cassandra.cqltypes;
 
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.serializers.MarshalException;
import org.apache.cassandra.serializers.TypeSerializer;
 
import java.nio.ByteBuffer;
 
/**
 * Representation of a state in the United States that
 * can be persisted to Cassandra database.
 */
public class UnitedStatesState extends AbstractType
{
   public static final UnitedStatesState instance = new UnitedStatesState();
 
   @Override
   public ByteBuffer fromString(final String stateName) throws MarshalException
   {
      return getStateAbbreviationAsByteBuffer(stateName);
   }
 
   @Override
   public TypeSerializer getSerializer()
   {
      return UnitedStatesStateSerializer.instance;
   }
 
   @Override
   public int compare(Object o1, Object o2)
   {
      if (o1 == null && o2 == null)
      {
         return 0;
      }
      else if (o1 == null)
      {
         return 1;
      }
      else if (o2 == null)
      {
         return -1;
      }
      else
      {
         return o1.toString().compareTo(o2.toString());
      }
   }
 
   /**
    * Provide standard two-letter abbreviation for United States
    * state whose state name is provided.
    *
    * @param stateName Name of state whose abbreviation is desired.
    * @return State's abbreviation as a ByteBuffer; will return "UK"
    *    if provided state name is unexpected value.
    */
   private ByteBuffer getStateAbbreviationAsByteBuffer(final String stateName)
   {
      final String upperCaseStateName = stateName != null ? stateName.toUpperCase().replace(" ", "_") : "UNKNOWN";
      String abbreviation;
      try
      {
         abbreviation =  upperCaseStateName.length() == 2
                       ? State.fromAbbreviation(upperCaseStateName).getStateAbbreviation()
                       : State.valueOf(upperCaseStateName).getStateAbbreviation();
      }
      catch (Exception exception)
      {
         abbreviation = State.UNKNOWN.getStateAbbreviation();
      }
      return ByteBuffer.wrap(abbreviation.getBytes());
   }
}

Приведенный выше список классов ссылается на перечисление State , которое показано далее.

State.java

01
02
03
04
05
06
07
08
09
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package dustin.examples.cassandra.cqltypes;
 
/**
 * Representation of state in the United States.
 */
public enum State
{
   ALABAMA("Alabama", "AL"),
   ALASKA("Alaska", "AK"),
   ARIZONA("Arizona", "AZ"),
   ARKANSAS("Arkansas", "AR"),
   CALIFORNIA("California", "CA"),
   COLORADO("Colorado", "CO"),
   CONNECTICUT("Connecticut", "CT"),
   DELAWARE("Delaware", "DE"),
   DISTRICT_OF_COLUMBIA("District of Columbia", "DC"),
   FLORIDA("Florida", "FL"),
   GEORGIA("Georgia", "GA"),
   HAWAII("Hawaii", "HI"),
   IDAHO("Idaho", "ID"),
   ILLINOIS("Illinois", "IL"),
   INDIANA("Indiana", "IN"),
   IOWA("Iowa", "IA"),
   KANSAS("Kansas", "KS"),
   LOUISIANA("Louisiana", "LA"),
   MAINE("Maine", "ME"),
   MARYLAND("Maryland", "MD"),
   MASSACHUSETTS("Massachusetts", "MA"),
   MICHIGAN("Michigan", "MI"),
   MINNESOTA("Minnesota", "MN"),
   MISSISSIPPI("Mississippi", "MS"),
   MISSOURI("Missouri", "MO"),
   MONTANA("Montana", "MT"),
   NEBRASKA("Nebraska", "NE"),
   NEVADA("Nevada", "NV"),
   NEW_HAMPSHIRE("New Hampshire", "NH"),
   NEW_JERSEY("New Jersey", "NJ"),
   NEW_MEXICO("New Mexico", "NM"),
   NORTH_CAROLINA("North Carolina", "NC"),
   NORTH_DAKOTA("North Dakota", "ND"),
   NEW_YORK("New York", "NY"),
   OHIO("Ohio", "OH"),
   OKLAHOMA("Oklahoma", "OK"),
   OREGON("Oregon", "OR"),
   PENNSYLVANIA("Pennsylvania", "PA"),
   RHODE_ISLAND("Rhode Island", "RI"),
   SOUTH_CAROLINA("South Carolina", "SC"),
   SOUTH_DAKOTA("South Dakota", "SD"),
   TENNESSEE("Tennessee", "TN"),
   TEXAS("Texas", "TX"),
   UTAH("Utah", "UT"),
   VERMONT("Vermont", "VT"),
   VIRGINIA("Virginia", "VA"),
   WASHINGTON("Washington", "WA"),
   WEST_VIRGINIA("West Virginia", "WV"),
   WISCONSIN("Wisconsin", "WI"),
   WYOMING("Wyoming", "WY"),
   UNKNOWN("Unknown", "UK");
 
   private String stateName;
 
   private String stateAbbreviation;
 
   State(final String newStateName, final String newStateAbbreviation)
   {
      this.stateName = newStateName;
      this.stateAbbreviation = newStateAbbreviation;
   }
 
   public String getStateName()
   {
      return this.stateName;
   }
 
   public String getStateAbbreviation()
   {
      return this.stateAbbreviation;
   }
 
   public static State fromAbbreviation(final String candidateAbbreviation)
   {
      State match = UNKNOWN;
      if (candidateAbbreviation != null && candidateAbbreviation.length() == 2)
      {
         final String upperAbbreviation = candidateAbbreviation.toUpperCase();
         for (final State state : State.values())
         {
            if (state.stateAbbreviation.equals(upperAbbreviation))
            {
               match = state;
            }
         }
      }
      return match;
   }
}

Мы также можем предоставить реализацию интерфейса TypeSerializer возвращаемого методом getSerializer() показанным выше. Этот класс, реализующий TypeSerializer , обычно легче всего написать, расширив одну из многочисленных существующих реализаций TypeSerializer которые Cassandra предоставляет в org.apache.cassandra.serializers package . В моем примере мой пользовательский Serializer расширяет AbstractTextSerializer и единственный метод, который мне нужно добавить, имеет public void validate(final ByteBuffer bytes) throws MarshalException . Оба моих пользовательских класса должны предоставлять ссылку на свой экземпляр через статический доступ. Вот класс, который реализует TypeSerializer через расширение AbstractTypeSerializer :

UnitedStatesStateSerializer.java — Реализует TypeSerializer

01
02
03
04
05
06
07
08
09
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
package dustin.examples.cassandra.cqltypes;
 
import org.apache.cassandra.serializers.AbstractTextSerializer;
import org.apache.cassandra.serializers.MarshalException;
 
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
 
/**
 * Serializer for UnitedStatesState.
 */
public class UnitedStatesStateSerializer extends AbstractTextSerializer
{
   public static final UnitedStatesStateSerializer instance = new UnitedStatesStateSerializer();
 
   private UnitedStatesStateSerializer()
   {
      super(StandardCharsets.UTF_8);
   }
 
   /**
    * Validates provided ByteBuffer contents to ensure they can
    * be modeled in the UnitedStatesState Cassandra/CQL data type.
    * This allows for a full state name to be specified or for its
    * two-digit abbreviation to be specified and either is considered
    * valid.
    *
    * @param bytes ByteBuffer whose contents are to be validated.
    * @throws MarshalException Thrown if provided data is invalid.
    */
   @Override
   public void validate(final ByteBuffer bytes) throws MarshalException
   {
      try
      {
         final String stringFormat = new String(bytes.array()).toUpperCase();
         final State state =  stringFormat.length() == 2
                            ? State.fromAbbreviation(stringFormat)
                            : State.valueOf(stringFormat);
      }
      catch (Exception exception)
      {
         throw new MarshalException("Invalid model cannot be marshaled as UnitedStatesState.");
      }
   }
}

После написания классов для создания пользовательского типа данных CQL их необходимо скомпилировать в .class и заархивировать в файл JAR. Этот процесс (компиляция с помощью javac -cp "C:\Program Files\DataStax Community\apache-cassandra\lib\*" -sourcepath src -d classes src\dustin\examples\cassandra\cqltypes\*.java и архивирование сгенерированного .class Файлы .class в JAR с именем CustomCqlTypes.jar с jar cvf CustomCqlTypes.jar * ) показаны на следующем снимке экрана.

compilingCustomTypesClasses

JAR с определениями классов пользовательских типов CQL необходимо поместить в каталог lib установки Cassandra, как показано на следующем снимке экрана.

movingCqlCustomTypesJarToCassandraLibDir

С JAR, содержащим пользовательские реализации классов типов данных CQL в каталоге lib установки Cassandra, Cassandra должен быть перезапущен, чтобы он мог «видеть» эти определения пользовательских типов данных.

В следующем листинге кода показан оператор языка Cassandra Query Language (CQL) для создания таблицы с использованием нового пользовательского типа dustin.examples.cassandra.cqltypes.UnitedStatesState .

createAddress.cql

01
02
03
04
05
06
07
08
09
10
CREATE TABLE us_address
(
   id uuid,
   street1 text,
   street2 text,
   city text,
   state 'dustin.examples.cassandra.cqltypes.UnitedStatesState',
   zipcode text,
   PRIMARY KEY(id)
);

Следующий снимок экрана демонстрирует результаты выполнения createAddress.cql выше кода createAddress.cql путем описания созданной таблицы в cqlsh .

descUSAddressWithCustomType

Приведенный выше снимок экрана демонстрирует, что пользовательский тип dustin.examples.cassandra.cqltypes.UnitedStatesState является типом для столбца состояния таблицы us_address .

Новая строка может быть добавлена ​​в таблицу US_ADDRESS с помощью обычной INSERT . Например, следующий снимок экрана демонстрирует вставку адреса с помощью команды INSERT INTO us_address (id, street1, street2, city, state, zipcode) VALUES (blobAsUuid(timeuuidAsBlob(now())), '350 Fifth Avenue', '', 'New York', 'New York', '10118'); :

insertingAddressWithCustomStateTypeIntoCassandraDB

Обратите внимание, что, хотя оператор INSERT вставил для штата «Нью-Йорк», он хранится как «NY».

selectingStateFromCassandraCustomType

Если я запускаю INSERT в cqlsh, используя для начала аббревиатуру ( INSERT INTO us_address (id, street1, street2, city, state, zipcode) VALUES (blobAsUuid(timeuuidAsBlob(now())), '350 Fifth Avenue', '', 'New York', 'NY', '10118'); ), он все еще работает, как показано в выводе, показанном ниже.

insertingAddressWithCustomStateTypeAbbreviationIntoCassandraDB

В моем примере недопустимое состояние не предотвращает возникновение INSERT , но вместо этого сохраняет состояние как «UK» (для неизвестного) [см. Реализацию этого в UnitedStatesState.getStateAbbreviationAsByteBuffer(String) ].

Одним из первых преимуществ, который приходит на ум, оправдывая, почему можно реализовать пользовательский тип данных CQL в Java, является возможность использовать поведение, подобное тому, которое обеспечивается проверочными ограничениями в реляционных базах данных. Например, в этом посте мой пример гарантировал, что любой столбец штатов, введенный для новой строки, будет одним из пятидесяти штатов США, округа Колумбия или «Великобритания» для неизвестных. Никакие другие значения не могут быть вставлены в значение этого столбца.

Еще одним преимуществом пользовательского типа данных является возможность втирать данные в предпочтительную форму. В этом примере я изменил каждое название штата на заглавную двузначную аббревиатуру. В других случаях я могу захотеть всегда хранить в верхнем регистре или всегда в нижнем регистре или отображать конечные наборы строк в числовые значения. Пользовательский тип данных CQL позволяет настраивать проверку и представление значений в базе данных Cassandra.

Вывод

Этот пост был ознакомительным обзором реализации пользовательских типов данных CQL в Cassandra. Поскольку я больше играю с этой концепцией и пробую разные вещи, я надеюсь написать еще один пост в блоге о некоторых более тонких наблюдениях, которые я делаю. Как показано в этом посте, довольно легко написать и использовать пользовательский тип данных CQL, особенно для разработчиков Java.

Ссылка: Пользовательские типы данных Cassandra от нашего партнера JCG Дастина Маркса в блоге Inspired by Actual Events .