В части 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") ListfindByPartnerId(@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 ResourceRepositoryBaseimplements 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. Это связано с тем, что им нельзя управлять при инициализации репозиториев, и при автоматическом подключении внутренний репозиторий может выдать ошибку, сообщающую, что его еще нет. Чтобы обойти это с помощью взлома, первым обращением к данным будут два хранилища. Что происходит, так это то, что он будет искать наше хранилище объектов модели, подключенное к базе данных, и загружать в это хранилище.
Разбивка по шагам:
- Сначала мы ищем наш компонент CrnkBoot, который будет содержать все наши репозитории.
- Загрузите все модули зарегистрируйтесь с помощью Crank Boot и получите все репозитории
- Начните с просмотра их и найдите каждый репозиторий, который является JpaRepository
- Для каждого 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”