Jackson with Spring boot: control object substitution for specific method - java

I have entity type as item in recursive tree, so any item has references to its parent and children (of same type)
public class Category {
private Integer id;
private String displayName;
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
#JsonIdentityReference(alwaysAsId=true)
private Category parent;
#JsonInclude(JsonInclude.Include.NON_EMPTY)
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
#JsonIdentityReference(alwaysAsId=true)
private Set<Category> children;
// constructors, getters and setters
}
As you can see, I marked both reference fields with #JsonIdentityReference annotation forcing them to render as plain ids. Currently with this setup an entity is rendered as follows:
// from .../categories/0
{
"id" : 0,
"displayName" : "Root",
"parent" : null,
"children" : [ 1, 13, 17 ]
}
(which is perfectly fine).
However, very common use scenario for client is to fetch whole sub-tree starting from specific node, which in this implementation requires several requests to server. I want to create another enpoint that allows client to perform this operation with single request.
#RestController
#RequestMapping(value = "/categories")
public class CategoryController {
#Autowired
private CategoryService categoryService;
#Autowired
private CategoryRepository categoryRepo;
#RequestMapping(value = "/tree", method = RequestMethod.GET)
public Category getTree(#RequestParam(name = "root", required = false) Integer id) {
Category root = id == null ? categoryService.getRoot() : categoryRepo.findOne(id);
return categoryService.getTree(root);
}
// other endpoints
// getOne(id)
// getAll()
}
Response from this endpoint renders full objects only if I manually remove (alwaysAsId=true) flag from the children field. However, I want both endpoints coexist and render different layout. So, the question is: How can I make specific controller method choose whether full entities are replaced with ids?.
I already tried various combinations with #JsonView, but it seems this approach doesn't work. #JsonView can only control whether specific field is included or completely omitted, whereas I need children field to only change layout. Also, since child type is same as entity type, there is no way to split annotation marks between different classes.

/* Updated answer, after the remark about loosing the view markers in multiple levels hierarchies. */
The result you are looking for can still be achieved by using #JsonViews and a minor work-around inside Category object.
Supposing we have two types used as markers to output JSON views FlatView and HierarchicalView.
The principle of the solution is:
we associate most of the fields with the views. children field remains associated with FlatView only.
we create an additional getter for the children field, providing it with another property name, not clashing with the field or its original getter. This property is associated with HierarchicalView only.
It gives the following layout for Category class:
public class Category {
#JsonView({ FlatView.class, HierarchicalView.class}) // both views
private Integer id;
#JsonView({ FlatView.class, HierarchicalView.class}) // both views
private String displayName;
#JsonView({ FlatView.class, HierarchicalView.class}) // both views
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
#JsonIdentityReference(alwaysAsId = true)
private Category parent;
#JsonView(FlatView.class) // flat view only!
#JsonInclude(JsonInclude.Include.NON_EMPTY)
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
#JsonIdentityReference(alwaysAsId = true)
private Set<Category> children;
#JsonView(HierarchicalView.class) // hierarchical view only!
// note that the name is not `getChildren`: this generates another JSON property.
// Or use #JsonProperty to customize it.
public Set<Category> getChildrenAsTree() {
return this.children;
}
// constructors, getters and setters
}
The controller methods producing the output should be annotated with respective #JsonViews.
Drawbacks:
This approach does not use the same property name for same information represented differently. This might cause some difficulties for property matching, deserialization at client side.
If the default view is still used somewhere for Category, it will contain both the field and the property accessed via the additional getter.
Redundant and repetitive annotations :) harder to maintain.
Redundant getter for children field.

I finally found solution. It is based on first version of Antot's answer and uses custom serializer for content of children collection. The tree producing endpoint is marked with #JsonView(Category.TreeView.class) annotation and unmarked field inclusion is turned ON
#application.properties
spring.jackson.mapper.default-view-inclusion=true
// Category.java
#JsonInclude(JsonInclude.Include.NON_EMPTY)
#JsonSerialize(contentUsing = CategoryChildSerializer.class)
private Set<Category> children;
// CategoryChildSerializer.java
public class CategoryChildSerializer extends JsonSerializer<Category> implements ResolvableSerializer {
private JsonSerializer<Object> defaultSerializer;
private JsonSerializer<Object> idSerializer;
public void serialize(Category value, JsonGenerator gen, SerializerProvider provider) throws IOException, JsonProcessingException {
if(provider.getActiveView() == Category.TreeView.class)
defaultSerializer.serialize(value, gen, provider);
else
idSerializer.serialize(value.getId(), gen, provider);
}
public void resolve(SerializerProvider provider) throws JsonMappingException {
defaultSerializer = provider.findValueSerializer(Category.class);
idSerializer = provider.findValueSerializer(Integer.class);
}
}
Note how switching serializer delegates serialization instead of directly using JsonGenerator. Implementing ResolvableSerializer is in fact optional, it's just optimization step.

Related

How to customize lombok's superbuilder?

I have an existing data model which was (unfortunately) written with bidirectional relationships. Currently, I'm trying to refactor it using Lombok. I've added the #SuperBuilder annotation, but the generated builder does not call my custom setter methods (the ones that ensure that the bidirectionality remain intact).
After running delombok and investigating the resulting code, it appears that a constructor is created on the class being built that takes an instance of the builder to use to set the values. Unfortunately, it simply assigns the field values directly. So I thought maybe I could just implement that constructor myself, and make the calls to the setters as required. Of course, this does not work. When I build I get an error because there are now apparently two implementations of that same method in my class (in other words SuperBuilder implemented it even though it was already implemented in the class).
Does anyone know how to override that constructor (or any other mechanism that would allow me to get the setters called when constructing my object using the SuperBuilder annotation)?
Edit: added code as requested
The entity class I'm trying to refactor to using lombok is:
#Entity
#Table(name = "APPLICATION_USER", uniqueConstraints = #UniqueConstraint(columnNames = { "PRINCIPAL_NAME", "APPLICATION", "SITE_ID" }))
#AttributeOverrides(#AttributeOverride(name = "id", column = #Column(name = "APP_USER_ID")))
#Filters({ #Filter(name = FilterQueryConstants.SITE_ID_FILTER_NAME, condition = FilterQueryConstants.SITE_ID_FILTER) })
#SuperBuilder
public class ApplicationUser extends User
{
private static final long serialVersionUID = -4160907033349418248L;
#Column(name = "APPLICATION", nullable=false)
private String application;
#ManyToMany(mappedBy = "applicationUsers", targetEntity = Group.class)
#Filters({ #Filter(name = FilterQueryConstants.GROUP_FILTER_NAME, condition = FilterQueryConstants.GROUP_FILTER),
#Filter(name = FilterQueryConstants.SITE_ID_FILTER_NAME, condition = FilterQueryConstants.SITE_ID_FILTER) })
#MappingTransform(operation = DTOSecurityOperation.ASSIGN_GROUP)
#Builder.Default
private Set groups = new HashSet ( );
// Other methods omitted for brevity
When I run the delombok, the resulting constructor looks like the following:
protected ApplicationUser(final ApplicationUserBuilder b) {
super(b);
this.application = b.application;
if (b.groups$set) this.groups = b.groups;
else this.groups = ApplicationUser.$default$groups();
}
So I thought I could just basically copy this code into my ApplicationUser class and modify it to call my setter method when it sets the value for groups (rather than just doing a direct assignment). I was thinking of something like this:
protected ApplicationUser(final ApplicationUserBuilder b) {
super(b);
this.application = b.application;
if (b.groups$set) this.setGroups(b.groups);
else this.setGroups(ApplicationUser.$default$groups());
}
Originally, when using 1.18.8, I was getting an error stating that this constructor already exists. Since updating to 1.18.22, I now get this:
error: cannot find symbol
if (b.groups$set) this.setGroups(b.groups);
^
symbol: variable groups
location: variable b of type ApplicationUserBuilder
Customizing #SuperBuilder only works in more recent lombok version; you should always use the most recent one, which is v1.18.22 at the time of the writing of this answer.
With that version, customizing a #SuperBuilder constructor is possible. However, you are using code as a basis for your constructor that has been delomboked with v1.18.8. That does not work any more with current lombok versions. lombok v1.18.10 introduced that the actual field value for #Default fields are stored in the builder in fields like fieldName$value, not simply fieldName.
Thus, your customized constructor has to look as follows:
protected ApplicationUser(final ApplicationUserBuilder<?, ?> b) {
super(b);
this.application = b.application;
if (b.groups$set) this.setGroups(b.groups$value);
else this.setGroups(ApplicationUser.$default$groups());
}

Spring Data JPA - bidirectional relation with infinite recursion

First, here are my entities.
Player :
#Entity
#JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class,
property="id")
public class Player {
// other fields
#ManyToOne
#JoinColumn(name = "pla_fk_n_teamId")
private Team team;
// methods
}
Team :
#Entity
#JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class,
property="id")
public class Team {
// other fields
#OneToMany(mappedBy = "team")
private List<Player> members;
// methods
}
As many topics already stated, you can avoid the StackOverflowExeption in your WebService in many ways with Jackson.
That's cool and all but JPA still constructs an entity with infinite recursion to another entity before the serialization. This is just ugly ans the request takes much longer. Check this screenshot : IntelliJ debugger
Is there a way to fix it ? Knowing that I want different results depending on the endpoint. Examples :
endpoint /teams/{id} => Team={id..., members=[Player={id..., team=null}]}
endpoint /members/{id} => Player={id..., team={id..., members=null}}
Thank you!
EDIT : maybe the question isn't very clear giving the answers I get so I'll try to be more precise.
I know that it is possible to prevent the infinite recursion either with Jackson (#JSONIgnore, #JsonManagedReference/#JSONBackReference etc.) or by doing some mapping into DTO. The problem I still see is this : both of the above are post-query processing. The object that Spring JPA returns will still be (for example) a Team, containing a list of players, containing a team, containing a list of players, etc. etc.
I would like to know if there is a way to tell JPA or the repository (or anything) to not bind entities within entities over and over again?
Here is how I handle this problem in my projects.
I used the concept of data transfer objects, implemented in two version: a full object and a light object.
I define a object containing the referenced entities as List as Dto (data transfer object that only holds serializable values) and I define a object without the referenced entities as Info.
A Info object only hold information about the very entity itself and not about relations.
Now when I deliver a Dto object over a REST API, I simply put Info objects for the references.
Let's assume I deliever a PlayerDto over GET /players/1:
public class PlayerDto{
private String playerName;
private String playercountry;
private TeamInfo;
}
Whereas the TeamInfo object looks like
public class TeamInfo {
private String teamName;
private String teamColor;
}
compared to a TeamDto
public class TeamDto{
private String teamName;
private String teamColor;
private List<PlayerInfo> players;
}
This avoids an endless serialization and also makes a logical end for your rest resources as other wise you should be able to GET /player/1/team/player/1/team
Additionally, the concept clearly separates the data layer from the client layer (in this case the REST API), as you don't pass the actually entity object to the interface. For this, you convert the actual entity inside your service layer to a Dto or Info. I use http://modelmapper.org/ for this, as it's super easy (one short method call).
Also I fetch all referenced entities lazily. My service method which gets the entity and converts it to the Dto there for runs inside of a transaction scope, which is good practice anyway.
Lazy fetching
To tell JPA to fetch a entity lazily, simply modify your relationship annotation by defining the fetch type. The default value for this is fetch = FetchType.EAGER which in your situation is problematic. That is why you should change it to fetch = FetchType.LAZY
public class TeamEntity {
#OneToMany(mappedBy = "team",fetch = FetchType.LAZY)
private List<PlayerEntity> members;
}
Likewise the Player
public class PlayerEntity {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "pla_fk_n_teamId")
private TeamEntity team;
}
When calling your repository method from your service layer, it is important, that this is happening within a #Transactional scope, otherwise, you won't be able to get the lazily referenced entity. Which would look like this:
#Transactional(readOnly = true)
public TeamDto getTeamByName(String teamName){
TeamEntity entity= teamRepository.getTeamByName(teamName);
return modelMapper.map(entity,TeamDto.class);
}
In my case I realized I did not need a bidirectional (One To Many-Many To One) relationship.
This fixed my issue:
// Team Class:
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<Player> members = new HashSet<Player>();
// Player Class - These three lines removed:
// #ManyToOne
// #JoinColumn(name = "pla_fk_n_teamId")
// private Team team;
Project Lombok might also produce this issue. Try adding #ToString and #EqualsAndHashCode if you are using Lombok.
#Data
#Entity
#EqualsAndHashCode(exclude = { "members"}) // This,
#ToString(exclude = { "members"}) // and this
public class Team implements Serializable {
// ...
This is a nice guide on infinite recursion annotations https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion
You can use #JsonIgnoreProperties annotation to avoid infinite loop, like this:
#JsonIgnoreProperties("members")
private Team team;
or like this:
#JsonIgnoreProperties("team")
private List<Player> members;
or both.

How to get Column names of JPA entity

All my JPA entity classes implement an interface called Entity which is defined like this:
public interface Entity extends Serializable {
// some methods
}
Some of the fields of my JPA entity have #Column annotation on top of them and some don't. MyEntity class is defined like below:
#Entity
public class MyEntity implements Entity {
#Id
private Long id; // Assume that it is auto-generated using a sequence.
#Column(name="field1")
private String field1;
private SecureString field2; //SecureString is a custom class
//getters and setters
}
My delete method accepts an Entity.
#Override
public void delete(Entity baseEntity) {
em.remove(baseEntity); //em is entityManager
}
Whenever the delete method is invoked I want three things inside my delete method:
1) Fields of MyEntity that are of type SecureString
2) Column name of that particular field in DB (The field may or may not have #Column annotation)
3) The value of id field
Note that when the delete() method is invoked, we don't know for which entity it is invoked, it may be for MyEntity1, MyEntity2 etc.
I have tried doing something like below:
for (Field field : baseEntity.getClass().getFields()) {
if (SecureString.class.isAssignableFrom(field.getType())) {
// But the field doesn't have annotation #Column specified
Column column = field.getAnnotation(Column.class);
String columnName = column.name();
}
}
But this will only work if the field has #Column annotation. Also it doesn't get me other two things that I need. Any ideas?
Hibernate can use different naming strategies to map property names, which are defined implicitly (without #Column(name = "...")). To have a 'physical' names you need to dive into Hibernate internals. First, you have to wire an EntityManagerFactory to your service.
#Autowired
private EntityManagerFactory entityManagerFactory;
Second, you have to retrieve an AbstractEntityPersister for your class
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
AbstractEntityPersister persister = ((AbstractEntityPersister)sessionFactory.getClassMetadata(baseEntity.getClass()));
Third, you're almost there with your code. You just have to handle both cases - with and without #Column annotation. Try this:
for (Field field : baseEntity.getClass().getFields()) {
if (SecureString.class.isAssignableFrom(field.getType())) {
String columnName;
if (field.isAnnotationPresent(Column.class)) {
columnName = field.getAnnotation(Column.class).name();
} else {
String[] columnNames = persister.getPropertyColumnNames(field.getName());
if (columnNames.length > 0) {
columnName = columnNames[0];
}
}
}
}
Note that getPropertyColumnNames() retrieves only 'property' fields, that are not a part of primary key. To retrieve key column names, use getKeyColumnNames().
And about id field. Do you really need to have all #Id's in child classes? Maybe would better to move #Id to Entity class and mark this class with #MappedSuperclass annotation? Then you can retrieve it just with baseEntity.getId();

Jackson: referencing an object as a property

In my java spring application, I am working with hibernate and jpa, and i use jackson to populate data in DB.
Here is the User class:
#Data
#Entity
public class User{
#Id
#GeneratedValue
Long id;
String username;
String password;
boolean activated;
public User(){}
}
and the second class is:
#Entity
#Data
public class Roles {
#Id
#GeneratedValue
Long id;
#OneToOne
User user;
String role;
public Roles(){}
}
In the class Roles i have a property of User
and then i made a json file to store the data:
[ {"_class" : "com.example.domains.User", "id": 1, "username": "Admin", "password": "123Admin123","activated":true}
,
{"_class" : "com.example.domains.Roles", "id": 1,"user":1, "role": "Admin"}]
Unfortunately, when i run the app it complains with:
.RuntimeException: com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.example.domains.User: no int/Int-argument constructor/factory method to deserialize from Number value (1)
at [Source: N/A; line: -1, column: -1] (through reference chain: com.example.domains.Roles["user"])
The problem comes from
{"_class" : "com.example.domains.Roles", "id": 1,"user":1, "role": "Admin"}
and when i remove the above line the app works well.
I think, it complains because it cannot make an instance of user.
So, how can i fix it?
Do yourself a favor and stop using your Entities as DTOs!
JPA entities have bidirectional relations, JSON objects don't, I also believe that the responsibilities of an Entity is very different from a DTO, and although joining these responsibilities into a single Java class is possible, in my experience it is a very bad idea.
Here are a couple of reasons
You almost always need more flexibility in the DTO layer, because it is often related to a UI.
You should avoid exposing primary keys from your database to the outside, including your own UI. We always generate an additional uniqueId (UUID) for every publicly exposed Entity, the primary key stays in the DB and is only used for joins.
You often need multiple views of the same Entity. Or a single view of multiple entities.
If you need to add a new entity to a relation with an existing, you will need find the existing one in the database, so posting the new and old object as a single JSON structure has no advantage. You just need the uniqueId of the existing, and then new.
A lot of the problems developers have with JPA, specifically with regards to merging comes from the fact that they receive a detached entity after their json has been deserialized. But this entity typically doesn't have the OneToMany relations (and if it does, it's the parent which has a relation to the child in JSON, but in JPA it is the child's reference to the parent which constitutes the relationship). In most cases you will always need to load the existing version of the entity from the database, and then copy the changes from your DTO into the entity.
I have worked extensively with JPA since 2009, and I know most corner cases of detachment and merging, and have no problem using an Entity as a DTO, but I have seen the confusion and types of errors that occur when you hand such code over to some one who is not intimately familiar with JPA. The few lines you need for a DTO (especially since you already use Lombok), are so simple and allows you much more flexibility, than trying to save a few files and breaking the separation of concerns.
Jackson provide ObjectIdResolver interface for resolving the objects from ids during de-serialization.
In your case you want to resolve the id based from the JPA/hibernate. So you need to implement a custom resolver to resolve id by calling the JPA/hierbate entity manager.
At high level below are the steps:
Implement a custom ObjectIdResolver say JPAEntityResolver (you may extends from SimpleObjectIdResolver). During resolving object it will call JPA entity manager class to find entity by given id and scope(see. ObjectIdResolver#resolveId java docs)
//Example only;
#Component
#Scope("prototype") // must not be a singleton component as it has state
public class JPAEntityResolver extends SimpleObjectIdResolver {
//This would be JPA based object repository or you can EntityManager instance directly.
private PersistentObjectRepository objectRepository;
#Autowired
public JPAEntityResolver (PersistentObjectRepository objectRepository) {
this.objectRepository = objectRepository;
}
#Override
public void bindItem(IdKey id, Object pojo) {
super.bindItem(id, pojo);
}
#Override
public Object resolveId(IdKey id) {
Object resolved = super.resolveId(id);
if (resolved == null) {
resolved = _tryToLoadFromSource(id);
bindItem(id, resolved);
}
return resolved;
}
private Object _tryToLoadFromSource(IdKey idKey) {
requireNonNull(idKey.scope, "global scope does not supported");
String id = (String) idKey.key;
Class<?> poType = idKey.scope;
return objectRepository.getById(id, poType);
}
#Override
public ObjectIdResolver newForDeserialization(Object context) {
return new JPAEntityResolver(objectRepository);
}
#Override
public boolean canUseFor(ObjectIdResolver resolverType) {
return resolverType.getClass() == JPAEntityResolver.class;
}
}
Tell Jackson to use a custom id resolver for a class, by using annotation JsonIdentityInfo(resolver = JPAEntityResolver.class). For e.g.
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
scope = User.class,
resolver = JPAObjectIdResolver.class)
public class User { ... }
JPAObjectIdResolver is a custom implementation and will have dependency on other resources( JPA Entity Manager) which might not be known to Jackson. So Jackson need help to instantiate resolver object. For this purpose, you need to supply a custom HandlerInstantiator to ObjectMapper instance. (In my case I was using spring so I asked spring to create instance of JPAObjectIdResolver by using autowiring)
Now de-serialization should work as expected.
Hope this helps.
I have changed the json file to :
[
{"_class" : "com.example.domains.User",
"id": 1,
"username": "Admin",
"password": "123Admin123",
"activated":true
},
{
"_class" : "com.example.domains.Roles",
"id": 1,
"user":{"_class" : "com.example.domains.User",
"id": 1,
"username": "Admin",
"password": "123Admin123",
"activated":true
},
"role": "Admin"
}
]
But i still think, the best ways is using a foreign key to user record.
Any solution is welcomed
If your bean doesn't strictly adhere to the JavaBeans format, Jackson has difficulties.
It's best to create an explicit #JsonCreator constructor for your JSON model bean, e.g.
class User {
...
#JsonCreator
public User(#JsonProperty("name") String name,
#JsonProperty("age") int age) {
this.name = name;
this.age = age;
}
..
}
1-1 mapping of fields works well , but when it comes to complex object mapping , better to use some API.
You can use Dozer Mapping or Mapstruct to map Object instances.
Dozer has spring integration also.
You could specify non default constructors and then use a custom deserialiser.
Something like this should work (it has not been tested).
public class RolesDeserializer extends StdDeserializer<Roles> {
public RolesDeserializer() {
this(null);
}
public RolesDeserializer(Class<?> c) {
super(c);
}
#Override
public Roles deserialize(JsonParser jp, DeserializationContext dsctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
long id = ((LongNode) node.get("id")).longValue();
String roleName = node.get("role").asText();
long userId = ((LongNode) node.get("user")).longValue();
//Based on the userId you need to search the user and build the user object properly
User user = new User(userId, ....);
return new Roles(id, roleName, user);
}
}
Then you need to register your new deserialiser (1) or use the #JsonDeserialize annotation (2)
(1)
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Item.class, new RolesDeserializer());
mapper.registerModule(module);
Roles deserializedRol = mapper.readValue(yourjson, Roles.class);
(2)
#JsonDeserialize(using = RolesDeserializer.class)
#Entity
#Data
public class Roles {
...
}
Roles deserializedRol = new ObjectMapper().readValue(yourjson, Roles.class);
public class Roles {
#Id
#GeneratedValue
Long id;
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
#JsonIdentityReference(alwaysAsId = true)
#OneToOne
User user;
String role;
public Roles(){}
}

Java - How to avoid creation of setter only for a particular class needs?

I am using Hibernate and currently using the setter to set the relation to parent in children at creation time (to avoid doing this manually for both sides). How I can avoid use of setter or avoid expose it to the rest of classes and get the same behaviour. Is it ok to use reflection? This is the code:
#Entity
#Table(name = "TEST_GROUP")
#Getter
public class TestGroupEntity extends AuditedEntity{
#ManyToOne
#JoinColumn(name = "owner", nullable = false)
protected UserEntity owner;
#Column(name = "description")
#Setter
protected String description;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
protected Set<TestEntity> tests = Sets.newHashSet();
public boolean addTest(TestEntity testEntity) {
return tests.add(testEntity);
}
public boolean removeTest(TestEntity testEntity) {
return tests.remove(testEntity);
}
public TestGroupEntity(UserEntity owner, Set<TestEntity> tests) {
this.owner = owner;
owner.setTestGroupEntity(this); ! how to avoid creation of setter
this.tests = tests;
tests.stream().forEach(t -> t.setTestGroupEntity(this)); ! how to avoid creation of setter
}
}
This is the children class ( I would like to keep immutability on api level):
#MappedSuperclass
#AllArgsConstructor
public class TestEntity extends AuditedEntity {
#Column(name = "name", nullable = false)
protected String name;
#Column(name = "description")
protected String description;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "test_group", nullable = false)
protected TestGroupEntity testGroupEntity;
public void setTestGroupEntity(TestGroupEntity testGroupEntity) {
this.testGroupEntity = testGroupEntity;
}
}
Edit: I think commented sections of code was not visible. Sorry.
How I can avoid use of setter or avoid expose it to the rest of
classes and get the same behaviour. Is it ok to use reflection?
Of course you can for example reduce visibility of public setters to a visibility less wide than public in order that client classes of your entities cannot use them.
Which is in your case the real problem since accessing any data from inside the object is possible in anyway
From hibernate doc :
Attributes (whether fields or getters/setters) need not be declared
public. Hibernate can deal with attributes declared with public,
protected, package or private visibility. Again, if wanting to use
runtime proxy generation for lazy loading the visibility for the
getter/setter should be at least package visibility.
So, try to use private setter for desired field. It should address your problem.
Update After comment
You have several workarounds to address your problem :
using reflection (your basic idea).
Drawback : it brings a little complexity, not a full check at compile-time and at last, someone who sees your code could wonder why you used that...
It is the same thing for any concepts which relies on reflection such as AOP.
declaring these setters with package-private level and put the 3 classes in the same package. Drawback : the used package.
creating public init methods which raises an exception if it used more than once for a same object. In this way, you guarantee the coherence of the object if bad used. Drawback : method which should not be used by clients is still provided to clients.
Unfortunately, you have not a perfect solution since Java visibility mechanisms cannot provide a ready-to-use solution for what you are looking for.
Personally, I prefer reflection or init method solutions.
Personally, I noticed that in based-class languages as Java, a good developer has often the reflex to over- protect accessibility of objects/methods. In fact, in many cases, it is not needed because it will not break the application or data integrity.
Here an example with init method :
public TestGroupEntity(UserEntity owner, Set<TestEntity> tests) {
this.owner = owner;
owner.constructInit(this);
this.tests = tests;
tests.stream().forEach(t -> t.constructInit(this));
}
public class UserEntity {
private TestGroupEntity testGroupEntity;
public void constructInit(TestGroupEntity testGroupEntity) {
if (this.testGroupEntity != null) {
throw new IllegalArgumentException("forbidden");
}
this.testGroupEntity=testGroupEntity;
}
}
Make a constructor in your parent class and call it from child.
Here is the parent constructor looks like
public AuditedEntity(UserEntity owner, Set<TestEntity> tests){
this.owner = owner;
this.tests = tests;
}
And change your child constructor like
public TestGroupEntity(UserEntity owner, Set<TestEntity> tests) {
super(owner,tests);
}

Categories

Resources