How to configure integration test for the new Spring for GrapQL - spring-boot

I am currently learning Spring for GraphQL and am working on an application to do some basic CRUD operations, using the GraphQL API using Spring-boot with JPA and an H2 in-memory database for testing. Now I am trying to write some integration tests using the GraphlQlTester interface. There is a possibility to test importing just the service layer, storing the data in a collection at this layer. Now I want to be able to access the repository layer too to temporarily store and manipulate the test data in the h2 database. I have been researching the documentation online, but unfortunately, I have not found any possible way to approach this particular issue. Perhaps you may have an idea?
Entity Class
#Data
#Entity
#NoArgsConstructor
#AllArgsConstructor
#Builder
#Table(name = "address")
public class Address {
#Id
#GeneratedValue(strategy = IDENTITY)
private long id;
private String street;
private String city;
private int postCode;
}
Controller Class (just the first method)
#Controller
#RequiredArgsConstructor
public class AddressController {
private final AddressService addressService;
private static final String ERROR_MESSAGE = "Address with id %d not found";
#QueryMapping
List<Address> findAllAddresses() {
return addressService.findAll();
}
Service Class (just the first method)
#Service
#RequiredArgsConstructor
public class AddressService {
private final String ERROR_MESSAGE = "Address with id %d not found";
private final AddressRepository addressRepository;
private final ContactInformationRepository contactInformationRepository;
public List<Address> findAll() {
return addressRepository.findAll();
}
Repository
public interface AddressRepository extends JpaRepository<Address, Long> {
}
Integration Test class
import com.soscarlos.dropit.entity.Address;
import com.soscarlos.dropit.repository.AddressRepository;
import com.soscarlos.dropit.service.AddressService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest;
import org.springframework.context.annotation.Import;
import org.springframework.graphql.test.tester.GraphQlTester;
#GraphQlTest(AddressController.class)
#Import({AddressService.class, AddressRepository.class})
class AddressControllerTest {
#Autowired
GraphQlTester tester;
#Autowired
AddressService addressService;
#Autowired
AddressRepository addressRepository;
#Test
void findAllAddresses() {
// language=GraphQL
String document = """
query {
findAllAddresses {
id
street
city
postCode
}
}
""";
tester.document(document)
.execute()
.path("findAllAddresses")
.entityList(Address.class)
.hasSize(1);
}
I cannot load the application context because of this error basically
"Error creating bean with name 'com.soscarlos.dropit.repository.AddressRepository': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.soscarlos.dropit.repository.AddressRepository]: Specified class is an interface"

Related

How do I search for a nested #Indexed key in Redis redis-om-spring?

I am trying to search for a nested #Indexed field using redis-om-spring
For some reason a find returns expected 1 entry for level 1, but returns unexpected 0 entries for level 2.
I am not sure if I can only search one level deep or if I am making a mistake.
#SpringBootApplication
#Configuration
#EnableRedisDocumentRepositories
#Slf4j
public class Application {
#Autowired
ProductRepository productRepository;
#Bean
CommandLineRunner loadTestData() {
return args -> {
productRepository.deleteAll();
productRepository.save(new Product(new MyKey(new MyId("A_level2Key1"),"A_level1Key1"),"FirstColour"));
productRepository.save(new Product(new MyKey(new MyId("B_level2Key1"),"B_level1Key1"),"SecondColour"));
var byMyKeyLevel1Key = productRepository.findByMyKeyLevel1Key("A_level1Key1");
System.out.println(byMyKeyLevel1Key.size());//returns expected 1 entry for level 1
var byMyKeyMyIdLevel2Key = productRepository.findByMyKeyMyIdLevel2Key("A_level2Key1");
System.out.println(byMyKeyMyIdLevel2Key.size());//returns unexpected 0 entries for level 2
};
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Product
import com.redis.om.spring.annotations.Document;
import com.redis.om.spring.annotations.Indexed;
import com.redis.om.spring.annotations.Searchable;
#Slf4j
#Data
#AllArgsConstructor
#NoArgsConstructor
#Document
public class Product {
#Indexed
#Id
private MyKey myKey;
#Searchable
#NonNull
private String colourDesc;
}
ProductRepository
public interface ProductRepository extends RedisDocumentRepository<Product, String> {
List<Product> findByMyKeyLevel1Key(String level1Key);
List<Product> findByMyKeyMyIdLevel2Key(String level2Key);
}
MyKey
import com.redis.om.spring.annotations.Indexed;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
#Data
#AllArgsConstructor
#NoArgsConstructor
public class MyKey {
#Indexed
MyId myId;
#Indexed
String level1Key;
}
Data in Redis:
FT.INFO "com.redis.om.documents.domain.ProductIdx"
From the logs, it generates searches:
FT.SEARCH "com.redis.om.documents.domain.ProductIdx" #myKey_level1Key:{A_level1Key1}
FT.SEARCH "com.redis.om.documents.domain.ProductIdx" #myKey_myId_level2Key:{A_level2Key1}
I start Redis with:
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
The issue is likely in the naming of the repository methods. I wrote your example as a test in the project repo, this will be using release 0.7.0 - but it should work with 0.6.x-SNAPSHOTs:
The entity:
#Data #AllArgsConstructor #NoArgsConstructor #Document public class Product {
#Indexed #Id private MyKey myKey;
#Searchable #NonNull private String colourDesc;
}
The first-level key:
#Data #AllArgsConstructor #NoArgsConstructor public class MyKey {
#Indexed MyId myId;
#Indexed String level1Key;
#Override public String toString() {
return myId.toString();
}
}
The second-level key:
#Data #AllArgsConstructor #NoArgsConstructor public class MyId {
#Indexed
private String level2Key;
#Override public String toString() {
return level2Key;
}
}
The repo, here's where things probably went wrong for you...
public interface ProductRepository extends RedisDocumentRepository<Product, MyKey> {
List<Product> findByMyKey_Level1Key(String level1Key);
List<Product> findByMyKey_MyId_Level2Key(String level2Key);
}
and here's a snippet of the test class I used:
#Autowired ProductRepository productRepository;
#BeforeEach
void setup() {
productRepository.deleteAll();
productRepository.save(new Product(new MyKey(new MyId("A_level2Key1"),"A_level1Key1"),"FirstColour"));
productRepository.save(new Product(new MyKey(new MyId("B_level2Key1"),"B_level1Key1"),"SecondColour"));
}
#Test
void testFindNestedKeyValues() {
var byMyKeyLevel1Key = productRepository.findByMyKey_Level1Key("A_level1Key1");
assertThat(byMyKeyLevel1Key).map(Product::getMyKey).map(MyKey::getLevel1Key).containsExactly("A_level1Key1");
var byMyKeyMyIdLevel2Key = productRepository.findByMyKey_MyId_Level2Key("A_level2Key1");
assertThat(byMyKeyMyIdLevel2Key).map(Product::getMyKey).map(MyKey::getMyId).map(MyId::getLevel2Key).containsExactly("A_level2Key1");
}
Hope that helps! Thanks for trying Redis OM Spring!

Spring Boot JPA returns correct count but no data

Evening,
I have a Spring application that is connected to a PostgresSQL db. I can connect to the database and see that the query is returning the correct number of elements for the array but nothing in them:
curl http://localhost:8080/books
[{},{},{}]%
My Book model looks like this:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.math.BigDecimal;
#Entity
public class Book {
#Id
#GeneratedValue
private Long id;
private String name;
private String author;
private BigDecimal price;
public Book() {}
public Book(String name, String author, BigDecimal price) {
this.name = name;
this.author = author;
this.price = price;
}
}
and the controller:
#RestController
public class BookController {
#Autowired
private BookRepository repository;
// Find
#GetMapping("/books")
List<Book> findAll() {
List<Book> books = repository.findAll();
System.out.println(books);
return repository.findAll();
}
}
I've looked at these questions here, here and here but those answers didn't fit with this.
What am I not doing to see data come back?
In order for your entity to be serialized by Spring the entity needs to have getters for its properties. You could use lombok to auto-generate getter/setters for you entity properties or just write them your own.

Instructing Sping Data MongoDB to use correct mapping between ObjectId and its class

I cannot retrieve the 2nd level nested objects in Spring Data MongoDB
I have nested collection in MongoDB to retrieve with Spring. Imagine this schema
#Data
#Builder
#Document(collection = "emitted")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Emitter{
#Id
private String id;
#Field("installation")
#DocumentReference(lazy = true)
private Installaton installation;
// other fields
#Data
#Builder
#Document(collection = "installation")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Installation {
#Id
private String id;
#Field("subject")
#DocumentReference(lazy = true)
private Subject subject;
// other fields
#Data
#Builder
#Document(collection = "subject")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Subject {
#Id
private String id;
// other fields
Plus, I have MapStruct to map nested object field to string, for the purpose of avoiding cyclic reference introducing the search by id of the collection:
#ObjectFactory
public <T> T map(#NonNull final String id, #TargetType Class<T> type) {
return mongoTemplate.findById(id, type);
}
Everything works at first level, but at nested level I have this error:
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [org.bson.types.ObjectId] to type [com.package.collections.Subject]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:322)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:195)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:175)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.doConvert(MappingMongoConverter.java:1826)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.doConvert(MappingMongoConverter.java:1818)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.getPotentiallyConvertedSimpleRead(MappingMongoConverter.java:1337)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.getPotentiallyConvertedSimpleRead(MappingMongoConverter.java:1311)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$DefaultConversionContext.convert(MappingMongoConverter.java:2371)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$ConversionContext.convert(MappingMongoConverter.java:2174)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$MongoDbPropertyValueProvider.getPropertyValue(MappingMongoConverter.java:1936)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readProperties(MappingMongoConverter.java:638)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.populateProperties(MappingMongoConverter.java:549)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:527)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readDocument(MappingMongoConverter.java:491)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:427)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:423)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:120)
at org.springframework.data.mongodb.core.MongoTemplate$ReadDocumentCallback.doWith(MongoTemplate.java:3326)
at org.springframework.data.mongodb.core.MongoTemplate.executeFindOneInternal(MongoTemplate.java:2940)
at org.springframework.data.mongodb.core.MongoTemplate.doFindOne(MongoTemplate.java:2618)
at org.springframework.data.mongodb.core.MongoTemplate.doFindOne(MongoTemplate.java:2588)
at org.springframework.data.mongodb.core.MongoTemplate.findById(MongoTemplate.java:922)
at com.package.myapp.services.mapper.ReferenceMapper.map(ReferenceMapper.java:26)
at com.package.myapp.services.mapper.InstallationMapperImpl.toEntity(InstallationMapperImpl.java:102)
When asking the conversion, the findById works correctly and retrieve the object and the nested one. It fails when the request is for 2nd level nested object, where the ObjectId is retrieved but cannot be converted and fails.
I'm answering myself because I found a solution suited for my problem.
I only needed the entity object with the id, so I wrote a converter:
public class ObjectIdToSubjectConverter implements Converter<ObjectId, Subject> {
#Override
public Subject convert(ObjectId source) {
return Subject.builder().id(source.toString()).build();
}
}
And register it as a bean:
#Configuration
public class MongoConfig {
#Bean
public MongoCustomConversions mongoCustomConversions() {
return new MongoCustomConversions(Collections.singletonList(new ObjectIdToSubjectConverter());
}
}

Mapstruct - How to convert a DTO String parameter to an Entity object?

I'm new to Mapstruct and I'm trying to understand it properly.
What I want to achieve is converting from a DTO String parameter (carModel) to his Entity, retrieve using Service and Repository.
The problem is that Mapper class generated by Mapstruct is trying to inject the Service class with #Autowired annotation, but it's not working. The service is null.
Here's my #Mapper class:
#Mapper(componentModel = "spring", uses = CarModelService.class)
public interface KitMapper extends EntityMapper<KitDTO, Kit> {
KitMapper INSTANCE = Mappers.getMapper(KitMapper.class);
#Mapping(source = "weight", target = "systemWeight")
#Mapping(source = "carModel", target = "carModel")
Kit toEntity(KitDTO kitDTO);
}
public interface EntityMapper<D, E> {
E toEntity(D dto);
List<E> toEntity(List<D> dtoList);
}
The #Service class:
#Service
#Transactional
public class CarModelService {
private final CarModelRepository carModelRepository;
#Transactional(readOnly = true)
public CarModel findByName(String name) {
return carModelRepository.findByName(name).orElse(null);
}
}
The #Repository class:
#Repository
public interface CarModelRepository extends JpaRepository<CarModel, Long> {
Optional<CarModel> findByName(String carModelName);
}
The DTO and Entity classes:
public class KitDTO {
private String id;
private String carModel; // e.g. "Ferrari Monza"
....
}
#Entity
#Table(name = "kit")
public class Kit implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
#Column(name = "id")
private Long id;
#ManyToOne
private CarModel carModel;
...
}
#Entity
#Table(name = "car_model")
public class CarModel implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
...
}
The build work properly but the application stop when I try to use the Mapper. It says that carModelService is null.
Here's the mapper generated implementation class:
#Component
public class KitMapperImpl implements KitMapper {
#Autowired // <-- this seems not working
private CarModelService carModelService;
#Override
public Kit toEntity(KitDTO kitDTO) {
if ( kitDTO == null ) {
return null;
}
Kit kit = new Kit();
kit.setSystemWeight( String.valueOf( kitDTO.getWeight() ) );
kit.carModel( carModelService.findByName(kitDTO.getCarModel()) ); // <-- carModelService is null!
// other setters
return kit;
}
}
I've tried many things, using Decorator, #Context, expression, inject the #Mapper class into the #Service class.
I've found many questions but actually no one helped me:
Mapstruct - How can I inject a spring dependency in the Generated Mapper class
#Service Class Not Autowired in org.mapstruct.#Mapper Class
MapStruct mapper not initialized with autowired when debug
Any help would be appreciated! Thanks in advance!
Found the solution!
Instead of calling directly the Mapper method toEntity() from the #RestController class, I injected the mapper in the CarModelService class and created a method that call the mapper.
In this way the flow is:
Controller --> Service --> Mapper
#Service
#Transactional
public class KitService {
private final KitRepository kitRepository;
private final KitSearchRepository kitSearchRepository;
private final KitMapper kitMapper; // <-- mapper declaration
public KitService(KitRepository kitRepository, KitSearchRepository kitSearchRepository, KitMapper kitMapper) {
this.kitRepository = kitRepository;
this.kitSearchRepository = kitSearchRepository;
this.kitMapper = kitMapper; // <-- mapper initilization
}
// here the method which calls mapper
public Kit convertDTOToEntity(KitDTO kitDTO) {
return kitMapper.toEntity(kitDTO);
}
In this way, the generated class by Mapstruct doesn't give error on the CarModelService.
Seems like this approach is the only way to achieve this, create a king of "bridge" between services and mappers.
(You can use also the #Autowired annotation instead of constructor)
Can you please share the error message?
From the information that you shared, I can see the carModel in KitDto is String and in Entity is CarModel class. Not sure how mapstruct's auto generated implementation class implemented this: kit.carModel( carModelService.findByName(kitDTO.getCarModel()) );.
But I would like to share another approach,Don't know this is a best practice or not. In this approach you can create a abstarct class of mapper, in which you can #Autowired repository can manually add those mapping.
I shared the snippet for it. Hopefully this will help you.
#Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public abstract class ProductMapper {
#Autowired
private CarModelService carModelService;
public abstract Kit convertDTOToEntity(KitDTO kitDTO);
public Kit toEntity(KitDTO kitDTO);
{
Kit kit = convertDTOToEntity(kitDTO);
kit.setCarModel(carModelService.findByName(kitDTO.getCarModel()));
return kit;
}
}
Curious about the other approaches, will follow this thread. We can discuss the best practices

Load datatype dynamically using Springboot #Conditional

I would like to load the database type ( Cassandra or MongoDB) in my Springboot Service class based on some condition. For this, I was planning to use #Conditional annotation mentioned in https://sivalabs.in/2016/03/how-springboot-autoconfiguration-magic/.
In my case, I am extending the org.springframework.stereotype. Repository Interface to create the Repository like below.
In my service class, how can I get the instance of Cassandra Type or Mongo Type based on a profile or configuration in application.yml?
Thanks
Ashish
#Repository
public interface CartCassandraRepository extends CrudRepository<com.cassandra.Cart, String> {
}
#Repository
public interface MongoCassandraRepository extends CrudRepository<com.mongo.Cart, String> {
}
Here is my com.cassandra.Cart.java
import org.springframework.data.cassandra.core.cql.PrimaryKeyType;
import org.springframework.data.cassandra.core.mapping.CassandraType;
import org.springframework.data.cassandra.core.mapping.Column;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn;
#Table("Cart")
public class Cart {
#PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED)
#GeneratedValue(strategy = GenerationType.AUTO)
protected String id;
#PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED)
private String userId;
#PrimaryKeyColumn(ordinal = 2, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING)
private String productId;
}
Here is my com.mongo.Cart.java
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field
#Document(collection = "Cart")
public class Cart {
#Id
protected String id;
#Indexed(unique = true)
#Field(value = "UserId")
private String userId;
#Field(value = "ProductId")
private String productId;
}
Here's the Service class
public class CartServiceImpl{
#Autowired
CassandraCartRepository cassandraCartRepository;
#Autowired
MongoCartRepository mongoCartRepository;
public Cart save(CartDTO cart){
// based on configuration or profile load com.mongo.Cart or com.cassandra.Cart
// based on the configuration or profile do mongoCartRepository.save(cart) or cassandraCartRepository.save(cart)
}
}

Resources