Spring Boot - Data JPA - download Large Lob files from DB - java

I have written a spring boot web/rest application to handle a legacy system (DB tables), this application does the following:
user create a bill request through a REST WS.
My rest app store the request to Legacy DB Tables.
another system(X) (out my control) reads the record and generate a zip file.
X system then store the file in our table (zip file and file name).
user can now download the file using my application.
the issue here some bill files are large (1-2 GB) and I'm getting:
java.lang.OutOfMemoryError: Java heap space
this happens in Hibernate when I'm trying to select the file content
now how can I let the user download a large file without getting above error? please check below for POJO classes and Repos
Entity Class
#Entity
public class EbillStatus implements Serializable {
private static final long serialVersionUID = 1L;
#Id #GeneratedValue(generator="system-uuid")
#GenericGenerator(name="system-uuid", strategy="uuid")
#Column(name="EBILL_STATUS_ID")
private String id;
#Column(name="CORPORATE_EBILL_ID", insertable=false, updatable=false)
private String corporateEbillId;
#Temporal(TemporalType.TIMESTAMP)
#Column(name="COMPLETION_DATETIME")
private Date completionDatetime;
#Column(name="FILENAME", insertable=false, updatable=false)
private String filename;
#Lob
private byte[] fileContent;
#Column(name="USER_ID")
private String username;
#Column(name="STATUS_ID")
private Integer status;
#Column(name="BILL_COUNT", insertable=false, updatable=false)
private Integer billCount;
private Integer accountCount;
}
The Repository:
public interface EbillStatusRepository extends JpaRepository<EbillStatus, String> {
EbillStatus findByCorporateEbill(CorporateEbill corporateEbill);
#Query(value="select s FROM EbillStatus s WHERE s.corporateEbillId=?1")
EbillStatus findFile(String corporateEbillId);
}
Service Method Implementation:
#Service
public class EbillServiceImpl implements EbillService {
private EbillStatusRepository statusRepo;
#Autowired
public EbillServiceImpl(EbillStatusRepository statusRepo){
this.statusRepo = statusRepo;
}
#Override
public EbillFileModel getEbillFile(String username, String eBillId) throws NotFoundException{
EbillStatus ebill= statusRepo.findFile(eBillId);
if(ebill == null || !isEbillFileAvailable(ebill.getStatus()))
throw new NotFoundException("file not available for username: "+username+", eBillId: "+ eBillId);
return new EbillFileModel(ebill.getFilename(), ebill.getFileContent());
}
private boolean isEbillFileAvailable(Integer status){
return COMPLETED.equals(String.valueOf(status)) || DELIVERED.equals(String.valueOf(status));
}
}
Pojo Class which is given to the controller
public class EbillFileModel {
private String fileName;
private byte[] fileContent;
public EbillFileModel(String fileName, byte[] fileContent) {
super();
this.fileName = fileName;
this.fileContent = fileContent;
}
public EbillFileModel() {
super();
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public byte[] getFileContent() {
return fileContent;
}
public void setFileContent(byte[] fileContent) {
this.fileContent = fileContent;
}
}
Controller method to Download the file
#GetMapping(value = "/{username}/order/{eBillId}/download", produces = "application/zip")
public ResponseEntity<ByteArrayResource> downloadEbillFile(#PathVariable String username, #PathVariable String eBillId)
throws IOException, NotFoundException {
EbillFileModel file = ebillservice.getEbillFile(username, eBillId);
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL_VAL);
headers.add(HttpHeaders.PRAGMA,PRAGMA_VAL);
headers.setContentDispositionFormData(ATTACHMENT, file.getFileName());
headers.add(HttpHeaders.EXPIRES, ZERO);
return ResponseEntity
.ok()
.headers(headers)
.contentType(APPLICATION_ZIP)
.body(new ByteArrayResource(file.getFileContent()));
}
Summary and Notes:
I cannot change the system design and how it works, I just wrote a Web/REST application on top of it.
I'm using Spring boot 1.5.1.RELEASE and java 8
the packaging is a WAR file deployed on JBoss EAP 7
what is the best way to download large files stored in DB.

Related

Cloudinary image upload not persisting image and returning null value in spring boot app

I'm working on a spring boot ecommerce app that requires cloudinary to persist image and get using the url.
However, all effort to get this done has been proved abortive. The code is not throwing any error but its not persisting in the cloudinary page and the database. And the response is null.
This is a response for example. Meanwhile i expect a link in the form of String
{
"productName": "Track suit",
"price": 300,
"productDescription": "XXL",
"productImage1": "",
"productImage2": "",
"productImage3": ""
}
This is my code
ENTITY
#Entity
public class Product {
#Id
#GeneratedValue(generator="system-uuid")
#GenericGenerator(name="system-uuid", strategy = "uuid")
private String id;
#Column
private String productName;
#Column
private double price;
#Column
private String productDescription;
#Column(nullable = false)
private String productImage1;
#Column(nullable = true)
private String productImage2;
#Column(nullable = true)
private String productImage3;
private LocalDateTime createdDate;
private LocalDateTime updatedDate;
#ManyToOne
#JoinColumn(name = "admin_id")
private Admin admin;
#ManyToOne
#JoinColumn(name = "category_id")
private Category category;
#ManyToOne
#JoinColumn(name = "users_entity_id")
private UsersEntity usersEntity;
}
REQUEST DTO
#Data
public class UploadProductRequestDto {
private String productName;
private double price;
private String productDescription;
private MultipartFile productImage1;
private MultipartFile productImage2;
private MultipartFile productImage3;
}
RESPONSE DTO
#Data
public class UploadProductResponseDto {
private String productName;
private double price;
private String productDescription;
private String productImage1;
private String productImage2;
private String productImage3;
}
REPOSITORY
public interface ProductRepository extends JpaRepository<Product,String> {
Optional<Product> findByProductName(String productName);
}
SERVICE
public interface ProductService {
UploadProductResponseDto uploadProducts(UploadProductRequestDto uploadProductRequestDto, String categoryName) throws AuthorizationException, GeneralServiceException, ImageUploadException;
}
SERVICEIMPL
#Slf4j
#Service
public class ProductServiceImpl implements ProductService {
#Autowired
CloudStorageService cloudStorageService;
#Autowired
AdminRepository adminRepository;
#Autowired
CategoryRepository categoryRepository;
#Autowired
PasswordEncoder passwordEncoder;
#Autowired
ModelMapper modelMapper;
#Autowired
UserPrincipalService userPrincipalService;
#Autowired
UserRepository userRepository;
#Autowired
ProductRepository productRepository;
#Override
public UploadProductResponseDto uploadProducts(UploadProductRequestDto uploadProductRequestDto, StringcategoryName) throws AuthorizationException, GeneralServiceException, ImageUploadException {
Optional<Category> checkCategory = categoryRepository.findByCategoryName(categoryName);
if (checkCategory.isEmpty()){
throw new AuthorizationException(CATEGORY_NOT_RECOGNIZED);
}
Product product = new Product();
product=mapAdminRequestDtoToProduct(uploadProductRequestDto,product);
productRepository.save(product);
UploadProductResponseDto adminUploadProductResponseDto = packageAdminProductUploadResponseDTO(product);
return adminUploadProductResponseDto;
}
private UploadProductResponseDto packageAdminProductUploadResponseDTO(Product product){
UploadProductResponseDto uploadProductResponseDto=new UploadProductResponseDto();
modelMapper.map(product,uploadProductResponseDto);
return uploadProductResponseDto;
}
private Product mapAdminRequestDtoToProduct(UploadProductRequestDto uploadProductRequestDto,Product product) throws ImageUploadException {
modelMapper.map(uploadProductRequestDto,product);
product=uploadProductImagesToCloudinaryAndSaveUrl(uploadProductRequestDto,product);
product.setId("Product "+ IdGenerator.generateId());
return product;
}
private Product uploadProductImagesToCloudinaryAndSaveUrl(UploadProductRequestDto uploadProductRequestDto,Product product) throws ImageUploadException {
product.setProductImage1(imageUrlFromCloudinary(uploadProductRequestDto.getProductImage1()));
product.setProductImage2(imageUrlFromCloudinary(uploadProductRequestDto.getProductImage2()));
product.setProductImage3(imageUrlFromCloudinary(uploadProductRequestDto.getProductImage3()));
return product;
}
private String imageUrlFromCloudinary(MultipartFile image) throws ImageUploadException {
String imageUrl="";
if(image!=null && !image.isEmpty()){
Map<Object,Object> params=new HashMap<>();
params.put("public_id","E&L/"+extractFileName(image.getName()));
params.put("overwrite",true);
try{
Map<?,?> uploadResult = cloudStorageService.uploadImage(image,params);
imageUrl= String.valueOf(uploadResult.get("url"));
}catch (IOException e){
e.printStackTrace();
throw new ImageUploadException("Error uploading images,vehicle upload failed");
}
}
return imageUrl;
}
private String extractFileName(String fileName){
return fileName.split("\\.")[0];
}
}
Controller
#Slf4j
#RestController
#RequestMapping(ApiRoutes.ENMASSE)
public class ProductController {
#Autowired
ProductService productService;
#PostMapping("/upload-product/categoryName")
public ResponseEntity<?> UploadProduct(#ModelAttribute UploadProductRequestDto UploadProductRequestDto,#RequestParam String categoryName){
try{
return new ResponseEntity<>
(productService.uploadProducts(UploadProductRequestDto,categoryName), HttpStatus.OK);
}catch (Exception exception){
return new ResponseEntity<>(exception.getMessage(),HttpStatus.BAD_REQUEST);
}
}
}
CLOUD
cloudConfig
#Component
#Data
public class CloudinaryConfig {
#Value("${CLOUD_NAME}")
private String cloudName;
#Value("${API_KEY}")
private String apikey;
#Value("${API_SECRET}")
private String secretKey;
}
CloudConfiguration
#Component
public class CloudinaryConfiguration {
#Autowired
CloudinaryConfig cloudinaryConfig;
#Bean
public Cloudinary getCloudinaryConfig(){
return new Cloudinary(ObjectUtils.asMap("cloud_name",cloudinaryConfig.getCloudName(),
"api_key",cloudinaryConfig.getApikey(),"api_secret",cloudinaryConfig.getSecretKey()));
}
}
CloudinaryStorageServiceImpl
#Service
public class CloudinaryStorageServiceImpl implements CloudStorageService{
#Autowired
Cloudinary cloudinary;
#Override
public Map<?, ?> uploadImage(File file, Map<?, ?> imageProperties) throws IOException {
return cloudinary.uploader().upload(file,imageProperties);
}
#Override
public Map<?, ?> uploadImage(MultipartFile multipartFile, Map<?, ?> imageProperties) throws IOException {
return cloudinary.uploader().upload(multipartFile.getBytes(),imageProperties);
}
}
CloudStorageService
public interface CloudStorageService {
Map<?,?> uploadImage(File file, Map<?,?> imageProperties) throws IOException;
Map<?,?> uploadImage(MultipartFile multipartFile, Map<?, ?> imageProperties) throws IOException;
}
You didn't include the implementation of cloudStorageService.uploadImage(?,?) in the code you pasted here.
Cloudinary's Java implementation requires you pass in the multipart file in bytes. I do not know if you have that since your upload method isn't here.
Maybe refer to the simple implementation here
PS: You can clone the repo to see the implementation of the upload method in the CloudinaryServiceImpl.
It happens that there is nothing wrong with my code. The issue is that it has to be coupled with the frontend so the image tag can render it. Thank you.

How do I save an image in mysql using spring boot? and how can I also retrieve it? I am getting 400 error while registering

I am trying to convert the uploaded file path in angular to a byte[] array after receiving it as a string in spring boot controller using #RequestParam. I am adding the byte array to object using simple setter in model class. while registering I am getting 400 HttpError Response
Model Class
#Entity
#Table(name="registration")
public class Registration {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
private int id;
private String name;
private String username;
private String password;
#Lob
private byte[] image;
// respective getters,setters and constructor
My Registration Repository
#Repository
public interface RegistrationRepository extends JpaRepository<Registration, Integer> {
public List<Registration> findByUsernameAndPassword(String username,String password);
}
Registration Service is present
Registration Controller:-
#RestController
#RequestMapping("/register")
#ComponentScan
public class RegistrationController {
#Autowired
private RegistrationService res;
#PostMapping("/registeruser")
public ResponseEntity<Registration> registeruser(#RequestBody Registration reg, #RequestParam String filename) throws IOException
{
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String userHashedPassword = bCryptPasswordEncoder.encode(reg.getPassword());
File file = new File(filename);
byte[] picInBytes = new byte[(int) file.length()];
FileInputStream fileInputStream = new FileInputStream(file);
fileInputStream.read(picInBytes);
fileInputStream.close();
reg.setImage(picInBytes);
Registration resm= new Registration(reg.getName(),reg.getUsername(),userHashedPassword, reg.getImage());
Registration userResp = res.registeruser(resm);
return new ResponseEntity<Registration>(userResp,HttpStatus.OK);
}
You are accepting a file name, intstead of actual file. Remove the "filename" from your controller's method and add a parameter Multipart file.
Your controller method does this: create a file with name given by the filename argument, which is empty, then gets its size, which is empty again and then reads 0 bytes via the stream.

Sending Entity Object in Response which contains blob

I am trying to create a springboot usermanagement application.
I have an entity object which contains two blob elements.Here is my entity object.
#Entity
#Table(name="user_meta_profile")
public class UserMetaProfile implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "user_id")
private int user_id;
#Column(name = "resume_file")
#Lob
private Blob resume_file;
#Column(name = "photo")
#Lob
private Blob photo;
#Column(name = "username")
private String username;
public int getUser_id() {
return user_id;
}
public void setUser_id(int user_id) {
this.user_id = user_id;
}
public Blob getResume_file() {
return resume_file;
}
public void setResume_file(Blob resume_file) {
this.resume_file = resume_file;
}
public Blob getPhoto() {
return photo;
}
public void setPhoto(Blob photo) {
this.photo = photo;
}
public void setUsername(String username) {
this.username = username;
}
}
As you can see there are two blob items 'resume_file' and 'photo'.
I want to send back a JSON response to the API call.
My Controller code is as shown below.
#Controller
#RequestMapping("/v1")
public class UsersController {
#Autowired
private IUserMetaProfileService userMetaProfileService;
#GetMapping("MetaProfile/{id}")
public ResponseEntity<UserMetaProfile> getUserMetaProfileById(#PathVariable("id") Integer id) {
UserMetaProfile userMetaProfile = userMetaProfileService.getUsersById(id);
return new ResponseEntity<UserMetaProfile>(userMetaProfile, HttpStatus.OK);
}
}
But when I call the API, I get the exception:
"exception": "org.springframework.http.converter.HttpMessageNotWritableException",
"message": "Could not write JSON document: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain:
...
...nested exception is com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
Since JSON cannot contain binary data you need to serialize those fields as something else. You have a couple of options:
If you intend to show the binary as an image (since yours is a photo) you can serialize it as a data uri.
Send links to photos instead and create a controller method that will output the binary data with the appropriate content type (beyond the scope here).
So for Option 1 you can do something like this:
#Entity
#Table(name="user_meta_profile")
public class UserMetaProfile implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "user_id")
private int user_id;
#Column(name = "resume_file")
#Lob
private Blob resume_file;
#Column(name = "photo")
#Lob
private Blob photo;
#Column(name = "username")
private String username;
public int getUser_id() {
return user_id;
}
public void setUser_id(int user_id) {
this.user_id = user_id;
}
#JsonIgnore // disable serializing this field by default
public Blob getResume_file() {
return resume_file;
}
// serialize as data uri insted
#JsonProperty("resumeData")
public String getResume() {
// just assuming it is a word document. you would need to cater for different media types
return "data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64," + new String(Base64.getEncoder().encode(resume_file.getBytes()));
}
public void setResume_file(Blob resume_file) {
this.resume_file = resume_file;
}
#JsonIgnore // disable this one too
public Blob getPhoto() {
return photo;
}
// serialize as data uri instead
#JsonProperty("photoData")
public String getPhotoBase64() {
// just assuming it is a jpeg. you would need to cater for different media types
return "data:image/jpeg;base64," + new String(Base64.getEncoder().encode(photo.getBytes()));
}
public void setPhoto(Blob photo) {
this.photo = photo;
}
public void setUsername(String username) {
this.username = username;
}
}
For the photo bit the value of the photoData JSON attribute can be set directly as the src attribute of an img tag and the photo will be rendered in the HTML. With the resume file you can attach it as an href to a <a> tag with a download attribute so it can be downloaded:
<a href={photoData value here} download>Download Resume File</a>
Just as an FYI if the files are large the JSON will be huge and it might also slow down the browser.

Spring HATEOAS with nested resources and JsonView filtering

I am trying to add HATEOAS links with Resource<>, while also filtering with #JsonView. However, I don't know how to add the links to nested objects.
In the project on on Github, I've expanded on this project (adding in the open pull request to make it work without nested resources), adding the "Character" entity which has a nested User.
When accessing the ~/characters/resource-filtered route, it is expected that the nested User "player" appear with the firstNm and bioDetails fields, and with Spring generated links to itself, but without the userId and lastNm fields.
I have the filtering working correctly, but I cannot find an example of nested resources which fits with the ResourceAssembler paradigm. It appears to be necessary to use a ResourceAssembler to make #JsonView work.
Any help reconciling these two concepts would be appreciated. If you can crack it entirely, consider sending me a pull request.
User.java
//package and imports
...
public class User implements Serializable {
#JsonView(UserView.Detail.class)
private Long userId;
#JsonView({ UserView.Summary.class, CharacterView.Summary.class })
private String bioDetails;
#JsonView({ UserView.Summary.class, CharacterView.Summary.class })
private String firstNm;
#JsonView({ UserView.Detail.class, CharacterView.Detail.class })
private String lastNm;
public User(Long userId, String firstNm, String lastNm) {
this.userId = userId;
this.firstNm = firstNm;
this.lastNm = lastNm;
}
public User(Long userId) {
this.userId = userId;
}
...
// getters and setters
...
}
CharacterModel.java
//package and imports
...
#Entity
public class CharacterModel implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView(CharacterView.Summary.class)
private Long characterId;
#JsonView(CharacterView.Detail.class)
private String biography;
#JsonView(CharacterView.Summary.class)
private String name;
#JsonView(CharacterView.Summary.class)
private User player;
public CharacterModel(Long characterId, String name, String biography, User player) {
this.characterId = characterId;
this.name = name;
this.biography = biography;
this.player = player;
}
public CharacterModel(Long characterId) {
this.characterId = characterId;
}
...
// getters and setters
...
}
CharacterController.java
//package and imports
...
#RestController
#RequestMapping("/characters")
public class CharacterController {
#Autowired
private CharacterResourceAssembler characterResourceAssembler;
...
#JsonView(CharacterView.Summary.class)
#RequestMapping(value = "/resource-filtered", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.OK)
public Resource<CharacterModel> getFilteredCharacterWithResource() {
CharacterModel model = new CharacterModel(1L, "TEST NAME", "TEST BIOGRAPHY", new User(1L, "Fred", "Flintstone"));
return characterResourceAssembler.toResource(model);
}
...
}
CharacterResourceAssembler.java
//package and imports
...
#Component
public class CharacterResourceAssembler implements ResourceAssembler<CharacterModel, Resource<CharacterModel>>{
#Override
public Resource<CharacterModel> toResource(CharacterModel user) {
Resource<CharacterModel> resource = new Resource<CharacterModel>(user);
resource.add(linkTo(CharacterController.class).withSelfRel());
return resource;
}
}

ElasticSearch index

ElasticSearch makes index for new records created by UI,but the records created by liquibase file not indexed so it don't appears in search result,ElasticSearch should index all records created by UI and liquibase files,Is there any process for indexing the records in liquibase files.
Liquibase only makes changes to your database. Unless you have some process which listens to the database changes and then updates Elasticsearch, you will not see the changes.
There might be multiple ways to get your database records into Elasticsearch:
Your UI probably calls some back-end code to index a create or an update into Elasticsearch already
Have a batch process which knows which records are changed (e.g. use an updated flag column or a updated_timestamp column) and then index those into Elasticsearch.
The second option can either be done in code using a scripting or back-end scheduled job or you might be able to use Logstash with the jdbc-input plugin.
As Sarwar Bhuiyan and Mogsdad sad
Unless you have some process which listens to the database changes and
then updates Elasticsearch
You can use liquibase to populate elasticsearch(this task will be executed once, just like normal migration). To do this you need to create a customChange:
<customChange class="org.test.ElasticMigrationByEntityName">
<param name="entityName" value="org.test.TestEntity" />
</customChange>
In that java based migration you can call the services you need. Here is an example of what you can do (please do not use code from this example in a production).
public class ElasticMigrationByEntityName implements CustomTaskChange {
private String entityName;
public String getEntityName() {
return entityName;
}
public void setEntityName(String entityName) {
this.entityName = entityName;
}
#Override
public void execute(Database database) {
//We schedule the task for the next execution. We are waiting for the context to start and we get access to the beans
DelayedTaskExecutor.add(new DelayedTask(entityName));
}
#Override
public String getConfirmationMessage() {
return "OK";
}
#Override
public void setUp() throws SetupException {
}
#Override
public void setFileOpener(ResourceAccessor resourceAccessor) {
}
#Override
public ValidationErrors validate(Database database) {
return new ValidationErrors();
}
/* ===================== */
public static class DelayedTask implements Consumer<ApplicationContext> {
private final String entityName;
public DelayedTask(String entityName) {
this.entityName = entityName;
}
#Override
public void accept(ApplicationContext applicationContext) {
try {
checkedAccept(applicationContext);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//We're going to find beans by name (the most controversial point)
private void checkedAccept(ApplicationContext context) throws ClassNotFoundException {
Class entityClass = Class.forName(entityName);
String name = entityClass.getSimpleName();
//Please do not use this code in production
String repositoryName = org.apache.commons.lang3.StringUtils.uncapitalize(name + "Repository");
String repositorySearchName = org.apache.commons.lang3.StringUtils.uncapitalize(name + "SearchRepository");
JpaRepository repository = (JpaRepository) context.getBean(repositoryName);
ElasticsearchRepository searchRepository = (ElasticsearchRepository) context.getBean(repositorySearchName);
//Doing our work
updateData(repository, searchRepository);
}
//Write your logic here
private void updateData(JpaRepository repository, ElasticsearchRepository searchRepository) {
searchRepository.saveAll(repository.findAll());
}
}
}
Because the beans have not yet been created, we will have to wait for them
#Component
public class DelayedTaskExecutor {
#Autowired
private ApplicationContext context;
#EventListener
//We are waiting for the app to launch
public void onAppReady(ApplicationReadyEvent event) {
Queue<Consumer<ApplicationContext>> localQueue = getQueue();
if(localQueue.size() > 0) {
for (Consumer<ApplicationContext> consumer = localQueue.poll(); consumer != null; consumer = localQueue.poll()) {
consumer.accept(context);
}
}
}
public static void add(Consumer<ApplicationContext> consumer) {
getQueue().add(consumer);
}
public static Queue<Consumer<ApplicationContext>> getQueue() {
return Holder.QUEUE;
}
private static class Holder {
private static final Queue<Consumer<ApplicationContext>> QUEUE = new ConcurrentLinkedQueue();
}
}
An entity example:
#Entity
#Table(name = "test_entity")
#Document(indexName = "testentity")
public class TestEntity implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Field(type = FieldType.Keyword)
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
#NotNull
#Column(name = "code", nullable = false, unique = true)
private String code;
...
}

Categories