This is a question about a Spring Boot MVC application with Hibernate and PostgreSQL.
I have a web page that allows a user to set administrative / configuration data for an application, and I want to store this data in a single database record. I have a POJO to contain the data. I have coded up a Spring MVC app that persists this data.
The trouble is that each time the user saves the data (by doing a POST from the web page) the Spring application creates a new record in the database.
I'm using a Repository, and I was under the impression that each time I did a Repository.save() on the object it would update the existing record if there is one, otherwise create a new one and that it would identify the record based upon the primary key. I could not find an "update" method.
I have tried several ways around this issue but they either still make extra records, fail with a duplicate key error or just plain don't work.
Also, it seems that each time I start the web page or the application all the data in the database is removed.
So what's the trick? Thanks very much...
Here is my code:
AdminFormController.java
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.beans.factory.annotation.Autowired;
#Controller
public class Admin_FormController
{
#Autowired
private AdminDataRepository rep;
#RequestMapping(value="/admin", method=RequestMethod.GET)
public String adminForm(Model model)
{
AdminData ad = new AdminData();
model.addAttribute("adminForm", ad);
ad = rep.findById(1L);
if(ad != null)
ad.setId(1L);
return "adminForm";
}
#RequestMapping(value="/admin", method=RequestMethod.POST)
public String adminSubmit(#ModelAttribute AdminData ad, Model model)
{
// ad.setId(1L);;
rep.save(ad);
model.addAttribute("adminForm", ad);
return "adminForm";
}
}
AdminDataRepository.java
import org.springframework.data.repository.CrudRepository;
public interface AdminDataRepository extends CrudRepository<AdminData, String>
{
AdminData findById(Long Id);
}
AdminData.java
import java.util.logging.Logger;
import javax.persistence.*;
#Entity
public class AdminData
{
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String useDates;
private String startDate;
private String endDate;
public String getUseDates()
{
return useDates;
}
public String getStartDate()
{
return startDate;
}
public String getEndDate()
{
return endDate;
}
public void setUseDates(String s)
{
Logger.getGlobal().info(() -> "UseDates: " + s);
useDates = s;
}
public void setStartDate(String s)
{
Logger.getGlobal().info(() -> "Start Date: " + s);
startDate = s;
}
public void setEndDate(String s)
{
Logger.getGlobal().info(() -> "End Date: " + s);
endDate = s;
}
}
You need to store the object somewhere between requests. Your options:
Using hidden form fields
Re-read it from the database (in your POST method)
Use session
1 is inconvenient, and not secure. 2 Doesn't support concurrency control. 3 is secure and correct.
To implement #3, add #SessionAttributes("yourAttributeName") just before your controller. Add a SessionStatus parameter to your POST method, and call sessionStatus.setComplete() when you're done with it.
Example here.
Related
I have list of products returned from a controller as a Flux. I am able to verify the count but I don't know how to verify the individual products. The product has a lot of properties so I do want to do a direct equals which may not work. Here is a subset of the properties of the class. The repository layer works fine and returns the data. The problem is that I don't know how to use the StepVerifier to validate the data returned by the ProductService. I am using a mock ProductRepository not shown as it just mocks return a Flux of hardcoded products like this Flux.just(productData)
import java.time.LocalDateTime;
import java.util.List;
public class ProductData {
static class Order {
String orderId;
String orderedBy;
LocalDateTime orderDate;
List<OrderItem> orderItems;
}
static class OrderItem {
String itemCode;
String name;
int quantity;
int quantityOnHold;
ItemGroup group;
}
static class ItemGroup{
String category;
String warehouseID;
String warehoueLocation;
}
}
Here is the service class.
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;
#RequiredArgsConstructor
public class ProductService {
final ProductRepository productRepository;
Flux<ProductData> findAll(){
return productRepository.findAll();
}
}
Since your example ProductData class doesn't have any fields to verify, let's assume it has one order field:
public class ProductData {
Order order;
//rest of the code
}
Then fields can be verified like this:
#Test
void whenFindAllThenReturnFluxOfProductData() {
Flux<ProductData> products = productRepository.findAll();
StepVerifier.create(products)
.assertNext(Assertions::assertNotNull)
.assertNext(product -> {
ProductData.Order order = product.order;
LocalDateTime expectedOrderDate = LocalDateTime.now();
assertNotNull(order);
assertEquals("expectedOrderId", order.orderId);
assertEquals(expectedOrderDate, order.orderDate);
//more checks
}).assertNext(product -> {
List<ProductData.OrderItem> orderItems = product.order.orderItems;
int expectedSize = 12;
assertNotNull(orderItems);
assertEquals( expectedSize, orderItems.size());
//more checks
})
.expectComplete()
.verify();
}
I am trying to insert data into the h2 database taking input from user.But primary key is getting inserted but the other is stored as null.
Here is my application.properties
spring.sql.init.platform==h2
spring.datasource.url=jdbc:h2:mem:preethi
spring.jpa.defer-datasource-initialization: true
spring.jpa.hibernate.ddl-auto=update
Here is Controller class AlienController.java
package com.preethi.springbootjpa.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.preethi.springbootjpa.model.Alien;
import com.preethi.springbootjpa.repo.AlienRepo;
#Controller
public class AlienController {
#Autowired
AlienRepo repo;
#RequestMapping("/")
public String home()
{
return "home.jsp";
}
#RequestMapping("/addAlien")
public String addAlien( Alien alien)
{
repo.save(alien);
return "home.jsp";
}
}
Here is Alien.java
package com.preethi.springbootjpa.model;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.springframework.stereotype.Component;
#Component
#Entity
public class Alien {
#Id
private int aid;
private String aname;
public Alien()
{
}
public Alien(int aid, String aname) {
super();
this.aid = aid;
this.aname = aname;
}
public int getAid() {
return aid;
}
public void setAid(int aid) {
this.aid = aid;
}
public String getName() {
return aname;
}
public void setName(String name) {
this.aname = name;
}
#Override
public String toString() {
return "Alien [aid=" + aid + ", name=" + aname + "]";
}
}
Here is AlienRepo.java
package com.preethi.springbootjpa.repo;
import org.springframework.data.repository.CrudRepository;
import com.preethi.springbootjpa.model.Alien;
public interface AlienRepo extends CrudRepository<Alien,Integer>{
}
Here is data.sql;
insert into alien values(101,'Preethi');
when I try to insert data from data.sql,it is getting inserted but when I try to insert data taking input from user, data is stored as null(except primary key).
Here is the table :
table
I got the same issue when i forgot to add the gettters and setters for an entity class.
Finally solved it, there is no problem with the code...It's just all the configuraton things that messed up.
Just did everything from scratch and took care of build path and configurations.
Solved..
I have something in mind:
If I use Spring Boot to build a Neo4j-based project, I need to define the properties and methods of the Entity in advance. If I later want to add new edges or attributes to the graph, even new types of nodes, how should I handle entities?
On Referring the Spring Data - Neo4j Docs
you can write Entities in the following way
Example Entity code:
package com.example.accessingdataneo4j;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;
#NodeEntity
public class Person {
#Id #GeneratedValue private Long id;
private String name;
private Person() {
// Empty constructor required as of Neo4j API 2.0.5
};
public Person(String name) {
this.name = name;
}
/**
* Neo4j doesn't REALLY have bi-directional relationships. It just means when querying
* to ignore the direction of the relationship.
* https://dzone.com/articles/modelling-data-neo4j
*/
#Relationship(type = "TEAMMATE", direction = Relationship.UNDIRECTED)
public Set<Person> teammates;
public void worksWith(Person person) {
if (teammates == null) {
teammates = new HashSet<>();
}
teammates.add(person);
}
public String toString() {
return this.name + "'s teammates => "
+ Optional.ofNullable(this.teammates).orElse(
Collections.emptySet()).stream()
.map(Person::getName)
.collect(Collectors.toList());
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Edges or Relationships can be done by the following way
#Relationship(type = "TEAMMATE", direction = Relationship.UNDIRECTED)
public Set<Person> teammates;
Here the person [Node] is connected to another nodes [team-mates] and Stored.
where ever you design you can add new classes and write schema and start the server.
To Perform CRUD operations you can use Spring data jpa Repository.
PersonRepository extends the GraphRepository interface and plugs in the type on which it operates: Person. This interface comes with many operations, including standard CRUD (create, read, update, and delete) operations.
I want to use binary UUID in a MariaDB database used for a spring-boot project, instead of using varchar uuid. For now, I am able to create, save and search a binary UUID, by override the column length to 16 but I have to manually put the annotation #Column(length=16) on any UUID field.
Is there a way to globally made this modification in the project ?
In other words, is there a way that, for all UUID field in the project, jpa/hibernate create a column "binary(16)" instead of "binary(255)" ?
My problem is that, by default, an UUID is converted into a binary(255) into MariaDB, and with this configuration, JPA Repositories queries are not able to find any data when searching on a UUID field.
To achieve Jpa repositories queries, I have to add the #Column(length=16) on any UUID field.
I have tried to use a "#Converter" but the Convert annotation should not be used to specify conversion of the following: Id attributes, version attributes, relationship attributes etc... And it doesn't work with an uuid relationship field.
I have also tried to use my own custom hibernate type (example here : https://www.maxenglander.com/2017/09/01/optimized-uuid-with-hibernate.html) but the jpa repositories queries don't find anything.
Now i have this :
My abstract entity :
public abstract class GenericEntity {
#Id
#GeneratedValue(generator = "uuid2")
#GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator")
#Column(length = 16)
private UUID id;
//...
}
When using an uuid in another object :
public abstract class AnotherEntity extends GenericEntity {
#NotNull
#Column(length = 16)
private UUID owner;
//...
}
I'm looking for a way to override the UUID field generation without putting the "#Column(length = 16)" everywhere.
It would be really great to avoid errors and / or omissions when using the UUID type in others features.
Thanks a lot !
The type descriptor, remapping the binary implementation onto OTHER Hibernate typedef:
import java.sql.Types;
import java.util.UUID;
import org.hibernate.type.descriptor.java.BasicJavaDescriptor;
import org.hibernate.type.descriptor.sql.BinaryTypeDescriptor;
import org.hibernate.type.spi.TypeConfiguration;
public class MariaDBUuidTypeDescriptor extends BinaryTypeDescriptor {
private static final long serialVersionUID = 1L;
public static final MariaDBUuidTypeDescriptor INSTANCE = new MariaDBUuidTypeDescriptor();
public MariaDBUuidTypeDescriptor() {
super();
}
#Override
public int getSqlType() {
return Types.OTHER;
}
#Override
#SuppressWarnings("unchecked")
public BasicJavaDescriptor<UUID> getJdbcRecommendedJavaTypeMapping(TypeConfiguration typeConfiguration) {
return (BasicJavaDescriptor<UUID>) typeConfiguration.getJavaTypeDescriptorRegistry().getDescriptor( UUID.class );
}
}
The type itself, wrapping the descriptor above and binding it to the UUID classdef.
import java.util.UUID;
import org.hibernate.type.AbstractSingleColumnStandardBasicType;
import org.hibernate.type.descriptor.java.UUIDTypeDescriptor;
public class MariaDBUuidType extends AbstractSingleColumnStandardBasicType<UUID> {
private static final long serialVersionUID = 1L;
public static final MariaDBUuidType INSTANCE = new MariaDBUuidType();
public MariaDBUuidType() {
super( MariaDBUuidTypeDescriptor.INSTANCE, UUIDTypeDescriptor.INSTANCE );
}
#Override
public String getName() {
return "mariadb-uuid-binary";
}
#Override
protected boolean registerUnderJavaType() {
return true;
}
}
The modified Hibernate dialect, making use of the type and remapping all its occurrences onto binary(16)
import java.sql.Types;
import org.hibernate.HibernateException;
import org.hibernate.boot.model.TypeContributions;
import org.hibernate.dialect.MariaDB103Dialect;
import org.hibernate.service.ServiceRegistry;
public class MariaDB103UuidAwareDialect extends MariaDB103Dialect {
#Override
public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) {
super.contributeTypes( typeContributions, serviceRegistry );
registerColumnType( Types.OTHER, "uuid" );
typeContributions.contributeType( MariaDBUuidType.INSTANCE );
}
#Override
public String getTypeName(int code, long length, int precision, int scale) throws HibernateException {
String typeName = super.getTypeName(code, length, precision, scale);
if (Types.OTHER == code && "uuid".equals(typeName)) {
return "binary(16)";
} else {
return typeName;
}
}
}
Please note that this is Hibernale-only implementation, i.e. does not matter if you use it along Spring (Boot) or not.
I have an entity named EmployeeDepartment as below
#IdClass(EmployeeDepartmentPK.class) //EmployeeDepartmentPK is a serializeable object
#Entity
EmployeeDepartment{
#Id
private String employeeID;
#Id
private String departmentCode;
---- Getters, Setters and other props/columns
}
and I have a Spring Data Repository defined as as below
#RepositoryRestResource(....)
public interface IEmployeeDepartmentRepository extends PagingAndSortingRepository<EmployeeDepartment, EmployeeDepartmentPK> {
}
Further, I have a converter registered to convert from String to EmployeeDepartmentPK.
Now, for an entity, qualified by ID employeeID="abc123" and departmentCode="JBG", I expect the ID to use when SDR interface is called is abc123_JBG.
For example http://localhost/EmployeeDepartment/abc123_JBG should fetch me the result and indeed it does.
But, when I try to save an entity using PUT, the ID property available in BasicPersistentEntity class of Spring Data Commons is having a value of
abc123_JBG for departmentCode. This is wrong. I'm not sure if this is an expected behaviour.
Please help.
Thanks!
Currently Spring Data REST only supports compound keys that are represented as by a single field. That effectively means only #EmbeddedId is supported. I've filed DATAJPA-770 to fix that.
If you can switch to #EmbeddedId you still need to teach Spring Data REST the way you'd like to represent your complex identifier in the URI and how to transform the path segment back into an instance of your id type. To achieve that, implement a BackendIdConverter and register it as Spring bean.
#Component
class CustomBackendIdConverter implements BackendIdConverter {
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
// Make sure you validate the input
String[] parts = id.split("_");
return new YourEmbeddedIdType(parts[0], parts[1]);
}
#Override
public String toRequestId(Serializable source, Class<?> entityType) {
YourIdType id = (YourIdType) source;
return String.format("%s_%s", …);
}
#Override
public boolean supports(Class<?> type) {
return YourDomainType.class.equals(type);
}
}
If you can't use #EmbeddedId, you can still use #IdClass. For that, you need the BackendIdConverter as Oliver Gierke answered, but you also need to add a Lookup for your domain type:
#Configuration
public class IdClassAllowingConfig extends RepositoryRestConfigurerAdapter {
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.withEntityLookup().forRepository(EmployeeDepartmentRepository.class, (EmployeeDepartment ed) -> {
EmployeeDepartmentPK pk = new EmployeeDepartmentPK();
pk.setDepartmentId(ed.getDepartmentId());
pk.setEmployeeId(ed.getEmployeeId());
return pk;
}, EmployeeDepartmentRepository::findOne);
}
}
Use #BasePathAwareController to customize Spring data rest controller.
#BasePathAwareController
public class CustInfoCustAcctController {
#Autowired
CustInfoCustAcctRepository cicaRepo;
#RequestMapping(value = "/custInfoCustAccts/{id}", method = RequestMethod.GET)
public #ResponseBody custInfoCustAccts getOne(#PathVariable("id") String id) {
String[] parts = id.split("_");
CustInfoCustAcctKey key = new CustInfoCustAcctKey(parts[0],parts[1]);
return cicaRepo.getOne(key);
}
}
It's work fine for me with sample uri /api/custInfoCustAccts/89232_70
A more generic approach would be following -
package com.pratham.persistence.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.istack.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import javax.persistence.EmbeddedId;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Customization of how composite ids are exposed in URIs.
* The implementation will convert the Ids marked with {#link EmbeddedId} to base64 encoded json
* in order to expose them properly within URI.
*
* #author im-pratham
*/
#Component
#RequiredArgsConstructor
public class EmbeddedBackendIdConverter implements BackendIdConverter {
private final ObjectMapper objectMapper;
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
return getFieldWithEmbeddedAnnotation(entityType)
.map(Field::getType)
.map(ret -> {
try {
String decodedId = new String(Base64.getUrlDecoder().decode(id));
return (Serializable) objectMapper.readValue(decodedId, (Class) ret);
} catch (JsonProcessingException ignored) {
return null;
}
})
.orElse(id);
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
try {
String json = objectMapper.writeValueAsString(id);
return Base64.getUrlEncoder().encodeToString(json.getBytes(UTF_8));
} catch (JsonProcessingException ignored) {
return id.toString();
}
}
#Override
public boolean supports(#NonNull Class<?> entity) {
return isEmbeddedIdAnnotationPresent(entity);
}
private boolean isEmbeddedIdAnnotationPresent(Class<?> entity) {
return getFieldWithEmbeddedAnnotation(entity)
.isPresent();
}
#NotNull
private static Optional<Field> getFieldWithEmbeddedAnnotation(Class<?> entity) {
return Arrays.stream(entity.getDeclaredFields())
.filter(method -> method.isAnnotationPresent(EmbeddedId.class))
.findFirst();
}
}