I want to create a unit test that will use reflection to find all missing fields in dto that implement BaseDto by their persistence entities. This is what I did.
#Slf4j
public class EntityAuditDtoTest {
#Test
public void find_MissingAndExtraFieldsThatUsedInAuditDtosByEntity_ReturnMissingAndExtraFields() throws ClassNotFoundException {
// Arrange
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(AuditEntityType.class));
// Find all classes annotated with #AuditEntityType in the package com.example.dto
Set<BeanDefinition> auditDtoBeans = scanner.findCandidateComponents("com.example.dto");
// Act
for (BeanDefinition auditDtoBean : auditDtoBeans) {
Class<?> auditDtoClass = Class.forName(auditDtoBean.getBeanClassName());
// Make sure the DTO class implements BaseAuditDto
if (!BaseAuditDto.class.isAssignableFrom(auditDtoClass)) {
continue;
}
Class<?> entityClass = getEntityClassForDto(auditDtoClass);
Field[] dtoFields = auditDtoClass.getDeclaredFields();
Field[] entityFields = entityClass.getDeclaredFields();
List<String> missingFields = Arrays.stream(entityFields).map(Field::getName)
.filter(field -> Arrays.stream(dtoFields).noneMatch(f -> f.getName().equals(field))).toList();
if (!missingFields.isEmpty()) {
log.error("Missing fields in DTO class: {} \nfor entity class: {} : {}", auditDtoClass.getName(),
entityClass.getName(), missingFields);
}
List<String> extraFields = Arrays.stream(dtoFields).map(Field::getName)
.filter(field -> Arrays.stream(entityFields).noneMatch(f -> f.getName().equals(field))).toList();
if (!extraFields.isEmpty()) {
log.error("Extra fields in DTO class: {} \nfor entity class: {} : {}", auditDtoClass.getName(),
entityClass.getName(), extraFields);
}
}
}
}
But the problem is that the dto may have a field that is in the entity class, but the test will think that this is a missing field.
For example:
Dto class: ContractAudit has customerId field (customerId). And ContractEntity has public CustomerEntity customer.
This is the same fields. But of course for test they are different. I don't understand how to ignore them. I also don't want to hardcode filter that skip all endings with 'id' prefix.
#Data
#AuditEntityType("Contract")
public class ContractAudit implements BaseAuditDto {
private Long id;
private String ref;
private String status;
private Long customerId;
}
#Entity
#Table(name = "contract")
#Getter
#Setter
#ToString
#AllArgsConstructor
#NoArgsConstructor
#Builder
public class ContractEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
#ToString.Include
private Long id;
#Column(name = "ref", updatable = true)
#ToString.Include
private String ref;
#Column(name = "status")
#ToString.Include
#Enumerated(value = EnumType.STRING)
private ContractStatusEnum status;
#ManyToOne
#JoinColumn(name = "customer_id")
public CustomerEntity customer;
#Column(name = "deleted")
#ToString.Include
private boolean deleted;
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "contract_id")
private List<ContractDocumentEntity> documents;
}
Output:
Missing fields in DTO class: ContractAudit for entity class: ContractEntity : [customer, deleted, documents]
Extra fields in DTO class: ContractAudit for entity class: ContractEntity : [customerId]
I want to have missing fields: [deleted, documents]
If you have any other ideas on how to do this, I'd love to hear it. I am not asking for implementation. Suggestions only)
Lol. I found solution for my case.
My previous approach was incorrect. Because it's impossible to find 'missing' and 'extra' fields by name correctly for every case. I decided to use:
assertThat(entityClass.getDeclaredFields()).hasSameSizeAs(auditDtoClass.getDeclaredFields());
So this code is checking if the entityClass and the DtoClass have the same number of fields (properties) declared. If not it fail test and print all fields from each classes. If anyone has better ideas I'll be happy to hear.
I am using the following class as an entity, and a controller class to write data on it:
#Entity
#Table(name = "TableA")
public class TableA {
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(nullable = false, name="Id")
private BigInteger id;
#Column(nullable = false, name="Name")
private String name;
//Setters Getters
}
#RestController
public class TableAController {
#Autowired
TableARepository tableARepository;
#Transactional(rollbackFor = Exception.class)
#PostMapping(value="/CreateTableA")
public void createTableA(#RequestBody TableA newTableA){
TableA tableA = new TableA();
tableA = newTableA;
tableARepository.save(tableA);
}
}
The Id column value will be generated by the DB, so I used the #JsonProperty. But, when I test the REST API using the following as a request:
{
"name" : "Leo Messi"
}
I am getting the aforementioned error message. I have also tried the #JsonIgnore property with the same result. Is there a way to except the id property from the deserialization process? Or should I use another class dedicated the API Request? I am not comfortable with creating different models for every new API.
I am not sure if I should focus on resolving the error, or if I should design the classes using a Design Pattern that never produces it.
I've got a UserRepository:
#Repository
public interface UserRepository extends JpaRepository<User, Long>
{
}
where User:
#Entity
#Table(name = 'user')
public class User
{
#Id
private Long id;
#OneToOne(mappedBy = "owner", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private UserDetails userDetails;
}
and UserDetails:
#Entity
#Table(name = 'user_details')
public class UserDetails
{
#Id
private Long id;
#OneToOne(fetch = FetchType.LAZY)
#MapsId
private User owner;
private String name;
}
Packages, imports, getters and setters are excluded for cleaner code.
Now, how can I find users by their name? Adding this to the UserRepository interface will not work:
List<User> findByName(String name);
because it throws:
No property name found for type User
I am looking for something like this:
List<User> findByNameOfUserDetails(String name);
Please take a look at the Spring Data JPA docs here.
You'll need something like findByUserDetailsName(String name).
To resolve this ambiguity you can use _ inside your method name to manually define traversal points. So our method name would be findByUserDetails_Name(String name).
I'm saving two entity related to each other. After it, I can get the first entity, but I get a NullPointerException when I try to get the second entity from the first entity. This is the example:
#Entity
#Table(name = "PARAMETRIZACION")
public class Parametrizacion {
#Id
#Column(name = "id_param", unique = true, nullable = false)
private Integer idParam;
#OneToMany(fetch = FetchType.LAZY)
private List<Arreglo> listArreglo;
}
And
#Entity
#Table(name = "ARREGLO")
public class Arreglo {
#Id
#Column(name = "id_arreglo", unique = true, nullable = false)
private Integer idArreglo;
}
And my Service:
#Service
#Repository
public class TestServiceImpl implements TestService {
#Override
#Transactional(rollbackFor = Exception.class)
public void methodTest(){
...
parametrizacionRepository.saveAndFlush(parametrizacion);//Id=1
...
arregloRepository.saveAndFlush(listArreglo);//Id=1
Parametrizacion paramFetch = parametrizacionRepository.findById(1);
Log.info("Param.Id=" + paramFetch.getIdParam());
Log.info("Size=" + paramFetch.getListArreglo().size());
}
}
The result for first log is: Param.Id=1
The result for second log is: NullPointerException
How can I get the full entity including his childrens? Only If I do this query after commit transaction I can get the data but I need Save data, Update data and Find data before do Commit on finish transaction.
Maybe there is a problem with the unidirectional relationship. Try adding some #ManyToOne field in the Arreglo class and declare how should they match by adding mappedBy="" to the #OneToMany annotation.
There are some nice examples how the relations should look like:
https://en.wikibooks.org/wiki/Java_Persistence/OneToMany
What you are doing is saving parametrizacion and listArreglo separately. And this don't set any relation for parametrizacion with Arreglo. You have to set listArreglo to parametrizacion's listArreglo variable and save only parametrizacion.
I have a two objects with simple #OneToMany relationship which looks as follows:
parent:
#Entity
public class ParentAccount {
#Id
#GeneratedValue
private long id;
private String name;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "parentAccount")
private Set<LinkedAccount> linkedAccounts;
}
child:
#Entity
public class LinkedAccount {
#Id
#GeneratedValue
private long id;
#ManyToOne(optional = false)
private ParentAccount parentAccount;
private String name;
// empty constructor for JPA
public LinkedAccount() {
}
}
I ma using Spring CrudRepository to operate with these entities. However, when calling ParentAccount parent = parentAccountRepository.findOne(id);, some kind of infinite loop starts happening and hibernate spams this all over the console:
Hibernate: select linkedacco0_.parent_account_id as parent_a6_1_0_, linkedacco0_.id as id1_0_0_, linkedacco0_.id as id1_0_1_, linkedacco0_.aws_id as aws_id2_0_1_, linkedacco0_.key_id as key_id3_0_1_, linkedacco0_.name as name4_0_1_, linkedacco0_.parent_account_id as parent_a6_0_1_, linkedacco0_.secret_key as secret_k5_0_1_ from linked_account linkedacco0_ where linkedacco0_.parent_account_id=?
I tried changed the fetch type to LAZY but then I get this error:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.berrycloud.scheduler.model.ParentAccount.linkedAccounts, could not initialize proxy - no Session
(It seems that it is trying to do the lazy load outside of the transactional context).
This is my CRUD repository:
#Repository
public interface ParentAccountRepository extends CrudRepository<ParentAccount, Long> {
}
Could someone tell me how to resolve this issue? I would prefer the solution with EAGER fetch. Thank you for any tips
EDIT: here is the schema I am using
CREATE TABLE parent_account (
id BIGINT auto_increment,
name VARCHAR(80) null,
PRIMARY KEY (`id`)
);
CREATE TABLE linked_account (
id BIGINT auto_increment,
parent_account_id BIGINT,
name VARCHAR(80) null,
FOREIGN KEY (`parent_account_id`) REFERENCES `parent_account` (`id`),
PRIMARY KEY (`id`)
);
As the first answer suggests:
Do not use Lombok's #Data annotation on #Entity classes.
Reason: #Data generates hashcode(), equals() and toString() methods that use the generated getters. Using the getter means of course fetching new data even if the property was marked with FetchType=LAZY.
Somewhere along the way hibernate tries to log the data with toString() and it crashes.
Problem solved. I was using a custom #toString method in the LinkedAccount which was referencing the ParentAccount. I had no idea that this could cause any problem and therefor I did not include the toString in my question.
Apparently, this was causing an infinite loop of lazy loading and removing this reference fixed the problem.
As user1819111 told, #Data from Lombok is not compatible with #Entity and FetchType=LAZY. I had used Lombok.Data (#Data) and I was getting this error.
As I don't want do create all get/set, I just put the Lombok #Setter and #Getter in your class and all will work fine.
#Setter
#Getter
#Entity
#Table(name = "file")
#SequenceGenerator(name = "File_Sequence", allocationSize=1, sequenceName = "file_id_seq")
public class MyClass{
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "File_Sequence")
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
#OneToMany(mappedBy = "file", cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
private Set<Base2FileDetail> details = new HashSet<>();
}
Something like this does not work?
#Entity
public class Account {
#Id
#GeneratedValue
private long id;
private String name;
#ManyToOne(cascade={CascadeType.ALL})
#JoinColumn(name="manager_id")
private Account manager;
#OneToMany((fetch = FetchType.EAGER, mappedBy="manager")
private Set<Account> linkedAccounts = new HashSet<Account>();
}
I recently had this issue due to a poorly defined Jackson2HttpMessageConverter.
I had done something like the following.
#Bean
RestTemplate restTemplate(#Qualifier("halJacksonHttpMessageConverter")
TypeConstrainedMappingJackson2HttpMessageConverter halConverter) {
final RestTemplate template = new RestTemplateBuilder().build();
halConverter.setSupportedMediaTypes(List.of(/* some media types */));
final List<HttpMessageConverter<?>> converters = template.getMessageConverters();
converters.add(halConverter);
template.setMessageConverters(converters);
return template;
}
This caused a problem because the media types did not include all the defaults. Changing it to the following fixed the issue for me.
halConverter.setSupportedMediaTypes(
new ImmutableList.Builder<MediaType>()
.addAll(halConverter.getSupportedMediaTypes())
.add(/* my custom media type */)
.build()
);
This simple way worked for me. Just use JsonIgnoreProperties .
#JsonIgnoreProperties(value = {"linkedAccounts"})
#ManyToOne(cascade = { CascadeType.PERSIST})
#JoinColumn(name = "abc", referencedColumnName = "abc")
private ParentAccount parentAccount;
This way worked for me without removing #ToSring annotation:
#Entity
#Getter
#Setter
#ToString
#RequiredArgsConstructor
#AllArgsConstructor
#Table(name = "parent_accounts")
public class ParentAccount {
#JsonIgnoreProperties({"parentAccount"})
#OneToMany(mappedBy = "parentAccount",
cascade = CascadeType.ALL,
orphanRemoval = true)
private List<LinkedAccount> linkedAcounts;
// ...
}
#Entity
#Getter
#Setter
#ToString
#RequiredArgsConstructor
#AllArgsConstructor
#Table(name = "linked_accounts")
public class LinkedAccount {
#JsonIgnoreProperties("linkedAcounts")
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "parentAccount_id")
private ParentAccount parentAccount;
// ...
}
PS: In #JsonIgnoreProperties You can also ignore more than one field for preventing infinite loop