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.
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'm doing a POC with Axon. I found that axon is able to process my first POST request and for all subsequent POST request I get the following exception. For every Create request , I create unique identifier IdentifierFactory.getInstance().generateIdentifier(), So, this should Ideally work and I see this also changing from breakpoint but index id is becoming same.
Can someone please find the missing part here.
org.hsqldb.HsqlException: integrity constraint violation: unique constraint or index violation; UK8S1F994P4LA2IPB13ME2XQM1W table: DOMAIN_EVENT_ENTRY
org.axonframework.modelling.command.ConcurrencyException(An event for aggregate [0] at
sequence [0] was already inserted)
POST Requests:
Request 1: This one succeeds
curl --location --request POST 'http://localhost:8080/raise/issues' \
--header 'Content-Type: application/json' \
--data-raw '{"description":"Demo issue1","type":"DEMO1"}'
Request 2: This one onwards it fails
curl --location --request POST 'http://localhost:8080/raise/issues' \
--header 'Content-Type: application/json' \
--data-raw '{"description":"Demo issue2","type":"DEMO2"}'
Controller:
public class IssueTracker {
#Inject
private IssueTrackerService issueTrackerService;
#GetMapping("/issues")
public List<Issue> getAllIssues() {
return issueTrackerService.getAllIssues();
}
#PostMapping(value = "/raise/issues", consumes = "application/json")
public CompletableFuture<IssueCommand> raiseIssue(#RequestBody IssueView issueView) {
return issueTrackerService.raiseIssue(issueView);
}
}
Service:
package com.axon.axondemo.service;
import com.axon.axondemo.dao.Issue;
import com.axon.axondemo.dto.IssueCommand;
import com.axon.axondemo.repository.IssueTRepository;
import com.axon.axondemo.view.IssueView;
import org.axonframework.commandhandling.gateway.CommandGateway;
import org.axonframework.common.IdentifierFactory;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.CompletableFuture;
#Service
public class IssueTrackerService {
private final IssueTRepository issueTRepository;
private final CommandGateway commandGateway;
public IssueTrackerService(IssueTRepository issueTRepository, CommandGateway commandGateway) {
this.issueTRepository = issueTRepository;
this.commandGateway = commandGateway;
}
public CompletableFuture<IssueCommand> raiseIssue(IssueView issueView) {
return commandGateway.send(new IssueCommand(IdentifierFactory.getInstance().generateIdentifier(), issueView.getDescription(), issueView.getType()));
}
public List<Issue> getAllIssues() {
return issueTRepository.findAll();
}
}
Entity:
import com.axon.axondemo.dto.IssueCommand;
import com.axon.axondemo.events.IssueEvent;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.eventsourcing.EventSourcingHandler;
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.spring.stereotype.Aggregate;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import static org.axonframework.modelling.command.AggregateLifecycle.apply;
#Aggregate
#Entity
public class Issue {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#AggregateIdentifier
private long id;
private String description;
private String type;
public Issue() {}
public Issue(String description, String type) {
this.description = description;
this.type = type;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
#CommandHandler
public Issue(IssueCommand issueCommand) {
apply(new IssueEvent(issueCommand.getAggregateRefno(), issueCommand.getDescription(), issueCommand.getType()));
}
#EventSourcingHandler
public void on(IssueEvent issueEvent) {
this.description = issueEvent.getDescription();
this.type = issueEvent.getType();
}
}
Command:
package com.axon.axondemo.dto;
import org.axonframework.modelling.command.TargetAggregateIdentifier;
public class IssueCommand {
private String description;
private String type;
#TargetAggregateIdentifier
private String aggregateRefno;
public IssueCommand(String aggregateRefno, String description, String type) {
this.aggregateRefno = aggregateRefno;
this.description = description;
this.type = type;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getAggregateRefno() {
return aggregateRefno;
}
public void setAggregateRefno(String aggregateRefno) {
this.aggregateRefno = aggregateRefno;
}
}
Event:
package com.axon.axondemo.events;
public class IssueEvent {
private String aggregateRefno;
private String description;
private String type;
public IssueEvent() {}
public IssueEvent(String aggregateRefno, String description, String type) {
this.description = description;
this.type = type;
this.aggregateRefno = aggregateRefno;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getAggregateRefno() {
return aggregateRefno;
}
public void setAggregateRefno(String aggregateRefno) {
this.aggregateRefno = aggregateRefno;
}
}
Query/Handler:
package com.axon.axondemo.handler;
import com.axon.axondemo.events.IssueEvent;
import org.axonframework.eventhandling.EventHandler;
import org.springframework.stereotype.Component;
#Component
public class IssueEventHandler {
#EventHandler
public void on(IssueEvent issueEvent) {
System.out.println("*************");
System.out.println("*************");
System.out.println("Issue event handled!!!!");
System.out.println(issueEvent.getDescription());
System.out.println("*************");
System.out.println("*************");
}
}
Repository:
package com.axon.axondemo.repository;
import com.axon.axondemo.dao.Issue;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
#Repository
public interface IssueTRepository extends JpaRepository<Issue, Long> {
}
#Flaxel his/her argument is something to take note of:
I would not implement the entity and aggregate as a common object.
I'd add though that it is definitely not wrong what you are doing there. The main difference is that you are not doing Event Sourcing if you make the Aggregate a stored entity as is. A choice you have, which from Axon's Reference Guide lands you up on the "State-Stored Aggregate". However, your Aggregate snippet does use an #EventSourcingHandler annotated method, seemingly showing you do want to use Event Sourcing for said aggregate. Hence it be worth taking either the state-stored or event sourcing approach within your aggregate design to keep things clear. However, this doesn't answer the problem you are encountering though, so let's focus on that further.
The exception you are receiving is being sent because your application tries to store events for the same aggregate on the same location. Normally this suggests that two distinct instances of your service are loading the same aggregate and performing operations on it, something which is undesirable because it introduces concurrency exceptions. Hence why Axon throws a ConcurrencyException.
As you've seen from the message, the uniqueness constraint is build out of the aggregate identifier and the sequence number. The latter is an incremental number describing the position of an events in an Aggregates stream. You don't have immediate control over this value. The thing you do control is the aggregate identifier.
Currently, your #AggregateIdentifier annotated field is the same as the #Id annotated field. Again nothing wrong with this. What I wouldn't do though is make it a long. Using a long (generated or not) will make it so that you will see that concurrency exception quite often I think, especially once you start scaling out. Assume you have four instances of this application running, all concurrently handling commands. Will you be using a distributed sequence generator just so that the Aggregate Identifiers all walk in line? Doable, yes, but it introduces quite some complexity on that end.
I'd recommend using a regular random UUID as the #AggregateIdentifier annotated field instead. You are far more certain to (virtually) never hit a duplicate id in that case.
Still, this doesn't answer to me why the second command you issue makes it so that your sequence generator reuses ID 0 instead of adjusting it. What I do know, is that it's not so much an Axon Framework thing anymore, as this occurs due to usage of the #GeneratedValue annotation.
The Baeldung page referenced by #flaxel could proof as a nice starting point, as it has been updated by the AxonIQ team themselves. On top of that, there are a bunch of quick start videos you could check out. Lastly, partaking in a Fast Lane Axon Training (just 2 hours) could proof helpful as well if you find yourself stuck in the future.
I would not implement the entity and aggregate as a common object. Maybe this causes problems because the AggregateIdentifier and the Id is set to a variable and the Id is autogenerated. It mixes two concepts. Baeldung made a nice tutorial about Axon and Spring Boot. If the tutorial does not help you, then you can ask again.
What else you could use is Lombok. This makes your classes look even more clearly arranged.
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();
}
}
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.