Рубрики
Без рубрики

Создание REST API с помощью CRANK, Часть 2

В части 1 мы создали базовое приложение REST с Spring Framework и CRANK. У нас была базовая модель ho… С тегами java, spring, микросервисы, crank.

В части 1 мы создали базовое приложение REST с Spring Framework и CRANK. У нас была базовая модель того, как наши данные репрезентативны в базе данных, которую внешний пользователь может ПОЛУЧИТЬ, ОПУБЛИКОВАТЬ или ПОМЕСТИТЬ.

Данные внешней облицовки

В предыдущей статье мы работали только с одной моделью, зная, что каждая информация, запрашиваемая извне, должна предоставляться в полном объеме. Но если вы создаете API, в котором модель может содержать информацию, которую внешний пользователь не должен видеть, скажем, вы используете некоторые данные для создания фильтров, есть способ скрыть это. Создаются две модели: одна для внешнего использования и одна для хранилища. Затем будет создан картограф, который свяжет внешнюю модель с внутренней моделью.

Это делается с помощью реализации конфигуратора модуля Jpa и переопределения configure. Параметр – это конфигурация модуля Jpa, которая позволяет настроить приложение CRNK. Здесь, ниже, мы добавляем репозиторий, в котором мы связываем две модели, внешнюю и внутреннюю, с помощью картографа.

package com.test.store.configuration;

import com.test.store.model.ModelMapper;
import com.test.store.model.ModelExternal;
import com.test.store.model.ModelEntity;
import io.crnk.jpa.JpaModuleConfig;
import io.crnk.jpa.JpaRepositoryConfig;
import io.crnk.spring.setup.boot.jpa.JpaModuleConfigurer;
import org.springframework.stereotype.Component;

@Component
public class JpaModuleConfigurator implements JpaModuleConfigurer {

  @Override
  public void configure(JpaModuleConfig jpaModuleConfig) {
       jpaModuleConfig.addRepository(JpaRepositoryConfig.builder(
                                     ModelEntity.class,
                                     ModelExternal.class, new ModelMapper())
                      .build());
    }
}

И для того, чтобы конфигуратор модуля Jpa использовал mapper, ему необходимо реализовать JpaMapper, где вы указываете две модели и переопределяете метод для перехода между ними.

package com.test.store.model;

import com.test.store.model.ModelExternal;
import com.test.store.model.ModelEntity;
import io.crnk.jpa.mapping.JpaMapper;
import io.crnk.jpa.query.Tuple;
import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Component
public class ModelMapper implements JpaMapper {

  @PersistenceContext
  private EntityManager entityManager;@Override
  public ModelExternal map(Tuple tuple) {
    ModelExternal dto = new ModelExternal();

    ModelEntity entity = tuple.get(0, ModelEntity.class);
    dto.setData(entity.getData());
    return dto;
  }

  @Override
  public ModelEntity unmap(ModelExternal dto) {
    ModelEntity entity;
    if (dto.getId() == null) {
      entity = new ModelEntity();
    } else {
      entity = entityManager.find(ModelEntity.class, dto.getId());
      if (entity == null) {
        throw new RuntimeException("Couldn't find entity");
      }
    }
    return translateToEntity(dto, entity);
  }

  private ModelEntity translateToEntity(ModelExternal dto, ModelEntity entity) {
    entity.setData(dto.getData);
    return entity;
  }
}

Внешний пользователь по-прежнему может использовать поисковый запрос CRNK по умолчанию для модели, но вы можете управлять тем, что будет видно. Если у вас есть данные, которые вы хотите скрыть, ими можно управлять в mapper, а во внешней модели у вас этих данных нет.

Изменение данных хранилища

Вышесказанное дает возможность скрыть данные от внешнего пользователя, которые могут присутствовать в базе данных. Вопрос и проблема могут возникнуть, если пользователю будет представлена информация, которой нет в базе данных. Что делать, если на основе запроса к API мы захотим изменить ответ обратно?

Для этого мы не можем соединить две модели с помощью конфигуратора модуля Jpa. Обе модели должны иметь свои собственные репозитории, где внутренняя сторона – это та, которая подключается к базе данных. Внешний репозиторий будет содержать внутренний репозиторий в качестве атрибута, который будет использоваться в различных вызовах.

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

частная разница в возрасте;

В предыдущей статье мы хранили данные в памяти. На этот раз мы собираемся создать репозиторий, подключенный к базе данных, в которой сохраняются данные. Как и в предыдущем случае, мы указываем, какую модель использует репозиторий и какой атрибут является идентификатором.

package com.test.store.repository;

import com.test.store.model.ModelEntity;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Service;import java.util.List;
import java.util.UUID;

@Service
public interface ModelEntityCrudRepository extends CrudRepository {
}

Этому репозиторию не нужно много места по сравнению с базой репозиториев ресурсов, поскольку CrudRepository является связующим звеном с базой данных, и мы не заинтересованы в том, чтобы что-то здесь менять. Если вы хотите выполнить определенные вызовы внутри вашего API к базе данных, вы всегда можете создать определенные запросы к базе данных, например

@Query("select s from model s where partner_id = :partnerId")  
List findByPartnerId(@Param("partnerId") String partnerId);

Внешний репозиторий будет немного сложнее настроить. Нам нужно определить его как хранилище CRANK, но также связать его с вашим ModelEntity выше. Сначала мы создаем интерфейс, расширяющий Resource Repository V2, чтобы использовать запросы CRNK по умолчанию.

package com.test.store.repository;

import com.test.store.model.store.Store;
import io.crnk.core.repository.ResourceRepositoryV2;
import java.util.UUID;

public interface ModelExternalRepository extends 
                             ResourceRepositoryV2 {}

Далее следует основной репозиторий.

package com.test.store.repository;

import com.test.store.model.ModelExternal;
import com.test.store.model.ModelEntity;
import com.test.store.translator.ModelTranslator;
import io.crnk.core.boot.CrnkBoot;
import io.crnk.core.exception.BadRequestException;
import io.crnk.core.exception.RepositoryNotFoundException;
import io.crnk.core.queryspec.QuerySpec;
import io.crnk.core.repository.ResourceRepositoryBase;
import io.crnk.jpa.JpaEntityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

@Component
public class ModelExternalRepositoryImpl extends 
                               ResourceRepositoryBase implements ModelExternalRepository {

  private JpaEntityRepository modelEntityJpaRepository;@Autowired
  ApplicationContext appContext;

  public ModelExternalRepositoryImpl() {
    super(ModelExternal.class);
    modelEntityJpaRepository = null;
  }

  @Override
  public ModelExternal findOne(UUID id, QuerySpec querySpec) {
    if (modelEntityJpaRepository == null) {
      initJpaRepositoryEntity();
    }
    return ModelTranslator.map((ModelEntity)  modelEntityJpaRepository.findOne(id, querySpec));
  }

  @Override
  public ModelExternalResourceList findAll(QuerySpec querySpec) {
    if (modelEntityJpaRepository == null) {
      initJpaRepositoryEntity();
    }
    ResourceList modelEntityResourceList = modelEntityJpaRepository.findAll(querySpec);
    ...

modelResourceList.addAll(ModelTranslator.translate(modelEntityResourceList));
    ...

    return modelResourceList;
  }

  /**
   * Initialize the JpaRepository.
  */
  private void initJpaRepositoryEntity() {
    CrnkBoot crnkBoot = appContext.getBean(CrnkBoot.class);
    modelEntityJpaRepository = crnkBoot.getModuleRegistry()
                                           .getRepositories()
                                           .stream()
                                           .filter(repository -> repository instanceof JpaEntityRepository)
                                           .map(repository -> (JpaEntityRepository) repository)
                                           .filter(repository -> repository.getEntityClass().equals(ModelEntity.class)
                                                           && repository.getResourceClass().equals(ModelEntity.class))
                                           .findFirst()
                                           .orElseThrow(() -> new RepositoryNotFoundException("ModelEntity JpaRepository not found"));
    }

  public static class ModelResourceList extends ResourceListBase {}
}

Мы разделим это по важности.

Одним из наиболее важных моментов здесь является initJpaRepositoryEntity. Это связано с тем, что им нельзя управлять при инициализации репозиториев, и при автоматическом подключении внутренний репозиторий может выдать ошибку, сообщающую, что его еще нет. Чтобы обойти это с помощью взлома, первым обращением к данным будут два хранилища. Что происходит, так это то, что он будет искать наше хранилище объектов модели, подключенное к базе данных, и загружать в это хранилище.

Разбивка по шагам:

  1. Сначала мы ищем наш компонент CrnkBoot, который будет содержать все наши репозитории.
  2. Загрузите все модули зарегистрируйтесь с помощью Crank Boot и получите все репозитории
  3. Начните с просмотра их и найдите каждый репозиторий, который является JpaRepository
  4. Для каждого JpaRepository найдите тот, который связан с нашим классом объектов модели

После всех этих шагов мы загрузили наш репозиторий базы данных в наш внешний репозиторий.

Внешний вызов попадет в наш внешний репозиторий, который подключен к нашему репозиторию базы данных. Он получит информацию из него с помощью запроса (и при необходимости вы всегда можете изменить запрос, создав новый и отправив его во внутренний репозиторий). Мы извлекаем из него некоторые данные и преобразуем их во внешние данные. С помощью перевода мы можем добавить данные, которые мы хотим, чтобы внешний пользователь видел.

Выше приведены всего два способа разделения данных от внешних пользователей и внутренних пользователей. И мы по-прежнему сохраняем поведение CRNK по умолчанию с фильтрацией и сортировкой. Последний совет при использовании двух репозиториев и сортировки заключается в том, чтобы изменить запрос к репозиторию, а затем получить больше данных, чем вам нужно. Отсортируйте эти данные во внешнем хранилище и верните только набор данных.

Добавить новую фильтрацию

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

Чтобы добавить фильтр, вам необходимо расширить оператор фильтра

package com.test.store.filter;

import io.crnk.core.queryspec.FilterOperator;

public class PersonFilterOperator extends FilterOperator {

  public PersonFilterOperator(String id) {
    super(id);
  }

  /**
   * Handle new filterOperator. Should in default pass if value2 (value given by request) is given.
   * @param value1 value from DB
   * @param value2 value from request
   * @return true if value from request isn't null
  */
  @Override
  public boolean matches(Object value1, Object value2) {
    /*
     This class and this method is only used for new FilterOperators. FilterOperators are used in making a request and using crnk filtering.
     Value 1 is the data from the DB and Value 2 is from the request. FilterOperator.LIKE may first check if value2 exist then later on
     check value1 == value2. As we don't want to check values on that level we are only interested to check if the user provided a value.
     */
    return value2 != null;
  }
}

Приведенный выше фильтр не делает ничего, кроме проверки значения, которое передается вместе с данными запроса. Но вы можете изменить соответствие методов в соответствии с тем, что вы хотите, чтобы ваша фильтрация выполняла. Теперь простое добавление этого класса не добавит фильтрацию в ваше приложение, но вам нужно добавить его в средство сопоставления URL-адресов.

package com.test.store.configuration;

import com.test.store.filter.StoreFilterOperator;
import io.crnk.core.queryspec.FilterOperator;
import io.crnk.core.queryspec.mapper.DefaultQuerySpecUrlMapper;
import org.springframework.stereotype.Component;

@Component
public class PersonQuerySpec extends DefaultQuerySpecUrlMapper {

  public static final FilterOperator WITHIN= new PersonFilterOperator("WITHIN");
  public static final FilterOperator NEAR = new PersonFilterOperator("NEAR");
  public ModelQuerySpec() {
    super.addSupportedOperator(WITHN);
    super.addSupportedOperator(NEAR);
  }
}

Мы добавляем новый оператор фильтра в сопоставитель URL-адресов спецификации запроса по умолчанию, поэтому, когда поступает вызов с новым фильтром, CRNK знает, что с ним делать. В приведенном выше примере используется тот же класс, что и выше, который теперь не выполняет ничего, кроме вызова фильтра с

/person?filter[NEAR][age]=25

работает. Что важно при использовании вашего собственного оператора фильтра, так это то, как упоминалось в части 1, что фильтр работает как между запросом к вашему API, так и между запросом из репозитория к базовой структуре данных. Это означает, что при использовании определенного оператора фильтра вам придется реализовать класс репозитория для распаковки вашего querySpec и изменения запроса.

Резюме

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

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

Оригинал: “https://dev.to/nchrisz/creating-rest-api-with-crnk-part-2-4m1h”