I am sorry for the long post, but I believe It is important to mention everything related to the issue.
I am dealing with a requirement for my webservice that sends out notifications for 20k+ users at a time. Since this is quite a heavy task, I thought that having it Async was probably the best approach as it will take some time to process the data. This feature is available for a vast majority of users on the platform, hence there can be multiple requests at once. The amount of users that will receive a notification can vary from 1k to 20k+. Since the request processing takes quite a long time - I basically create a notification, assign it to the correct talents and then send it out in waves. This feature alone seems to have a massive impact on performance when there is multiple concurrent requets for notifications active at the same time and I end up with an out of memory error. I am not sure if this can be optimized at all, or If I should just perhaps choose a completely different approach to everything. I apologize for the long post but I believe it important that I mention everything.
I designed the system to act as follows:
I receive a notification request, which is created in a separate table
I receive a token that indicates which users should get the notification
I fetch the users via a mapped class that is used as a predicate inside of a findAll method (QueryDSL)
I created a relational table taht contains the notificationId, talentId and an extra 'sent' column. Every talent that should receive the message is added to this table along with the notificationId
I have a #Scheduled method that picks up a portion of the notification/talent relations and sends out the notification periodically
My Async configuration class is as follows:
#Component
#Configuration
#EnableAsync
public class AsyncConfiguration implements AsyncConfigurer {
#Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(5);
executor.setCorePoolSize(5);
executor.setThreadNamePrefix("asyncExec-");
executor.initialize();
return executor;
}
#Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
System.out.println("Exception with message :" + ex.getMessage());
System.out.println("Method :" + method);
System.out.println("Number of parameters :" + params.length);
};
}
}
My user entity has the following mapping:
#OneToMany(mappedBy = "talent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TalentRoleNotificationRelations> roleNotifications;
My notification has the following mapping:
#OneToMany(mappedBy = "roleNotification", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TalentRoleNotificationRelations> roleNotifications;
And my relational table entity is as follows:
#Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class TalentRoleNotificationRelations implements Serializable {
private static final long serialVersionUID = 1L;
#EmbeddedId
private TalentRoleNotificationIdentity identity;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("talentId")
#JoinColumn(name = "talent_id")
private Talent talent;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("roleNotificationId")
#JoinColumn(name = "role_notification_id")
private RoleNotification roleNotification;
private Boolean sent;
}
And the composite key identity:
#Embeddable
#Builder
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode
#Data
public class TalentRoleNotificationIdentity implements Serializable {
private static final long serialVersionUID = 1L;
#Column(name = "talent_id")
private String talentId;
#Column(name = "role_notification_id")
private String roleNotificationId;
}
This mapping was done following the guide here which indicates how a many to many relation with an extra column should be implemented.
And the actual process of creating the notification
Controller method:
#PostMapping(value = "notify/{searchToken}/{roleId}", produces = MediaType.APPLICATION_JSON_VALUE)
public RoleNotificationInfo notifyTalentsOfNewRole(#PathVariable String searchToken, #PathVariable String roleId) {
var roleProfileInfo = (RoleProfileInfo) Optional.ofNullable(searchToken)
.map(token -> searchFactory.fromToken(token, RoleProfileInfo.class))
.orElse(null);
return productionService.notifyTalentsOfMatchingRole(roleProfileInfo, roleId);
}
Service method: (This is where I believe the issue is, as well as a very bad use of a many to many table mapping?)
#Transactional(propagation = Propagation.REQUIRES_NEW)
#Async
public RoleNotificationInfo notifyTalentsOfMatchingRole(RoleProfileInfo roleProfileInfo, String roleId) {
var predicate = Optional.ofNullable(userService.getPredicate(roleProfileInfo)).orElse(new BooleanBuilder());
var role = roleDao.findById(roleId).orElseThrow(NoSuchRole::new);
var isNotificationLimitReached = isNotificationLimitReached(roleId);
if (!isNotificationLimitReached) {
var notificationBody = notificationBodyDao.findById(NotificationBodyIdentifier.MATCHING_ROLE)
.orElseThrow(NoSuchNotificationBody::new);
var newNotification = RoleNotification.builder()
.notificationBody(notificationBody)
.role(role)
.build();
roleNotificationDao.saveAndFlush(newNotification);
talentDao.findAll(predicate)
.forEach(talent -> {
var identity = TalentRoleNotificationIdentity.builder()
.talentId(talent.getId())
.roleNotificationId(newNotification.getId())
.build();
var talentRoleNotification = TalentRoleNotificationRelations.builder()
.identity(identity)
.roleNotification(newNotification)
.talent(talent)
.sent(false)
.build();
talentRoleNotification.setIdentity(identity);
talentRoleNotification.setTalent(talent);
talentRoleNotificationRelationsDao.save(talentRoleNotification);
});
return dtoFactory.toInfo(newNotification);
} else throw new RoleNotificationLimitReached();
}
Scheduled method that sends out the notifications:
#Transactional(propagation = Propagation.REQUIRES_NEW)
#Scheduled(fixedDelay = 5)
public void sendRoleMessage() {
var unsentNotificationIds = talentRoleNotificationRelationsDao.findAll(
QTalentRoleNotificationRelations.talentRoleNotificationRelations.sent.isFalse()
.and(QTalentRoleNotificationRelations.talentRoleNotificationRelations.talent.notificationToken.isNotNull()),
PageRequest.of(0, 50)
);
unsentNotificationIds.forEach(unsentNotification -> {
var talent = unsentNotification.getTalent();
var roleNotification = unsentNotification.getRoleNotification();
markAsSent(unsentNotification);
talentRoleNotificationRelationsDao.saveAndFlush(unsentNotification);
notificationPusher.push(notificationFactory.buildComposite(talent.getNotificationToken()), dtoFactory.toInfo(roleNotification));
});
}
The notificationPusher method itself:
#Override
public void push(PushMessageComposite composite, RoleNotificationInfo roleNotificationInfo) {
String roleId = roleNotificationInfo.getRoleId();
String title = "New matching role!";
var push = Message.builder()
.setToken(composite.getMeta().getDeviceToken())
.setAndroidConfig(AndroidConfig.builder()
.setNotification(AndroidNotification.builder()
.setTitle(title)
.setBody(roleNotificationInfo.getBody())
.setSound(composite.getMeta().getSound())
.build())
.build())
.setApnsConfig(ApnsConfig.builder()
.setAps(Aps.builder()
.setAlert(ApsAlert.builder()
.setTitle(title)
.setBody(roleNotificationInfo.getBody())
.build())
.setBadge(composite.getMeta().getBadge().intValue())
.setSound(composite.getMeta().getSound())
.build())
.build())
.putData("roleId", roleId)
.build();
Try.run(() -> FirebaseMessaging.getInstance().sendAsync(push).get())
.onFailure(e -> {
log.error("Firebase Cloud Messaging failed during sendNotification", e);
var talent = talentDao.findTalentByNotificationToken(composite.getMeta().getDeviceToken());
talent.setNotificationToken(null);
talentDao.save(talent);
});
}
And the dto factory mapper method:
#Transactional(propagation = Propagation.MANDATORY)
public RoleNotificationInfo toInfo(RoleNotification source) {
return RoleNotificationInfo.builder()
.id(source.getId())
.body(source.getNotificationBody().getBody())
.created(source.getCreated())
.roleId(source.getRole().getId())
.build();
}
I am unsure where the problem lies. I assume it is due to the high amount of users fetched from certain queries (20k+). I did some profiling, and this were the results:
Memory/CPU charts:
My question is, should this even be possible at all? Is there a different approach that would be much more efficient? Should I use an external service for this? Is the problem something very obvious that I do not see? I am not sure where to look. If anything in my code is unclear and needs further clarification, please let me know and I'll try to edit and format it the best way I can.
Related
I'm totally new to reactive programming and I have problem with coding even such an elementary task. The following method of an RestController should:
Take as parameter DoiReservationRequest object that represents reservation of yet-to-be-published DOI number (https://www.doi.org/). This reservation is meaningful only within our internal systems. The parameter is passed in the body of the POST request. DOI reservation request is a simple object:
public record DoiReservationRequest(String doi) {
}
Check that there is no previous reservation of the same number, or that the DOI number is not actually already submitted and published. For this purpose, try to find submissions with the same DOI in DoiSubmissionRepository, which is defined as:
#EnableMongoRepositories
#Repository
public interface DoiSubmissionRepository extends ReactiveMongoRepository<DoiSubmission, String> {
Flux<DoiSubmission> findAllByDoi(Publisher<String> doi);
}
DoiSubmission is itself defined as:
#Getter
#NoArgsConstructor(access = AccessLevel.PROTECTED)
#AllArgsConstructor
#ToString
#Document
public final class DoiSubmission {
#Id
private String id;
#Indexed
private String doi;
private Integer version;
private String xml;
private Date timestamp;
}
If no submission exists then return HTTP 201 with body that for now is empty, but before that save the reservation as DOI submission that has version 0 and empty xml content.
If submissions with the same doi exist (several different versions of the same DOI number with different xml data), return HTTP 409 with body that is yet to be determined that describes the error.
The code hangs indefinitely when POST request is made:
#PostMapping("/api/v1/reservation/")
public Mono<ResponseEntity<String>> create(#RequestBody Publisher<DoiReservationRequest> doi) {
return doiSubmissionRepository
.findAllByDoi(Mono.from(doi)
.map(DoiReservationRequest::doi))
.hasElements()
.flatMap(hasElements->{
if (hasElements) {
return Mono.just(ResponseEntity.status(HttpStatus.CONFLICT).body(""));
} else {
return Mono.from(doi)
.map(doiReservationRequest -> new DoiSubmission(
UUID.randomUUID().toString(),
doiReservationRequest.doi(), 0, "", new Date()))
.flatMap(doiSubmissionRepository::save)
.then(Mono.just(ResponseEntity.status(HttpStatus.OK).body("")));
}
});
}
I have a method controller which gonna return all advertisements that are being filtered by passed paremetrs.
#GetMapping("/search/advertisements")
public List<Advertisement> getSpecicShortAdvertisements(
#RequestParam(value = "city", required = false) String city,
#RequestParam(value = "category", required = false) AdvertisementCategory category) {
return shortAdvertisementService.getSpecificShortAdvertisements(city,category);
}
So for example it can be: http://localhost:8080/search/advertisements?city=WARSZAWA&category=ANIMALSCARE
but it can be also http://localhost:8080/search/advertisements?category=ANIMALSCARE
If I do not send any parameters it shoudl return all advertisements.
It would be so hard to write query for every single parameter, so I've used Example, but do not know why it returns empty list all the time despite having data in DB.
It's being added for every run of application (initial data)
advertisementRepository.save(Advertisement.builder()
.advertisementCategory(AdvertisementCategory.ANIMALSCARE)
.dateTime(LocalDateTime.now())
.description("LAAAAAAAAAAAAAAAAAAA")
.featured(false)
.photos(null)
.title("TiTitle 2tle 1")
.city("KRAKOW")
.build());
advertisementRepository.save(Advertisement.builder()
.advertisementCategory(AdvertisementCategory.ANIMALSCARE)
.dateTime(LocalDateTime.now())
.description("LAAAAAAAAAAAAAAAAAAALA" +
"AALAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAALAAAAA" +
"AAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAALAAAAAAAAAAAAAAAAAAA")
.featured(false)
.photos(null)
.title("TTitle 2Title 2Title 2itle 2")
.city("WARSZAWA")
.build());
So as you can see if I send 'WARSZAWA' as an city it should return this advertisement, but it returns nothing.
Service method:
public List<Advertisement> getSpecificShortAdvertisements(String city, AdvertisementCategory category) {
Advertisement example = Advertisement.builder()
.advertisementCategory(category)
.city(city)
.build();
return advertisementRepository.findAll(Example.of(example));
}
Repo:
#Repository
public interface AdvertisementRepository extends JpaRepository<Advertisement, Long> {
}
//EDIT
SQL generated:
select advertisem0_.advertisementid as advertis1_0_, advertisem0_.advertisement_category as advertis2_0_, advertisem0_.city as city3_0_, advertisem0_.date_time as date_tim4_0_, advertisem0_.description as descript5_0_, advertisem0_.featured as featured6_0_, advertisem0_.title as title7_0_ from advertisement advertisem0_ where advertisem0_.featured=? and advertisem0_.advertisementid=0 and advertisem0_.city=?
If I made
public List<Advertisement> getSpecificShortAdvertisements(String city, AdvertisementCategory category) {
ExampleMatcher exampleMatcher = ExampleMatcher.matchingAny();
Advertisement example = Advertisement.builder()
.advertisementCategory(category)
.city(city)
.build();
return advertisementRepository.findAll(Example.of(example, exampleMatcher));
}
then it returns everything despite sending http://localhost:8080/search/advertisements?city=LALA which should not match anything
I am developing a Springboot application, and my requirements are the following:
I have 8 sql queries, that I have to run in parallel and that return a single result.
My entities using this sql requests are:
#SqlResultSetMapping(name = "bud01Mapping", classes = {
#ConstructorResult(targetClass = Bud01Entity.class, columns = {
#ColumnResult(name = "MNTBUDGETMB", type = Double.class),
}
) })
#Entity
#Getter
#Setter
#AllArgsConstructor
public class Bud01Entity {
#Id
private Double mntBudgetMB;
}
To avoid duplication in my post, please consider: Bud01Entity, Bud02Entity ... until Bud08Entity.
My queries are implemented in an hbm.xml file Bud01Entity.hbm.xml as follow:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings
xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
version="2.0">
<entity
class="budget.Bud01Entity"
name="Bud01Entity">
<named-native-query
name="Bud01Entity.findBud01"
result-set-mapping="bud01Mapping">
<query>
SELECT
SUM(CASE WHEN budget.sens='C' THEN -1 * budget.mnt_budget ELSE budget.mnt_budget END) AS mntBudgetProduction,
ent.code as codService
FROM si.t_sb_budget budget
JOIN si.t_sb_rubrique_budget rub ON rub.pk_id_rubrique_budget = budget.fk_id_rubrique_budget
JOIN si.t_ga_activite act ON act.pk_id_activite = budget.fk_id_activite
JOIN si.t_rd_service serv ON act.fk_id_service = serv.pk_id
JOIN si.t_rd_entite ent ON ent.pk_id = serv.pk_id
JOIN si.t_sb_type_budget typ_bud ON typ_bud.pk_id_type_budget=budget.fk_id_type_budget
WHERE budget.exercice = :codAnnee
AND ent.code = :codService
AND typ_bud.pk_id_type_budget = 6
GROUP BY
ent.code
</query>
</named-native-query>
</entity>
</entity-mappings>
As for the first one, to avoid duplication in my post, please consider: Bud01Entity.hbm.xml, Bud02Entity.hbm.xml ... Bud08Entity.hbm.xml.
My service is implemented as follow:
#Async
#Slf4j
#Service
public class Bud01Service {
#Autowired
private Bud01Repository bud01Repository;
public CompletableFuture<List<Bud01Entity>> findBud(String codService, String codAnnee) {
log.info("Running findBud for service {} and annee {}", codService, codAnnee);
List<Bud01Entity> data = bud01Repository.findBud01(codService, codAnnee);
return CompletableFuture.completedFuture(data);
}
}
Same as the other, same logic for Bud02Service, ...Bud08Service.
My repository interface is as follow:
#Repository
public interface Bud01Repository extends JpaRepository<Bud01Entity, Double> {
List<Bud01Entity> findBud01(#Param("codService") String codService, #Param("codAnnee") String codAnnee);
}
Each of this 8 results represent attributes of BudAllEntity:
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class BudAllEntity {
private Double mntBudgetProduction;
private Double mntBudgetMB;
private Double mntBudgetRegul;
private Double mntBudgetFraisGenAct;
private Double mntBudgetFraisGenSer;
private Double mntBudgetFraisGenReg;
private Double mntBudgetFraisGenSie;
private Double mntBudgetFraisFin;
}
And my aggregation endpoint:
#Slf4j
#RestController
#RequestMapping("/AggBud")
public class BudAggEndpoint {
#Autowired
private Bud01Service bud01Service;
#Autowired
private Bud02Service bud02Service;
#Autowired
private Bud03Service bud03Service;
#Autowired
private Bud04Service bud04Service;
#Autowired
private Bud05Service bud05Service;
#Autowired
private Bud06Service bud06Service;
#Autowired
private Bud07Service bud07Service;
#Autowired
private Bud08Service bud08Service;
#GetMapping(value = "details", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Page<BudAllEntity> findBudClient(#RequestParam(value = "codService") String codService,
#RequestParam(value = "codAnnee") String codAnnee,
#RequestParam(required = false) Pageable pageRequest) throws ExecutionException, InterruptedException {
CompletableFuture<List<Bud01Entity>> att1 = bud01Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud02Entity>> att2 = bud02Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud03Entity>> att3 = bud03Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud04Entity>> att4 = bud04Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud05Entity>> att5 = bud05Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud06Entity>> att6 = bud06Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud07Entity>> att7 = bud07Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud08Entity>> att8 = bud08Service.findBud(codService, codAnnee);
CompletableFuture.allOf(att1, att2, att3, att4, att5, att6, att7, att8);
log.info("Retrieve Budgets");
List<BudAllEntity> finalResult = new ArrayList<BudAllEntity>();
BudAllEntity result = new BudAllEntity();
try{
result.setMntBudgetProduction(att1.get().get(0).getMntBudgetProduction());
result.setMntBudgetMB(att2.get().get(0).getMntBudgetMB());
result.setMntBudgetRegul(att3.get().get(0).getMntBudgetRegul());
result.setMntBudgetFraisGenAct(att4.get().get(0).getMntBudgetFraisGenAct());
result.setMntBudgetFraisGenReg(att5.get().get(0).getMntBudgetFraisGenSer());
result.setMntBudgetFraisGenSer(att6.get().get(0).getMntBudgetFraisGenReg());
result.setMntBudgetFraisGenSie(att7.get().get(0).getMntBudgetFraisGenSie());
result.setMntBudgetFraisFin(att8.get().get(0).getMntBudgetFraisFin());
} catch (Exception e){
log.error(e.getMessage(),e);
}
finalResult.add(result);
Pageable page = Optional.ofNullable(pageRequest).orElse(PageRequest.of(0, 20));
return new ResponsePage<>(finalResult, page, finalResult.size());
}
}
I have added #EnableAsync annotation in my main class.
Is there a way to make this code more performant, more simple ?
Thanks in advance for your help
The approach you are following looks pretty good, but there are some minute mistakes you need to take care
#Async Annotation It should be annotated on method not class
it must be applied to public methods only
self-invocation – calling the async method from within the same class – won't work
The reasons are simple – the method needs to be public so that it can be proxied. And self-invocation doesn't work because it bypasses the proxy and calls the underlying method directly.
Override the Executor :
By default, Spring uses a SimpleAsyncTaskExecutor to actually run these methods asynchronously.
Override the default executor
#Configuration
#EnableAsync
public class SpringAsyncConfig {
#Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setMaxPoolSize(10);
return pool;
}
}
Then the executor name should be provided as an attribute in #Async:
#Async("threadPoolTaskExecutor")
public void asyncMethodWithConfiguredExecutor() {
System.out.println("Execute method with configured executor - "
+ Thread.currentThread().getName());
}
Note : Make sure you create executor thread pool with N of threads that matches to cpu resources here
I am trying to only fetch newly created messages from a reactive Mongodb repository using Spring Data.
The client is fetching the messages via SSE. I am using an "after"-query, which should only return messages which were sent after "LocalDateTime.now()".
Unfortunately the SSE is pushing out old messages which are older than "now", too. I have no clue why it returns those old messages.
My controller method:
#GetMapping(value = "/receiving-sse", produces = "text/event-stream")
public Flux<Message> streamEvents() {
Mono<String> username = getUsernameFromAuth();
Flux<Message> message = findOrCreateUser(username)
.flatMapMany(user -> messageRepository
.findAllBySenderIdOrReceiverIdAndSentAtAfter(user.getId(), user.getId(), LocalDateTime.now()));
Flux<Message> heartBeat = Flux.interval(Duration.ofSeconds(30)).map(sequence -> {
Message heartBeatMessage = new Message();
heartBeatMessage.setHeartbeat(true);
return heartBeatMessage;
});
return Flux.merge(message, heartBeat);
}
My repository:
public interface MessageRepository extends ReactiveMongoRepository<Message, String> {
Flux<Message> findAllByReceiverId(String receiverId);
#Tailable
Flux<Message> findAllBySenderIdOrReceiverIdAndSentAtAfter(String senderId, String receiverId, LocalDateTime sentAt);
Flux<Message> findAllBySenderId(String senderId);
Flux<Message> findAllByIdIn(Collection<String> ids);
}
And my document:
#Data
#Document
public class Message {
private String id;
private LocalDateTime sentAt;
private String message;
private boolean heartbeat;
#DBRef
private User sender;
#DBRef
private User receiver;
}
Any hints on why the repo is fetching messages that have a "sentAt" older than "LocalDateTime.now()" is much appreciated.
Problem is in LocalDateTime.now().
LocalDateTime creates a time during initialization of a Flux<Message> message variable.
You need to replace it with construction like Mono.defer(). It will create LocalDateTime in every subscribe.
More you can read here
I have a simple model class:
#Entity
public class Task {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Size(min = 1, max = 80)
#NotNull
private String text;
#NotNull
private boolean isCompleted;
And here is my Spring Rest Data repository:
#CrossOrigin // TODO: configure specific domains
#RepositoryRestResource(collectionResourceRel = "task", path
= "task")
public interface TaskRepository extends CrudRepository<Task,
Long> {
}
So just as a sanity check, I was creating some tests to verify the end-points that are automatically created. post, delete, and get works just fine. I am however unable to properly update the isCompleted property.
Here are my test methods. The FIRST one passes no problem, but the SECOND one fails.
#Test
void testUpdateTaskText() throws Exception {
Task task1 = new Task("task1");
taskRepository.save(task1);
// update task text and hit update end point
task1.setText("updatedText");
String json = gson.toJson(task1);
this.mvc.perform(patch("/task/1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(json))
.andExpect(status().isNoContent());
// Pull the task from the repository and verify text="updatedText"
Task updatedTask = taskRepository.findById((long) 1).get();
assertEquals("updatedText", updatedTask.getText());
}
#Test
void testUpdateTaskCompleted() throws Exception {
Task task1 = new Task("task1");
task1.setCompleted(false);
taskRepository.save(task1);
// ensure repository properly stores isCompleted = false
Task updatedTask = taskRepository.findById((long) 1).get();
assertFalse(updatedTask.isCompleted());
//Update isCompleted = true and hit update end point
task1.setCompleted(true);
String json = gson.toJson(task1);
this.mvc.perform(patch("/task/1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(json))
.andExpect(status().isNoContent());
// Pull the task from the repository and verify isCompleted=true
updatedTask = taskRepository.findById((long) 1).get();
assertTrue(updatedTask.isCompleted());
}
EDIT: Modified test methods to be clear.
Finally figured out. Turns out the getter and setter in my model class was named incorrectly.
They should have been:
public boolean getIsCompleted() {
return isCompleted;
}
public void setIsCompleted(boolean isCompleted) {
this.isCompleted = isCompleted;
}
Found the answer per this SO Post:
JSON Post request for boolean field sends false by default