I created abstract class Entity (I want to create different types of shapes):
#Entity
#Inheritance(strategy = TABLE_PER_CLASS)
#Getter
#Setter
#NoArgsConstructor
public abstract class ShapeEntity {
#Id
#GeneratedValue(generator = "system-uuid")
#GenericGenerator(name = "system-uuid", strategy = "uuid")
private String id;
#OneToOne
private ShapeDetailsEntity shapeDetailsEntity;
public abstract double getArea();
public abstract double getPerimeter();
}
and I want to every add to every entity table with details:
#Entity
#Getter
#Setter
#Table(name = "shape_details")
#AllArgsConstructor
public class ShapeDetailsEntity {
#Id
#GeneratedValue(generator = "system-uuid")
#GenericGenerator(name = "system-uuid", strategy = "uuid")
private String id;
...
#OneToOne(cascade = CascadeType.ALL, mappedBy = "shapeDetailsEntity", fetch = FetchType.LAZY)
private ShapeEntity shapeEntity;
The logic of creating entites is in service:
public class ShapeService {
public ShapeEntity createShape(ShapeType type, List<Double> parameters) {
switch (type) {
case CIRCLE:
return circleEntityRepository.saveAndFlush(new CircleEntity(parameters));
case SQUARE:
return squareEntityRepository.saveAndFlush(new SquareEntity(parameters));
case RECTANGLE:
return rectangleEntityRepository.saveAndFlush(new RectangleEntity(parameters));
default:
throw new IllegalArgumentException();
}
}
and now for tests in controller I would like to create new entity - in comments I put response in console:
#PostMapping
public ResponseEntity<String> post(#Valid #RequestBody ShapeRequestModel shapeRequestModel) {
ShapeEntity shapeEntity = shapeService.createShape(ShapeType.valueOf(shapeRequestModel.getType()), shapeRequestModel.getParameters());
ShapeDetailsEntity shapeDetailsEntity = shapeService.createShapeDetails(shapeEntity);
System.out.println(shapeDetailsEntity.getShapeEntity().toString()); // -> CircleEntity{radius=4.5}
System.out.println(shapeDetailsEntity); // -> ShapeDetailsEntity{all details...}
System.out.println(shapeEntity.getShapeDetailsEntity().toString()); // -> java.lang.NullPointerException: null
return new ResponseEntity<>(shapeEntity.toString(), HttpStatus.CREATED);
}
in shapeService.createShapeDetails(shapeEntity) looks like:
public ShapeDetailsEntity createShapeDetails(ShapeEntity shapeEntity) {
ShapeDetailsEntity shapeDetailsEntity = new ShapeDetailsEntity();
shapeDetailsEntity.setShapeEntity(shapeEntity);
return shapeDetailsEntityRepository.saveAndFlush(shapeDetailsEntity);
}
How should I do it correctly to not getting null in
shapeEntity.getShapeDetailsEntity().toString())
? In database place when should be id of shapeDetailsEntity I am getting null.
The ShapeEntity you are creating has no ShapeDetailsEntity assigned i.e. the field is null, so that's why you get a NPE. If you don't want that, you have to assign an object to that field.
IMO, this shapeService.createShapeDetails(shapeEntity); code shouldn't be necessary. Your ShapeService.createShape method should assign a ShapeDetailsEntity object to the field.
Related
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 have an entity with a composite key using #IdClass as following:
#Entity
#Builder
#NoArgsConstructor
#AllArgsConstructor
#JsonInclude(JsonInclude.Include.NON_EMPTY)
#JsonIgnoreProperties(ignoreUnknown = true)
#IdClass(MyId.class)
#SequenceGenerator(
name = "My_SEQUENCE",
initialValue = 1,
allocationSize = 1
)
public class MyEntity implements Serializable {
#Id
#Column(name = "id")
#GenericGenerator(
name = "MY_SEQUENCE",
strategy = "StringPrefixedSequenceIdGenerator",
parameters = {
#org.hibernate.annotations.Parameter(name = StringPrefixedSequenceIdGenerator.INCREMENT_PARAM, value = "1"),
#org.hibernate.annotations.Parameter(name = StringPrefixedSequenceIdGenerator.VALUE_PREFIX_PARAMETER, value = "ABC"),
#org.hibernate.annotations.Parameter(name = StringPrefixedSequenceIdGenerator.NUMBER_FORMAT_PARAMETER, value = "%07d") })
#GeneratedValue(strategy = GenerationType.IDENTITY, generator = "MY_SEQUENCE")
private String reqId;
#Id
#Column(name = "rev_no")
private Long revisionNumber;
...
}
And, it's repository as:
#Repository
public interface MyEntityRepository extends JpaRepository<MyEntity, Long> {
}
And on my service I want following:
public class MyEntityService extends BaseService<MyEntityRepository, MyEntity, Long> {
#Transactional
public MyEntity create(Payload payload) {
...
myEntity.setRevisionNumber(1);
myEntityRepository.save(myEntity);
...
}
public MyEntity update(String reqId, Long revisionNumber, Payload payload) {
var myEntity = findByIdAndRevisionNumber(reqId, revisionNumber);
...
myEntity.setRevisionNumber(revisionNumber + 1);
myEntity.setReqId(reqId);
myEntityRepository.save(myEntity);
}
I have two cases:
On create, Auto-generate reqId using sequence defined in Entity.
On update, increment revision number manually and save new entity with the same reqId.
For case 1, it's working fine but on case#2 it's overriding reqId with auto-generated sequence.
Is there any way to setId manually so that generator won't override it?
Since you are using GenerationType.IDENTITY, you won't be able to use setters for particular attribute. It won't work.
I'm trying to persist a Person entity, but I keep getting this null constraint violation error:
org.postgresql.util.PSQLException: ERROR: null value in column "created_by_party_id" violates not-null constraint
Detail: Failing row contains (5023, John, null, Smith, null, 1, null, null, 2020-11-11 07:33:31.590766-05, null, null, null).
What I have tried:
With and without the Discriminator annotations
Setting the value in #PrePersist
Initializing createdById in the base class with a default value
Making the Party class instantiable (not abstract), and persisting it directly - THAT WORKS, but it's not what I need
For some reason, the createdById is null by the time the SQL gets generated and passed off to to PostgreSQL. (I have verified in debug mode that this field is set, on the person entity, when it gets passed to the DAO save call.)
I'm using Spring boot, Hibernate, and PostgreSQL to map my tables and classes like this:
#MappedSuperclass
#EntityListeners( value = { EntityAuditListener.class } )
public abstract class BaseJPA {
#Column(name = "created_by_party_id", nullable = false)
private Long createdById = 1l;
/* Getters and Setters ... */
}
public class EntityAuditListener {
#PrePersist
public void prePersist(BaseJPA jpa) {
jpa.setCreatedById( 1l );
}
}
The Party class, although abstract, maps to a PARTY table:
#Entity(name = "Party")
#Table(name = "PARTY")
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorColumn(name = "PARTY_TYPE_CODE", discriminatorType = DiscriminatorType.INTEGER)
public abstract class Party extends BaseJPA implements Serializable {
private static final long serialVersionUID = 5434024967600745049L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "PARTY_ID_SEQ")
#SequenceGenerator(name = "PARTY_ID_SEQ", sequenceName = "PARTY_ID_SEQ", allocationSize = 1)
#Column(name = "PARTY_ID")
protected Long partyId;
#Column(name = "PARTY_TYPE_CODE", insertable = false, updatable = false)
protected PartyType partyType;
/* Getters & Setters ... */
}
The PartyType enum is registered with an AttributeConverter, with #Converter(autoApply = true)
public enum PartyType {
PERSON(1), UNIT(2), SYSTEM(3);
private final int value;
PartyType(int value) {
this.value = value;
}
/* Getter */
}
#Entity(name = "Person")
#Table(name = "PERSON")
#DiscriminatorValue( "1" )
#Inheritance(strategy = InheritanceType.JOINED)
public class Person extends Party implements Serializable {
private static final long serialVersionUID = -5747077306637558893L;
#Column(name = "FIRST_NAME")
private String firstName;
#Column(name = "LAST_NAME")
private String lastName;
/* More fields ...*/
public Person() {
this.partyType = PartyType.PERSON;
}
/* Getters & Setters */
}
Are you sure that the entity listener method is called? Are you sure that there is no other method that might set the value to null?
If all that doesn't help, please create a reproducing test case and submit an issue to the Hibernate issue tracker.
I am getting an exception when I am updating Parent record in spring data jpa.
This is my code:
ParentEntity
#Entity
#Table(name = "CAMP")
#Getter
#Setter
public class Parent extends AbstractEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tkeygenerator")
#GenericGenerator(name = "tkeygenerator", strategy = "com.custom.TKeyGenerator",
parameters = {#org.hibernate.annotations.Parameter(name = "sequence", value = "TKEY_SEQ")})
#Column(name = "TKEY", nullable = false)
private String id;
#ManyToOne
#JoinColumn(name = "SUB_CAT_TYPE_CODE", referencedColumnName = "SUB_CAT_TYPE_CODE")
private Child child;
#Column(name = "DATE")
#Basic
private LocalDate date;
}
Child Entity
#Entity
#Table(name = "SUB_CAT_TYPE")
#AttributeOverrides({
#AttributeOverride(name = "code",
column = #Column(name = "SUB_CAT_TYPE_CODE", length = 30)),
#AttributeOverride(name = "description",
column = #Column(name = "SUB_CAT_TYPE_DESC", length = 255))})
#EqualsAndHashCode(callSuper = true)
public class Child extends AbstractTypeDesc {}
TestCode
public Parent update(#PathVariable("id") String id, #Valid #RequestBody UpdateDto dto) {
Parent parentObj = parentRepository.findById(id);
mapper.map(dto, parentObj); // Dozer to map incoming dto to domain
childRepository.findByCode(dto.child().getCode())
.map(child -> {
parentObj.setChild(child);
return child;
});
return parentRepository.save(parentObj); //Exception occurs here
}
I am getting an exception while trying to update code variable of child entity in parent entity as fk. It says can't alter code from X to Y.
Any suggestion?
I figured out what went wrong in above code block for update operation.Although not much clear why its happening.Dozer mapping which maps dto to Domain was causing issue it was changing the value of child entity and then again when i was trying to set child entity through setter method It was causing "Id alter exception", Though i thats the same thing i dont know why it was taking it differently. below is the working Working code.
Test code should be like
public Parent update(#PathVariable("id") String id, #Valid #RequestBody UpdateDto dto) {
Parent parentObj = parentRepository.findById(id);
childRepository.findByCode(dto.child().getCode())
.map(child -> {
parentObj.setChild(child);
return child;
});
mapper.map(dto, parentObj); // Dozer dto to domain mapping was causing problem
return parentRepository.save(parentObj);
}
There is simple model of associated entities:
MainModel class
#Entity
#Table(name = "main_model")
public class MainModel
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "id_main_model", referencedColumnName = "id")
private List<AssociatedModel> am_collection;
public MainModel() { }
public long getId()
{
return this.id;
}
public void addAssociated(AssociatedModel am)
{
am_collection.add(am);
}
}
AssociatedModel class
#Entity
#Table(name = "associated_model")
public class AssociatedModel
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
public AssociatedModel() { }
public long getId()
{
return this.id;
}
}
Simplest jpa repository for MainModel
public interface MainModelDAO extends JpaRepository<MainModel, Long>
{
}
and finally, controller's method for creating and saving AssociatedModel instance
#RequestMapping("/url")
public String createAssociated(#RequestParam("id_mainmodel") long id_mainmodel, #RequestBody AssociatedModel newAm)
{
MainModel mm = MainModelDAOobject.findOne(id_mainmodel);// MainModelDAOobject - #Autowired
mm.addAssociated(newAm);
MainModelDAOobject.saveAndFlush(mm);
return String.valueOf(newAm.getId());// get Id of associated saved object
}
Associated obect saves into database correctly, but id value of this object, allowed by its getId() method, always zero. How can I get Id of the saved associated object correctly?
Try calling MainModelDAOobject.flush() after saving the value, but before reading the ID.
See this answer for an explanation.
Edit: try manually saving newAm by adding a new line:
mm.addAssociated(newAm); // Existing line
newAm = MainModelDAOobject.save(newAm); // New line
MainModelDAOobject.save(mm); // Existing line
you need a sequencegenerator:
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MainModel SequenceGenerator")
#SequenceGenerator(name = "MainModel SequenceGenerator", sequenceName = "MainModel Sequence", initialValue = SEQUENCE_INITIAL_VALUE, allocationSize = SEQUENCE_ALLOCATION_SIZE)
Thanks to David Levesque for helpful suggestion.
problem is solved as follows:
need additional repository for AssotiatedModel
public interface AssotiatedModelDAO extends JpaRepository {}
then just save associated object manually:
mm.addAssociated(newAm);
newAm = AssotiatedModelDAOobject.save(newAm);
newAm = MainModelDAOobject.save(newAm)`;
MainModelDAOobject.save(mm);