I have two entities with fields that I´d like to localize. However, I´m not sure how to implement that correctly, because I would need to have a reference to the entities as well as a reference to the field that is translated, in order to have a shared "i18n" table.
#Entity
public class EntityA {
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<Translation> name;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<Translation> description;
}
Second entity
#Entity
public class EntityB {
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<Translation> name;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<Translation> shortDescription;
}
Translation Entity
#Entity
#Table(name = "i18n")
public class Translation {
private String languageCode;
private String translation;
//private String referenceToEntity
//private String referenceToField
}
Is there a given way to enable internationalization on entity fields in Spring or at least some kind of workaround to make it working without too much overhead?
EDIT
I´ve written a short post about how I solved it using XmlAnyAttribute https://overflowed.dev/blog/dynamical-xml-attributes-with-jaxb/
I did some research and found this #Convert JPA annotation. You would need to encapsulate the name and description properties into an object (that implements AttributeConverter), and use a convertion class to specify how it will be translated when persisted, and how will it be translated when retreived.
To execute translations on persistence and retrieval, you can consume Google translate's API.
Here:
#Entity
public class EntityA {
#Convert(converter = DescriptionConverter.class)
private Description description
// getters and setters
},
The encapsulated object, something like:
public class Description {
private String name;
private String language;
private String description;
// Getters and Setters.
}
And the translation applies here:
#Converter
public class DescriptionConverter implements AttributeConverter<Description, String> {
#Override
public String convertToDatabaseColumn(Description description) {
// consume Google API to persist.
}
#Override
public Document convertToEntityAttribute(String description) {
// consume Google API to retrieve.
}
}
this tutorial helped me a lot. i hope it will help you too. i used the second way and it's work perfectly.Localized Data – How to Map It With Hibernate
Related
Lets say I have an entity like this,
#Entity(name = "Post")
#Table(name = "post")
public class Post {
#Id
#GeneratedValue
private Long id;
private String title;
#OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true,
FetchType.LAZY
)
#JsonManagedReference
private List<PostComment> comments = new ArrayList<>();
#OneToOne(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY
)
private PostDetails details;
}
I want clone this entity, modified a few fields and then persist the new cloned entity in the database. What's the best approach to achieve this?
You can use a copy using constructor or the builder pattern, or to set the properties using simple setters. Then, persist the new entity instance using the EntityManager's persist() method. To avoid issues with duplicated id values, you must not copy the id field and instead let the JPA generate a new id when the entity is persisted.
Code example
public class Post {
// constructor, getters and setters are omitted
public Post(Post post) {
//Don’t copy id!
this.title = post.getTitle();
this.comments = new ArrayList<>(post.getComments());
this.details = new PostDetails(post.getDetails());
}
}
public class PostDetails {
// constructor, getters and setters are omitted
public PostDetails(PostDetails details) {
// Don’t copy id!
this.description = details.getDescription();
// copy other fields as needed
}
}
// Usage
public class PostsRepository{
#Autowired
EntityManager em;
public void saveCopy(Post originalPost){
Post clonedPost = new Post(originalPost);
em.persist(clonedPost);
}
}
Also I would highly recommend you to write tests in order to check that you are doing everything fine. Moreover you will more space for experiments.
I am making a Spring Boot backend, and I have the following problem. When I get a Software from VersionableFileRepository and call the getSystem function on that I get the actual System within the relationship. But when I get a Documentation from VersionableFileRepository its getSystem function returns null. I handle the Software and Documentation in the same way, and all instance of these have a System.
Illustrated with code:
versionableFileRepository.findById(fileId).get().getSystem() returns a valid System when fileId identify a Software and returns null when a Documentation
What's wrong? Did I mess something up in the implementation?
I have the following classes:
#Entity
public class System {
#Id
#GeneratedValue
private long id;
private String name;
#OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true)
#JoinColumn(name = "software_id", referencedColumnName = "id")
private Software software;
#OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true)
#JoinColumn(name = "documentation_id", referencedColumnName = "id")
private Documentation documentation;
//other fields, getters and setters...
}
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class VersionableFile {
#Id
#GeneratedValue
private long id;
#OneToMany(mappedBy = "file", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<FileVersion> versions = new ArrayList<>();
public abstract System getSystem();
public abstract void setSystem(System system);
//getters and setters...
}
#Entity
public class Software extends VersionableFile {
#OneToOne(mappedBy = "software")
#JsonIgnore
private System system;
#Override
public System getSystem() {
return system;
}
#Override
public void setSystem(System system) {
this.system = system;
}
}
#Entity
public class Documentation extends VersionableFile {
#OneToOne(mappedBy = "documentation")
#JsonIgnore
private System system;
#Override
public System getSystem() {
return system;
}
#Override
public void setSystem(System system) {
this.system = system;
}
}
#Repository
public interface VersionableFileRepository extends CrudRepository<VersionableFile, Long> {
}
Database:
Everything looks good in the database, this is the system table:
And the corresponding objects can be found in the other two tables (software and documentation). Furthermore the appropriate constraints are also defined.
I think this is a JPA issue, because when I get a System object from SystemRepository (not mentioned here) it has the right software and documentation fields.
Thank you in advance for your help!
Have already commented but looking better I think I found something major here.
Proposal 1
Your Entities structure seems good to me. However you have a major Issue with your java code to retrieve those entities back from database.
versionableFileRepository.findById(fileId).get().getSystem()
fileId as well as documentId are plain Long numbers. How would JPA know if you want to retrieve a Software or a Documentation? This will not work. As you have constructed it, it will have separate tables Documentation and Software and each one of those will have a column Id as primary key.
Make it easier for JPA by using specific repositories
#Repository
public interface SoftwareRepository extends CrudRepository<Software, Long> {
}
Then to retrieve software just use softwareRepository.findById(id).get().getSystem()
And
#Repository
public interface DocumentationRepository extends CrudRepository<Documentation, Long> {
}
Then to retrieve documentation just use documentationRepository.findById(id).get().getSystem()
Proposal 2
If you wish to go along the way you are going then I would consider that the error is specifically on your ids that are generated. You want different tables in your case Documentation and Software to have distinct Ids. Then JPA could distinct from the Id what entity you have.
To achieve that you have to change the strategy of generating Ids
public abstract class VersionableFile {
#Id
#GeneratedValue( strategy = GenerationType.TABLE)
private long id;
....
I am new to spring and while fetching records from a table having relationship with other tables getting this lazily initialling error.
I have read a lot online but not getting a appropriate approach.
Table1:
#SuppressWarnings("serial")
#Entity
public class Terminal extends BaseEntity {
#Column(length = 100, unique = true)
private String shortName;
#Column
private short number; // short stores up to 32767 value
#Column
private String description;
#OneToMany
#JoinColumn(name = "terminal_id", referencedColumnName = "uuid")
#Cascade({ CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DELETE })
private Set<BusinessHour> businessHour;
public String getShortName() {
return shortName;
}
public void setShortName(String shortName) {
this.shortName = shortName;
}
public short getNumber() {
return number;
}
public void setNumber(short number) {
this.number = number;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Set<BusinessHour> getBusinessHour() {
return businessHour;
}
public void setBusinessHour(Set<BusinessHour> businessHour) {
this.businessHour = businessHour;
}
Table2:
#SuppressWarnings("serial")
#Entity
public class BusinessHour extends BaseEntity {
#Column
private DayOfWeek dayOfWeek;
#Column
private LocalTime startOfOperation;
#Column
private LocalTime endOfOperation;
public DayOfWeek getDayOfWeek() {
return dayOfWeek;
}
}
Service Code:
#Service
public class TerminalServiceImpl implements TerminalService {
#Autowired
TerminalRepository terminalRepository;
Iterable<Terminal> allTerminals = terminalRepository.findAll();
List<Terminal> terminalList = new ArrayList<Terminal>();
for (Terminal terminal : allTerminals) {
terminalList.add(terminal);
}
return terminalList;
}
Terminal Repository code:
#Transactional
public interface TerminalRepository extends CrudRepository<Terminal, Long> {
}
Code where i got error during debug:
private List<Terminal> updateTerminalList() {
List<Terminal> allTerminals = terminalService.fetchAllTerminal();
return allTerminals;
}
public void terminalWrapperRun() {
try {
Payload payload = createTerminalPayload(applicationId);
String json3 = object2Json(payload);
kafkaRESTUtils.sendServerPayload(json3);
} catch (Exception e1) {
e1.printStackTrace();
}
}
public String object2Json(Object dataArray) throws JsonProcessingException {
return mapper.writeValueAsString(dataArray);
}
Error:
com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role: terminal.model.Terminal.businessHour, could not initialize proxy - no Session (through reference chain:
Getting exception while converting fetching object to json. which i found due to proxy object return due to fetch type lazy(which i want to kept as it is).
I believe this issue relates to the LAZY loading of Collections by default by your ORM.
#OneToMany
#JoinColumn(name = "terminal_id", referencedColumnName = "uuid")
#Cascade({ CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DELETE })
private Set<BusinessHour> businessHour;
The #OneToMany annotation has a fetch property which is set to LAZY by default.
FetchType fetch() default LAZY;
OneToMany reference
This means that it will only be loaded when the data is accessed. In the case of your example, this will happen when you try to create the JSON string. By this point, however, you are outside the scope of the ORM session so it does not know how to load the data.
Therefore you have 2 options.
Change your annotation to eagerly load the data (which means the BusinessHour Set will be loaded at the same time as the parent object
#OneToMany(fetch = FetchType.EAGER)
perform your JSON generation within an ORM session (I would only really recommend doing this is the first option causes performance issues).
If I recall correctly this is the kind of error caused by an Entity being detached from the EntityManager at the time of its use (being it a Proxy it cannot perform a database query to retrive the data).
You can use:
#OneToMany(fetch = FetchType.EAGER)
...
private Set<BusinessHour> businessHour;
Using FetchType=EAGER means that any query against your entity will load the whole bunch of annotated entities.
Imho, this is only a sensible action if you are 100% sure that your entity will only be used for your special business case.
In all other cases - like programming a data acess as a library, or accepting different kinds of queries on your entities, you should use entity graphs (https://docs.oracle.com/javaee/7/tutorial/persistence-entitygraphs002.htm) or explicit loading (Hibernate.initialize, join-fetch, see for example https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/).
If your use case only is the transformation, you have two good options:
Transform your entity to JSON within a Transactional method (as PillHead suggested)
Load your entity explicitly with all the entities needed (via entity graphs or Hibernate.initialize) within the transaction, and then convert to JSON where you need it.
I am working on a Restful service built with Java Spring and I have some issues modeling the data. I want to store shelfs with books. The books belong to a given category. I have a POST request to store shelfs to a mysql database (via service and CrudRepository). However I am not able to store more than one book of the same category. Here are my (simplified) entities.
A Shelf with an id and a collection of books.
#Entity
public class Shelf{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "shelf")
private List<Book> books= new ArrayList<>();
...
}
The class Book is defined as follows:
#Entity
public class Book{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name = "category_id")
private Category category;
#OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JsonIgnore
private Shelf shelf;
Each book belongs to a category(e.g. thriller, fiction, etc.). Here is the category entity:
#Entity
public class Category {
private Long id;
private String name;
And finally my Controller:
#RestController
public class ShelfController {
#Autowired
private ShelfService shelfService;
#PostMapping("/shelfs")
public Shelf addShelf(#RequestBody Shelf shelf) {
return shelfService.addShelf(shelf);
}
Now here is my problem: The categories will be given and there will be no option to change these, I would therefore like to have them stored in the database or hard code them as static objects. In the Post request for new shelfs I would like to provide only the category id and make the controller find the corresponding object itself.
What I did so far was to treat the categories as a usual Entity, so whenever I added a new shelf with books having a category_id, the category was created with the given id and an empty name. But as soon as I used the same category id again, the application threw a com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '1' for key 'PRIMARY' exception. I don't want the controller to create new category objects, but instead want it to fetch the corresponding objects from a service or a static Collection.
So my question is: How can I achieve this?
Hints for solutions/tricks to improve the design are most welcome, I am new to the topic.
One solution is to create Data Transfer Objects (DTO). Example:
class ShelfDTO {
private Long id;
private List<Long> bookIds;
}
Then use this class to receive the POST requests:
#RestController
public class ShelfController {
#Autowired private ShelfService shelfService;
#PostMapping("/shelfs")
public Shelf addShelf(#RequestBody ShelfDTO shelfDto) {
return shelfService.addShelf(shelfDto);
}
}
Then modify your ShelfService to convert the DTO to an Entity:
#Service
public class ShelfService {
#Autowired private ShelfRepository shelfRepository;
#Autowired private BookRepository bookRepository;
#Transactional
public Shelf addShelf(ShelfDTO shelfDto) {
List<Book> books = bookRepository.findAllById(shelfDto.getBookIds());
return shelfService.addShelf(new Shelf(books));
}
}
Final comment: I noticed that you have a bidirectional relationship. You are responsible for keeping it in a consistent state.
The easiest way is to create the methods addTo(shelf, book) and removeFrom(shelf, book) that encapsulate the logic of both adding the book to the list in the shelf and setting the shelf in the book.
I'm building a Spring Data REST / Spring HATEOAS based application and I'm attempting to following the principles of DDD outlined here (and elsewhere):
BRIDGING THE WORLDS OF DDD & REST - Oliver Gierke
In particular the concept of aggregates and complex state changes via dedicated resources.
Also avoid using HTTP PATCH or PUT for (complex) state transitions of your business domain because you are missing out on a lot of information regarding the real business domain event that triggered this update. For example, changing a customer’s mailing address is a POST to a new "ChangeOfAddress" resource, not a PATCH or PUT of a “Customer” resource with a different mailing address field value.
What I'm struggling with is a means of enforcing this while allowing cosmetic changes to the aggregate root.
Using this simplified example:
#Entity
public class Customer
{
private #Id #GeneratedValue(strategy = GenerationType.AUTO) Long id;
private String name;
private String comment;
#Access(AccessType.PROPERTY)
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Address> addresses = new HashSet<>();
... getters and setters
public void addAddress(Address address)
{
addresses.add(address);
... custom code to raise events etc
}
}
public interface Customer extends CrudRepository<Customer, Long>
{
}
What is the best/correct way to allow a cosmetic change (e.g. update comment) but but prevent changes that directly update the child collection?
The only thing I can think of doing if having the setter throw an exception if there is an attempt to modify the child collection.
#Entity
public class Customer
{
private #Id #GeneratedValue(strategy = GenerationType.AUTO) Long id;
private String name;
private String comment;
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
#Access(AccessType.PROPERTY)
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Address> addresses = new HashSet<>();
}