Hibernate makes its magic with reflection and init #Entity with data when it fetches data from DB. So I don't need to add setters or a constructor to init my data.
#Entity
#Table(name = "products")
public class Product implements Serializable {
private integer id;
private String name;
}
#Repository
public interface ProductsRepository extends JpaRepository<Product, String> {
#Query("FROM #{#entityName} p where p.id = :id")
Product findById(integer id);
}
Fun starts when you make a unit test for some Service that deals with JpaRepository. I want to mock the repo to return mocked Product with test data
#Service
public class MyAwesomeService {
#Autowired
private ProductsRepository productsRepository;
public void returnProductName(Long productId) {
Product product = productRepository.findById(productCode);
return product.getName();
}
}
// The test will look like
#ExtendWith(SpringExtension.class)
public class ProductsDeliveryTest {
#InjectMocks
private MyAwesomeService myAwesomeService;
#Mock
private ProductsRepository productsRepository;
#Test
public void test() {
String productId = 1L;
Product product = new Product() // here I need to init Product with data
doReturn(productBookingFields)
.when(productsRepository)
.findById(productId);
String name = myAwesomeService.returnProductName(productId);
assertThat(name).isEqualTo("Test Product");
}
}
So I could
add a constructor for the Product where I will init fields
or make Product as JavaBean and use setters to init my data
The problem is - I need them only for the tests and it doesn't look like a good idea when I change my code to make my tests work.
Is there a more elegant way how can I mock Entity with preset data?
You could simply mock the entity.
#Test
public void test() {
String productName = "w00t!";
long productId = 1L;
Product product = mock(Product.class);
doReturn(productName).when(product).getName();
doReturn(product)
.when(productsRepository)
.findById(productId);
String name = myAwesomeService.returnProductName(productId);
assertThat(name).isEqualTo(productName);
}
It is a bit strange though to see your entity with only the default constructor, and no setters. Such an entity could only be used as a read only entity. Your application code could never insert a new Product or update the fields of an existing one.
This may be helpful...
#Mock Product productMock;
#Mock ProductRepository productRepoMock;
#Test
public void validateEntityMock() {
given(productMock.getName()).willReturn("name");
given(productRepoMock.findOne(anyInt())).willReturn(productMock);
// assert the results...
}
Related
I am currently working on a project where I have created the following custom Repository:
public interface ServiceRepository<T extends ServiceEntity> extends JpaRepository<T, UUID>, ServiceRepositoryCustom {
}
public interface ServiceRepositoryCustom {
List<ServiceEntity> findAllContainingName(String query);
}
#Repository("Repo")
public class ServiceRepositoryCustomImpl implements ServiceRepositoryCustom {
private final EntityManager em;
public ServiceRepositoryCustomImpl(EntityManager em) {
System.out.println("I got constructed");
this.em = em;
}
#Override
public List<ServiceEntity> findAllContainingName(String name) {
System.out.println("I got called with: " + name);
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<ServiceEntity> cq = cb.createQuery(ServiceEntity.class);
Root<ServiceEntity> serviceEntity = cq.from(ServiceEntity.class);
List<Predicate> predicates = new ArrayList<>();
if(name != null) {
// predicates.add(cb.equal(serviceEntity.get("name"), name));
predicates.add(cb.like(serviceEntity.get("name"), name + "%"));
}
cq.where(predicates.toArray(predicates.toArray(new Predicate[0])));
return em.createQuery(cq).getResultList();
}
}
The print statement "I got called with: " never gets called. So for whatever reason Spring Boot is not running the method through my custom implementation.
Any suggestions? Any help is much appreciated
Edit:
Here is the code that injects and uses the Repository in question
#Repository
public interface PineappleServiceRepository extends ServiceRepository<PineappleServiceEntity> {
}
#Component("Registry")
#DependsOn({"Context", "Repo"})
public class Registry {
private final List<ServiceRepository<? extends ServiceEntity>> serviceRepositories = new ArrayList<>();
public Registry(PineappleServiceRepository pineappleServiceRepository) {
this.serviceRepositories.add(pineappleServiceRepository);
}
}
Edit 2:
The code prints "I got constructed"
Edit 3:
Class where findAllContainingName is called
#RestController
#RequestMapping("/test")
#DependsOn("Registry")
public class ServiceController {
private final Registry registry;
public ServiceController(#NotNull Registry registry) {
this.registry = registry;
}
#GetMapping("")
List<ServiceEntity> all(#RequestParam("q") String query) {
return getAllServices(query);
}
private #NotNull List<ServiceEntity> getAllServices(String query) {
List<ServiceEntity> response = new ArrayList<>();
for(ServiceRepository<? extends ServiceEntity> repo: this.registry.getServiceRepositories()){
response.addAll(repo.findAllContainingName(query));
}
return response;
}
}
Edit 4:
Here the entities:
#Entity
#Table(name = "services")
public abstract class ServiceEntity {
protected #Id
UUID id = UUID.randomUUID();
protected String name;
// Constructor + Getters and Setters
}
#Entity
public class PineappleServiceEntity extends ServiceEntity {
// Additional Properties, matching Constructors, Getters and Setters
}
So I was able to reproduce your problem and fix it. Issue with your code is that your PineappleServiceRepository is not extending ServiceRepositoryCustom directly. It seems your repository needs to implement it directly if you are accessing custom repository methods from that repository. I got that idea from this post.
So to fix your issue, either remove PineappleServiceRepository(as you don't have any properties in PineappleEntity) and use ServiceRepository to call that custom method or make PineappleServiceRepository extend ServiceRepositoryCustom.
I have pushed changes to GitHub with fix. You can take a look. If you want to keep PineappleServiceRepository and access custom method using this repository, let me know, I can update code.
I am working spring boot project that uses spring data as an abstraction to access the database[MongoDB]. I want to change the write concern only for two specific operations.
Below is the entity and repository class that I use to access the Mongo DB collection:
Entity
#Document(collection = "tests")
#Data
#NoArgsConstructor
public class Test {
#Id
private String id;
private String name;
private String category;
}
Repository
#Repository
public interface TestRepository extends BaseMongoRepository<Test> {
...
#DeleteQuery(value="{'id':?0}, { writeConcern: { w : '2', wtimeout : 1000 }, delete=true")
void safeDeleteByTestId(String id,String name);
default void updateNameForAll(String category) {
final Query query = query(where("category").is(category);
final Update update = Update.update("name", name);
getMongoOperations().updateMulti(query, updategetMetadata().getCollectionName());
}
...
}
How can I modify the updateNameForAll method to increase the write concern only for this query? I don't want to override write concern for the entire collection or database.
Kind Regards,
Rando.
I found a workaround to this issue:
I created a new interface named ETestRepository like below:
public interface ETestRepository {
void safeUpdateNameForAll(String category);
}
Then I created a implementation of the interface:
public interface ETestRepositoryImpl implements ETestRepository {
#Autowired
private MongoTemplate mongoTemplate;
#Override
public void safeUpdateNameForAll(String accountId, String contextId, ChangeSetRowAction action) {
mongoTemplate.setWriteConcern(WriteConcern.W2);
final Query query = query(where("category").is(category);
final Update update = Update.update("name", name);
mongoTemplate.updateMulti(query,update, Test.class);
}
}
In the end, I modified the TestRepository interface to extend the ETestRepository interface to include the safeUpdateNameForAll method.
I'm creating DTO versions of all my entities. I have a problem with an entity that has one Enum value. This is my entity:
#Getter
#Setter
#Table(name = "TIPOS_MOVIMIENTO")
#Entity
public class TipoMovimiento {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column
#Convert(converter = TipoMovEnumConverter.class)
private TipoMov tipo;
public String getTipo() {
return tipo.getTipoNombre();
}
#OneToMany(mappedBy = "tipoMov")
private List<Movimiento> movimientos;
No, I don't have #Enumerated because I followed a tutorial: "JPA 2.1 Attribute Converter – The better way to persist enums" and I had to remove it. I use a converter, as you can see.
This is my DTO of the previous entity:
#Getter
public class TipoMovimientoDto implements DtoEntity {
#Convert(converter = TipoMovEnumConverter.class) //I don't even know if write this here!!!!!
private TipoMov tipo;
}
The reason why I've followed that tutorial ↑ is because I wanted to write in database the variable values (tipoNombre) of enum (not enum name itself) because format. I want to store it in DB with accents, and I want to show it in Postman or whatever REST Client app with accents! Don't tell me anything about format it in front-end because this project is only back-end :(
Well, I think you will understand what I found with this with a image:
If you know a better way to do this, let me know, but this is not my problem now.
Let me show you the Enum:
public enum TipoMov {
INGRESO("Ingreso"),
PRESTAMO("Prestamo"),
PAGO("Pago"),
AMORTIZACION("Amortización"),
INTERES("Interés");
private String tipoNombre;
public String getTipoNombre() {
return tipoNombre;
}
TipoMov(String tipoNombre) {
this.tipoNombre = tipoNombre;
}
public static TipoMov fromDBName(String tipoNombre) {
switch (tipoNombre) {
case "Ingreso":
return TipoMov.INGRESO;
case "Préstamo":
return TipoMov.PRESTAMO;
case "Pago":
return TipoMov.PAGO;
case "Amortización":
return TipoMov.AMORTIZACION;
case "Interés":
return TipoMov.INTERES;
default:
throw new IllegalArgumentException("ShortName [" + tipoNombre
+ "] not supported.");
}
}
}
The problem is that I can't get the output in Postman if I convert this to DTO version. I get the appropiate output without DTO. I'm using REST services. Let me show you the services and controller.
(They include both versions, without DTO and with DTO (that is not working)).
ServiceImpl
#Service
public class TipoMovimientoServiceImpl implements TipoMovimientoService {
#Autowired
TipoMovimientoRepository repository;
#Autowired
DtoUtils dtoUtils;
public List<DtoEntity> findAllDto() {
List<TipoMovimiento> tiposMovimiento = repository.findAll();
List<DtoEntity> tiposMovimientoDto = new ArrayList();
for (TipoMovimiento tipoMovimiento : tiposMovimiento) {
DtoEntity tipoMovimientoDto= dtoUtils.convertToDto(tipoMovimiento, new TipoMovimientoDto());
tiposMovimientoDto.add(tipoMovimientoDto);
}
return tiposMovimientoDto;
}
public List<TipoMovimiento> findAll() {
List<TipoMovimiento> tiposMovimiento = repository.findAll();
return tiposMovimiento;
}
}
Service Interface
public interface TipoMovimientoService {
List<DtoEntity> findAllDto();
List<TipoMovimiento> findAll();
}
Controller:
#RestController
public class PruebasController {
#Autowired
TipoMovimientoService service;
#GetMapping("tiposmovdto")
public ResponseEntity <List <DtoEntity> > findAllDto() {
return ResponseEntity.ok(service.findAllDto());
}
#GetMapping("tiposmov")
public ResponseEntity <List <TipoMovimiento> > findAll() {
return ResponseEntity.ok(service.findAll());
}
}
As I said, the nonDto version works perfectly, but DTO version no. Is not the fault of DTO converter, because I have other REST services (that don't have enums) working perfectly with DTO. This is a problem about making compatible Enum and Dto!
I got it!!! I never thought this would work.
#Getter
public class TipoMovimientoDto implements DtoEntity {
private TipoMov tipo;
}
I just changed in the code above (Dto):
private TipoMov tipo;
to
private String tipo;
I can't explain how Enum from Entity could have been converted to DTO, using String instead Enum... But that worked!
In case you have the same problem... this is my Attribute Converter between Enum and String
#Converter(autoApply = true)
public class TipoMovEnumConverter implements AttributeConverter<TipoMov, String> {
public String convertToDatabaseColumn(TipoMov tipoMov) {
return tipoMov.getTipoNombre();
}
public TipoMov convertToEntityAttribute(String dbData) {
return dbData == null ? null : TipoMov.fromDBName(dbData);
}
}
Is still necessary to use it in Entity class, above of the enum variable:
#Convert(converter = TipoMovEnumConverter.class)
But not necessary in DTO. Just use String instead Enum in DTO!
I have domain:
class Company {
List<Job> jobs;
}
Is there a way to return nested object from collection like:
#Repository
public interface CompanyRepository extends MongoRepository<Company, String>{
Job findByJobId(String jobId);
}
Yes it is possible, try this:
Company.class
#Document
public class Company {
#Id
private String id;
#Field("name")
private String Name;
#DBRef
List<Job> job;
// Getters and Setters
}
Job.class
#Document
public class Job {
#Id
private String id;
#Field("name")
private String name;
// Getters and Setters
}
CompanyRepository.class
public interface CompanyRepository extends MongoRepository<Company, String> {
Company findOneByJobId(String id);
List<Company> findByJobId(String id);
}
JobRepository.class
public interface JobRepository extends MongoRepository<Job, String>{
Job findOneByName(String name);
}
Then you can #Autowire the repositories and invoke the methods:
Job java = new Job("Core Java Developer");
Job spring = new Job("Spring Web Developer");
Job cSharp = new Job("C# Developer");
Job dotNet = new Job(".Net Web Developer");
List<Job> allJobs = Arrays.asList(java,cSharp,spring, dotNet);
// Save All Jobs
jobRepository.save(allJobs);
// Create Companies
Company oracle = new Company("Oracle", Arrays.asList(java));
Company microsoft = new Company("Microsoft", Arrays.asList(cSharp, dotNet));
Company pivotal = new Company("Pivotal", Arrays.asList(java, spring));
// Save all companies
companyRepository.save(Arrays.asList(oracle,microsoft,pivotal));
// Find job by name - C#
Job cSharpJob = jobRepository.findOneByName("C# Developer");
System.out.println("*******************Found Job by Name************************");
System.out.println(cSharpJob);
System.out.println("*******************************************");
// Find One Company having Job with Job Id - C#
Company companyWithcSharpJob = companyRepository.findOneByJobId(cSharpJob.getId());
System.out.println("********************Company having C# Job found using Job Id: "+ cSharpJob.getId() +"***********************");
System.out.println(companyWithcSharpJob.getName());
System.out.println("*******************************************");
Checkout the Complete Project in my GitHub repository.
I have to make some assumptions about the structure of your Job model, but assuming something like this:
public class Job {
private String id;
// other attributes and methods
}
... and assuming that this model is embedded in your Company model, and not represented in another collection, you will have to go the custom implementation via MongoTemplate route. The Spring Data query API is not going to be able to figure out how to get what you want, so you must implement the method yourself.
#Repository
public interface CompanyRepository extends CompanyOperations, MongoRepository<Company, String>{
}
public interface CompanyOperations {
Job findByJobId(String jobId);
}
public class CompanyRepositoryImpl implements CompanyOperations {
#Autowired private MongoTemplate mongoTemplate;
#Override
public Job findByJobId(String jobId){
Company company = mongoTemplate.findOne(new Query(Criteria.where("jobs.id").is(jobId)), Company.class);
return company.getJobById(jobId); //implement this method in `Company` and save yourself some trouble.
}
}
you have to use #Query annotation for return the data on the basis of nested object field.
#Repository public interface CompanyRepository extends MongoRepository<Company, String>{ #Query("{'jobs.$jobId' : ?0}") List<Job> findCompanyByJobId(String jobId);}
I have a controller which produces JSON, and from this controller, I return an entity object, which is automatically serialized by Jackson.
Now, I want to avoid returning some fields based on a parameter passed to the controller. I looked at examples where this is done using FilterProperties / Mixins etc. But all the examples I saw requires me to use ObjectMapper to serialize / de-serialize the bean manually. Is there any way to do this without manual serialization? The code I have is similar to this:
#RestController
#RequestMapping(value = "/myapi", produces = MediaType.APPLICATION_JSON_VALUE)
public class MyController {
#Autowired
private MyService myService;
#RequestMapping(value = "/test/{variable}",method=RequestMethod.GET)
public MyEntity getMyEntity(#PathVariable("variable") String variable){
return myservice.getEntity(variable);
}
}
#Service("myservice")
public class MyService {
#Autowired
private MyEntityRepository myEntityRepository;
public MyEntity getEntity(String variable){
return myEntityRepository.findOne(1L);
}
}
#Entity
#Table(name="my_table")
#JsonIgnoreProperties(ignoreUnknown = true)
public class MyEntity implements Serializable {
#Column(name="col_1")
#JsonProperty("col_1")
private String col1;
#Column(name="col_2")
#JsonProperty("col_2")
private String col2;
// getter and setters
}
Now, based on the value of "variable" passed to the controller, I want to show/hide col2 of MyEntity. And I do not want to serialize/deserialize the class manually. Is there any way to do this? Can I externally change the Mapper Jackson uses to serialize the class based on the value of "variable"?
Use JsonView in conjunction with MappingJacksonValue.
Consider following example:
class Person {
public static class Full {
}
public static class OnlyName {
}
#JsonView({OnlyName.class, Full.class})
private String name;
#JsonView(Full.class)
private int age;
// constructor, getters ...
}
and then in Spring MVC controller:
#RequestMapping("/")
MappingJacksonValue person(#RequestParam String view) {
MappingJacksonValue value = new MappingJacksonValue(new Person("John Doe", 44));
value.setSerializationView("onlyName".equals(view) ? Person.OnlyName.class : Person.Full.class);
return value;
}
Use this annotation and set the value to null, it will not be serialised:
#JsonInclude(Include.NON_NULL)