NoSQL Данные без схемы и статически типизированный язык

Одним из ключевых преимуществ хранилищ данных NoSQL, таких как MongoDB, является то, что они являются схематичными. С динамически типизированными языками это кажется естественным. Вы можете получить некоторые произвольные входы JSON, выполнить бизнес-логику в известных полях и сохранить все это, не указав сначала объект.

Что делать, если ваш выбор языка ограничен статическим типом, например Java? Как я мог достичь такого же уровня гибкости?

Типичный поток данных, например:

  • Вход JSON
  • Сериализация для Java-объекта для выполнения бизнес-логики
  • Дезертициализация в BSON для сохранения в Mongo

где шаг сериализации к объекту необходим, поскольку вы хотите выполнять бизнес-логику с помощью POJO, а не строк JSON. Однако, прежде чем я смогу сериализовать ввод в объекты, я должен сначала определить его. Что делать, если вход содержит дополнительные поля undefined в объекте? Хотя они не могут использоваться в бизнес-логике, я все же могу захотеть их сохранить. У меня есть реализации, где поля undefined помещаются в карту, но я не уверен, что это лучший подход. Во-первых, поля undefined также могут быть сложными объектами.

Ответ 1

Данные схемы не обязательно означают бесструктурные данные; поля обычно известны заранее, и поверх них можно применить некоторый тип безопасного шаблона, чтобы избежать антипатового шаблона Magic Container. Но это не всегда так. Иногда ключи вводятся пользователем и не могут быть известны заранее.

Я несколько раз использовал шаблон ролевого объекта, чтобы обеспечить согласованность с динамической структурой. Я думаю, что это хорошо подходит для обоих случаев.

Шаблон объектов ролей определяет способ доступа к различным представлениям объекта. Канонический пример - Пользователь, который может принимать несколько ролей, таких как Клиент, Продавец и Продавец. Каждое из этих представлений имеет различные операции, которые он может выполнять, и их можно получить из любого другого представления. Общие поля обычно доступны на уровне интерфейса (особенно userId(), или в вашем случае toJson()).

Вот пример использования шаблона:

public void displayPage(User user) {
    display(user.getName());

    if (user.hasView(Customer.class))
       displayShoppingCart(user.getView(Customer.class);

    if (user.hasView(Seller.class))
       displayProducts(user.getView(Seller.class));
}

В случае данных с известной структурой вы можете иметь несколько представлений, которые приносят разные наборы ключей в единые единицы. Эти разные представления могут считывать данные json по построению.

В случае данных с динамической структурой авторитетный RawDataView может иметь в нем динамическую форму (т.е. Магический контейнер, например, HashMap<String, Object>), Это можно использовать для запроса динамических данных. В то же время защищенные от типа обертки могут создаваться лениво и могут делегировать RawDataView, чтобы помочь в удобочитаемости/ремонтопригодности программы:

 public class Customer implements User {
     private final RawDataView data;
     public CustomerView(UserView source) {
         this.data = source.getView(RawDataView.class);
     }

     // All User views must specify this
     @Override
     public long id() {
         return data.getId();
     }

     @Override
     public <T extends UserView> T getView(Class<T> view) {
         // construct or look up view
     }

     @Override
     public Json toJson() {
         return data.toJson();
     }


     //
     // Specific to Customer
     //
     public List<Item> shoppingCart() {
         List<Item> items = (List<Item>) data.getValue("items", List.class); 
     }

     // etc....
 }

У меня был успех с обоими этими подходами. Вот некоторые дополнительные указатели, которые я обнаружил на этом пути:

  • Как можно больше у вас есть структура статической структуры. Это упрощает работу. Мне пришлось нарушить это правило и использовать подход RawDataView при работе с устаревшей системой. Вы также можете сломать его с помощью динамически введенных пользовательских данных, как указано выше. В этом случае используйте соглашение для имен нединамических полей, таких как символ подчеркивания (_userId)
  • Имейте equals() и hashcode(), чтобы user.getView(A.class).equals(user.getView(B.class)) всегда был прав для одного и того же пользователя.
  • У вас есть класс UserCore, который выполняет весь тяжелый подъем общего кода, например создание представлений; выполнение общих операций (например, toJson()), возвращающих общие поля (например, userId()); и реализации equals() и hashcode(). Имейте все представления делегировать этот основной объект
  • Имейте AbstractUserView, который делегирует UserCore и реализует equals() и hashcode()
  • Используйте гетерогенный контейнер типа безопасного типа (например ClassToInstanceMap).
  • Разрешить существование запроса для запроса. Это можно сделать либо с помощью метода hasView(), либо с помощью getView return Optional<T>

Ответ 2

У вас всегда может быть класс, который предоставляет оба:

  • легкий доступ к атрибутам, о которых вы знаете, и к дополнительным резервным случаям в более старые форматы (например, он может возвращать "имя", если он существует, или более старый случай "name.first" + "name.last", если он не (или подобный сценарий))
  • легкий доступ к неизвестным элементам, имитирующим интерфейс карты.

Выполняете ли вы полную проверку или нет, разрешаете ли вы дополнительные атрибуты undefined или нет, зависит от того, чего вы хотите достичь. Но я думаю, что создание абстракции, которая позволяет вам любой способ доступа к данным, является лучшим решением.

Надеемся, что с течением времени вы попадете на сцену, где ваша схема довольно стабильна, и беспорядок непосредственно с атрибутами больше не нужен.

Ответ 3

Это не очень хорошо решено в Java из-за отсутствия динамических типов. Одним из способов решения этой проблемы является использование карт.

Map

Объект может снова быть Картой объектов.

Это не элегантный способ, но работает на Java. Пример: библиотека SnakeYaml для YAML позволяет обходить таким образом.