├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.md ├── images └── custom-soft-deletes-1.png ├── mvnw ├── mvnw.cmd ├── pom.xml └── src └── main ├── java └── com │ └── piinalpin │ └── customsoftdeletes │ ├── CustomSoftDeletesApplication.java │ ├── config │ └── CustomJpaRepositoryFactoryBean.java │ ├── constant │ └── AppConstant.java │ ├── entity │ ├── Author.java │ ├── Book.java │ ├── BookDetail.java │ ├── Transaction.java │ ├── TransactionDetail.java │ └── base │ │ ├── BaseEntity.java │ │ └── BaseEntityWithDeletedAt.java │ ├── http │ ├── controller │ │ ├── AuthorController.java │ │ ├── BookController.java │ │ └── TransactionController.java │ └── dto │ │ ├── AuthorRequest.java │ │ ├── BookRequest.java │ │ ├── TransactionDetailRequest.java │ │ ├── TransactionRequest.java │ │ └── base │ │ └── BaseResponse.java │ ├── repository │ ├── AuthorRepository.java │ ├── BookDetailRepository.java │ ├── BookRepository.java │ ├── TransactionDetailRepository.java │ ├── TransactionRepository.java │ └── softdeletes │ │ ├── SoftDeletesRepository.java │ │ └── SoftDeletesRepositoryImpl.java │ ├── service │ ├── AuthorService.java │ ├── BookService.java │ └── TransactionService.java │ └── util │ └── ResponseUtil.java └── resources ├── META-INF └── additional-spring-configuration-metadata.json └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piinalpin/springboot-data-jpa-soft-delete/d2322f101d38ba25977337fe6a80da9e6a0d1929/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Deleting data permanently from a table is a common requirement when interacting with database. But, sometimes there are business requirements to not permanently delete data from the database. The solution is we just hide that data so that can't be accessed from the front-end. 4 | 5 | In this documentation, I will share how I implementing custom JPA repository with soft deletes using `JpaRepositoryFactoryBean`. So, that data can be tracked or audited when is created, updated, or deleted. For example, let's design a table with a book sale case study like this. There are `created_at`, `created_by`, `updated_at` and `deleted_at` fields. Some case `updated_at` can be replace with `modified_at` and `modified_by`. But, the point is `deleted_at` field. 6 | 7 | ![Book Sale ERD](images/custom-soft-deletes-1.png) 8 | 9 | ## Project Setup and Dependency 10 | I'm depending [Spring Initializr](https://start.spring.io/) for this as it is much easier. 11 | 12 | We need `spring-boot-starter-data-jpa`, `spring-boot-starter-web`, `lombok` and `h2database`. There is my `pom.xml`. 13 | 14 | ```xml 15 | 16 | org.springframework.boot 17 | spring-boot-starter-data-jpa 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-web 22 | 23 | 24 | 25 | org.projectlombok 26 | lombok 27 | true 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-test 32 | test 33 | 34 | 35 | 36 | com.h2database 37 | h2 38 | runtime 39 | 40 | ``` 41 | 42 | Change configuration `application.properties` file like following below. 43 | 44 | ```sh 45 | server.port=8080 46 | spring.application.name=custom-soft-deletes 47 | server.servlet.context-path=/api 48 | 49 | spring.datasource.url=jdbc:h2:mem:db; 50 | spring.datasource.driverClassName=org.h2.Driver 51 | spring.datasource.username=sa 52 | spring.datasource.password=password 53 | spring.jpa.database-platform=org.hibernate.dialect.H2Dialect 54 | spring.h2.console.enabled=true 55 | spring.jpa.show-sql=true 56 | ``` 57 | 58 | ## Implementation 59 | 60 | **Soft Deletes Repository Interface** 61 | 62 | Create an interface `SoftDeletesRepository` which will be used to replace the repository that inherit from `JpaRepository`. 63 | 64 | ```java 65 | @SuppressWarnings("java:S119") 66 | @Transactional 67 | @NoRepositoryBean 68 | public interface SoftDeletesRepository extends PagingAndSortingRepository { 69 | 70 | @Override 71 | Iterable findAll(); 72 | 73 | @Override 74 | Iterable findAll(Sort sort); 75 | 76 | @Override 77 | Page findAll(Pageable page); 78 | 79 | Optional findOne(ID id); 80 | 81 | @Modifying 82 | void delete(ID id); 83 | 84 | @Override 85 | @Modifying 86 | void delete(T entity); 87 | 88 | void hardDelete(T entity); 89 | 90 | } 91 | ``` 92 | 93 | Create an implementation from `SoftDeletesRepository` interface class. 94 | 95 | ```java 96 | @SuppressWarnings("java:S119") 97 | @Slf4j 98 | public class SoftDeletesRepositoryImpl extends SimpleJpaRepository 99 | implements SoftDeletesRepository { 100 | 101 | private final JpaEntityInformation entityInformation; 102 | private final EntityManager em; 103 | private final Class domainClass; 104 | private static final String DELETED_FIELD = "deletedAt"; 105 | 106 | public SoftDeletesRepositoryImpl(Class domainClass, EntityManager em) { 107 | super(domainClass, em); 108 | this.em = em; 109 | this.domainClass = domainClass; 110 | this.entityInformation = JpaEntityInformationSupport.getEntityInformation(domainClass, em); 111 | } 112 | 113 | 114 | @Override 115 | public Optional findOne(ID id) { 116 | return Optional.empty(); 117 | } 118 | 119 | @Override 120 | public void delete(ID id) { 121 | 122 | } 123 | 124 | @Override 125 | public void hardDelete(T entity) { 126 | 127 | } 128 | } 129 | ``` 130 | 131 | Add method in `SoftDeletesRepositoryImpl` to check if field `deletedAt` is exist on super class, because some entity have `deletedAt` some case the don't have. So, I create method returning boolean to handle that. 132 | 133 | ```java 134 | private boolean isFieldDeletedAtExists() { 135 | try { 136 | domainClass.getSuperclass().getDeclaredField(DELETED_FIELD); 137 | return true; 138 | } catch (NoSuchFieldException e) { 139 | return false; 140 | } 141 | } 142 | ``` 143 | 144 | Create predicate specification class to filter entity if `deletedAt` is null. So, if translated in a native query is `SELECT * FROM table WHERE deleted_at is null`. 145 | 146 | ```java 147 | private static final class DeletedIsNUll implements Specification { 148 | 149 | private static final long serialVersionUID = -940322276301888908L; 150 | 151 | @Override 152 | public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) { 153 | return criteriaBuilder.isNull(root.get(DELETED_FIELD)); 154 | } 155 | 156 | } 157 | 158 | private static Specification notDeleted() { 159 | return Specification.where(new DeletedIsNUll<>()); 160 | } 161 | ``` 162 | 163 | Create predicate specification class to filter entity by ID. And can be reuse with `notDeleted()` or without `notDeleted()`. If I translated in sql is `SELECT * FROM table WHERE id = ?` or `SELECT * FROM table WHERE id = ? AND deletedAt is null`. 164 | 165 | ```java 166 | private static final class ByIdSpecification implements Specification { 167 | 168 | private static final long serialVersionUID = 6523470832851906115L; 169 | private final transient JpaEntityInformation entityInformation; 170 | private final transient ID id; 171 | 172 | ByIdSpecification(JpaEntityInformation entityInformation, ID id) { 173 | this.entityInformation = entityInformation; 174 | this.id = id; 175 | } 176 | 177 | @Override 178 | public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) { 179 | return cb.equal(root.get(Objects.requireNonNull(entityInformation.getIdAttribute()).getName()), id); 180 | } 181 | } 182 | ``` 183 | 184 | Then, create method to do updating `deletedAt` with `LocalDateTime.now()` when process delete data. 185 | 186 | ```java 187 | private void softDelete(ID id, LocalDateTime localDateTime) { 188 | Assert.notNull(id, "The given id must not be null!"); 189 | 190 | Optional entity = findOne(id); 191 | 192 | if (entity.isEmpty()) 193 | throw new EmptyResultDataAccessException( 194 | String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1); 195 | 196 | softDelete(entity.get(), localDateTime); 197 | } 198 | 199 | private void softDelete(T entity, LocalDateTime localDateTime) { 200 | Assert.notNull(entity, "The entity must not be null!"); 201 | 202 | CriteriaBuilder cb = em.getCriteriaBuilder(); 203 | 204 | CriteriaUpdate update = cb.createCriteriaUpdate(domainClass); 205 | 206 | Root root = update.from(domainClass); 207 | 208 | update.set(DELETED_FIELD, localDateTime); 209 | 210 | update.where( 211 | cb.equal( 212 | root.get(Objects.requireNonNull(entityInformation.getIdAttribute()).getName()), 213 | entityInformation.getId(entity) 214 | ) 215 | ); 216 | 217 | em.createQuery(update).executeUpdate(); 218 | } 219 | ``` 220 | 221 | Enhance override method `findAll()`, `findOne`, `delete()` and etc. 222 | 223 | ```java 224 | @Override 225 | public List findAll(){ 226 | if (isFieldDeletedAtExists()) return super.findAll(notDeleted()); 227 | return super.findAll(); 228 | } 229 | 230 | @Override 231 | public List findAll(Sort sort){ 232 | if (isFieldDeletedAtExists()) return super.findAll(notDeleted(), sort); 233 | return super.findAll(sort); 234 | } 235 | 236 | @Override 237 | public Page findAll(Pageable page) { 238 | if (isFieldDeletedAtExists()) return super.findAll(notDeleted(), page); 239 | return super.findAll(page); 240 | } 241 | 242 | @Override 243 | public Optional findOne(ID id) { 244 | if (isFieldDeletedAtExists()) 245 | return super.findOne(Specification.where(new ByIdSpecification<>(entityInformation, id)).and(notDeleted())); 246 | return super.findOne(Specification.where(new ByIdSpecification<>(entityInformation, id))); 247 | } 248 | 249 | @Override 250 | @Transactional 251 | public void delete(ID id) { 252 | softDelete(id, LocalDateTime.now()); 253 | } 254 | 255 | @Override 256 | @Transactional 257 | public void delete(T entity) { 258 | softDelete(entity, LocalDateTime.now()); 259 | } 260 | 261 | @Override 262 | public void hardDelete(T entity) { 263 | super.delete(entity); 264 | } 265 | ``` 266 | 267 | **Jpa Repository Factory Bean** 268 | 269 | I create a custom repository factory to replace the default `RepositoryFactoryBean` that will in turn produce a custom `RepositoryFactory`. The new repository factory will then provide your `SoftDeletesRepositoryImpl` as the implementation of any interfaces that extend the `Repository` interface, replacing the `SimpleJpaRepository` implementation I just extended. 270 | 271 | ```java 272 | @SuppressWarnings("all") 273 | public class CustomJpaRepositoryFactoryBean, S, ID extends Serializable> 274 | extends JpaRepositoryFactoryBean { 275 | 276 | public CustomJpaRepositoryFactoryBean(Class repositoryInterface) { 277 | super(repositoryInterface); 278 | } 279 | 280 | @Override 281 | protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) { 282 | return new CustomJpaRepositoryFactory(entityManager); 283 | } 284 | 285 | private static class CustomJpaRepositoryFactory extends JpaRepositoryFactory { 286 | 287 | private final EntityManager entityManager; 288 | 289 | CustomJpaRepositoryFactory(EntityManager entityManager) { 290 | super(entityManager); 291 | this.entityManager = entityManager; 292 | } 293 | 294 | @Override 295 | protected JpaRepositoryImplementation getTargetRepository(RepositoryInformation information, EntityManager entityManager) { 296 | return new SoftDeletesRepositoryImpl((Class) information.getDomainType(), this.entityManager); 297 | } 298 | 299 | @Override 300 | protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { 301 | return SoftDeletesRepositoryImpl.class; 302 | } 303 | } 304 | 305 | } 306 | ``` 307 | 308 | **Enable Custom JPA Repository Bean** 309 | 310 | Add `@EnableJpaRepositories` in the main `Application` class. 311 | 312 | ```java 313 | @SpringBootApplication 314 | @EnableJpaRepositories(repositoryFactoryBeanClass = CustomJpaRepositoryFactoryBean.class) 315 | ``` 316 | 317 | **Base Entity** 318 | 319 | Create a base entity so I can reuse it for all entity by extending the base entity. I create `BaseEntity` and `BaseEntityWithDeletedAt` which is extending from `BaseEntity`. It means the `BaseEntityWithDeletedAt` has the attributes contained in the `BaseEntity`. 320 | 321 | ```java 322 | @Data 323 | @SuperBuilder 324 | @MappedSuperclass 325 | @NoArgsConstructor 326 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 327 | public abstract class BaseEntity implements Serializable { 328 | 329 | private static final long serialVersionUID = 346886977546599767L; 330 | 331 | @Column(name = "created_at", nullable = false) 332 | private LocalDateTime createdAt; 333 | 334 | @Column(name = "created_by", nullable = false) 335 | private String createdBy; 336 | 337 | @Column(name = "updated_at") 338 | private LocalDateTime updatedAt; 339 | 340 | @PrePersist 341 | void onCreate() { 342 | this.createdAt = LocalDateTime.now(); 343 | if (createdBy == null) createdBy = AppConstant.DEFAULT_SYSTEM; 344 | } 345 | 346 | @PreUpdate 347 | void onUpdate() { 348 | this.updatedAt = LocalDateTime.now(); 349 | } 350 | 351 | } 352 | ``` 353 | 354 | ```java 355 | @EqualsAndHashCode(callSuper = true) 356 | @Data 357 | @SuperBuilder 358 | @MappedSuperclass 359 | @NoArgsConstructor 360 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 361 | public abstract class BaseEntityWithDeletedAt extends BaseEntity { 362 | 363 | private static final long serialVersionUID = 8570014337552990877L; 364 | 365 | @JsonIgnore 366 | @Column(name = "deleted_at") 367 | private LocalDateTime deletedAt; 368 | 369 | } 370 | ``` 371 | 372 | **Create Entity According Study Case** 373 | 374 | *Author* 375 | 376 | ```java 377 | @EqualsAndHashCode(callSuper = true) 378 | @Data 379 | @Entity 380 | @SuperBuilder 381 | @NoArgsConstructor 382 | @AllArgsConstructor 383 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 384 | @Table(name = "M_AUTHOR") 385 | public class Author extends BaseEntityWithDeletedAt { 386 | 387 | private static final long serialVersionUID = 5703123232205376654L; 388 | 389 | @Id 390 | @GeneratedValue(strategy = GenerationType.IDENTITY) 391 | private Long id; 392 | 393 | @Column(name = "full_name", nullable = false) 394 | private String fullName; 395 | 396 | } 397 | ``` 398 | 399 | **Create Request DTO** 400 | 401 | *AuthorRequest* 402 | 403 | ```java 404 | @Data 405 | @Builder 406 | @NoArgsConstructor 407 | @AllArgsConstructor 408 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 409 | @JsonIgnoreProperties(ignoreUnknown = true) 410 | public class AuthorRequest implements Serializable { 411 | 412 | private static final long serialVersionUID = 2120677063776280918L; 413 | 414 | private String fullName; 415 | 416 | } 417 | ``` 418 | 419 | **Create Repository, Service and Controller** 420 | 421 | *AuthorRepository* 422 | 423 | ```java 424 | @Repository 425 | public interface AuthorRepository extends SoftDeletesRepository { 426 | } 427 | ``` 428 | 429 | *AuthorService* 430 | 431 | ```java 432 | @Slf4j 433 | @Service 434 | public class AuthorService { 435 | 436 | private final AuthorRepository authorRepository; 437 | 438 | @Autowired 439 | public AuthorService(AuthorRepository authorRepository) { 440 | this.authorRepository = authorRepository; 441 | } 442 | 443 | public ResponseEntity save(AuthorRequest request) { 444 | log.info("Save new author: {}", request); 445 | Author author = Author.builder() 446 | .fullName(request.getFullName()) 447 | .build(); 448 | return ResponseEntity.ok().body(authorRepository.save(author)); 449 | } 450 | 451 | public ResponseEntity getAll() { 452 | log.info("Get all author"); 453 | return ResponseEntity.ok().body(authorRepository.findAll()); 454 | } 455 | 456 | } 457 | ``` 458 | 459 | *AuthorController* 460 | 461 | ```java 462 | @RestController 463 | @RequestMapping(value = "/author", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) 464 | public class AuthorController { 465 | 466 | private final AuthorService authorService; 467 | 468 | public AuthorController(AuthorService authorService) { 469 | this.authorService = authorService; 470 | } 471 | 472 | @PostMapping(value = "") 473 | public ResponseEntity createAuthor(@RequestBody AuthorRequest request) { 474 | return authorService.save(request); 475 | } 476 | 477 | @GetMapping(value = "") 478 | public ResponseEntity getAllAuthor() { 479 | return authorService.getAll(); 480 | } 481 | 482 | } 483 | ``` 484 | 485 | ## Spring Boot JPA Relational 486 | 487 | **Many-to-One and One-to-One** 488 | 489 | Let's see our ERD in the top page, there are `Many-to-One`, `One-to-Many`, `Many-to-Many` and `One-to-One` relationship. I will implement the relation of `M_AUTHOR`, `M_BOOK` and `M_BOOK_DETAIL` first. 490 | 491 | **Entity Class** 492 | 493 | *Book* 494 | 495 | ```java 496 | @EqualsAndHashCode(callSuper = true) 497 | @Data 498 | @Entity 499 | @SuperBuilder 500 | @NoArgsConstructor 501 | @AllArgsConstructor 502 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 503 | @Table(name = "M_BOOK") 504 | public class Book extends BaseEntityWithDeletedAt { 505 | 506 | private static final long serialVersionUID = 3000665212891573963L; 507 | 508 | @Id 509 | @GeneratedValue(strategy = GenerationType.IDENTITY) 510 | private Long id; 511 | 512 | @ManyToOne 513 | @JoinColumn(name = "author_id", nullable = false) 514 | private Author author; 515 | 516 | @Column(name = "title", nullable = false) 517 | private String title; 518 | 519 | @Column(name = "price", nullable = false) 520 | private Integer price; 521 | 522 | @JsonIgnore 523 | @OneToOne(cascade = CascadeType.ALL) 524 | private BookDetail detail; 525 | 526 | } 527 | ``` 528 | 529 | *Author* 530 | 531 | Add this method to mapping an author have books. 532 | 533 | ```java 534 | @JsonIgnore 535 | @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "author") 536 | private List books; 537 | ``` 538 | 539 | *BookDetail* 540 | 541 | ```java 542 | @EqualsAndHashCode(callSuper = true) 543 | @Data 544 | @Entity 545 | @SuperBuilder 546 | @NoArgsConstructor 547 | @AllArgsConstructor 548 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 549 | @Table(name = "M_BOOK_DETAIL") 550 | public class BookDetail extends BaseEntityWithDeletedAt { 551 | 552 | private static final long serialVersionUID = -4930414280222129820L; 553 | 554 | /** 555 | * @Id column should exists for one to one relationship 556 | */ 557 | @Id 558 | @JsonIgnore 559 | @GeneratedValue(strategy = GenerationType.IDENTITY) 560 | private Long bookId; 561 | 562 | @OneToOne(mappedBy = "detail") 563 | @JoinColumn(name = "book_id", nullable = false) 564 | private Book book; 565 | 566 | @Column(name = "page", nullable = false) 567 | private Integer page; 568 | 569 | @Column(name = "weight", nullable = false) 570 | private Integer weight; 571 | 572 | } 573 | ``` 574 | 575 | In this case, `BookDetail` should not have field `bookId` but the JPA entity should have an id. So, I added `bookId` as id field but it is ignored from json. 576 | 577 | *BookRequest* 578 | 579 | ```java 580 | @Data 581 | @Builder 582 | @NoArgsConstructor 583 | @AllArgsConstructor 584 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 585 | @JsonIgnoreProperties(ignoreUnknown = true) 586 | public class BookRequest implements Serializable { 587 | 588 | private static final long serialVersionUID = 7993247371386533518L; 589 | 590 | private Long authorId; 591 | 592 | private String title; 593 | 594 | private Integer price; 595 | 596 | private Integer page; 597 | 598 | private Integer weight; 599 | 600 | } 601 | ``` 602 | 603 | **Repository, Service and Controller** 604 | 605 | *BookRepository* 606 | 607 | ```java 608 | @Repository 609 | public interface BookRepository extends SoftDeletesRepository { 610 | } 611 | ``` 612 | 613 | *BookDetailRepository* 614 | 615 | ```java 616 | @Repository 617 | public interface BookDetailRepository extends SoftDeletesRepository { 618 | } 619 | ``` 620 | 621 | *BookService* 622 | 623 | ```java 624 | @Slf4j 625 | @Service 626 | public class BookService { 627 | 628 | private final AuthorRepository authorRepository; 629 | private final BookRepository bookRepository; 630 | private final BookDetailRepository bookDetailRepository; 631 | 632 | @Autowired 633 | public BookService(AuthorRepository authorRepository, BookRepository bookRepository, 634 | BookDetailRepository bookDetailRepository) { 635 | this.authorRepository = authorRepository; 636 | this.bookRepository = bookRepository; 637 | this.bookDetailRepository = bookDetailRepository; 638 | } 639 | 640 | public ResponseEntity addBook(BookRequest request) { 641 | log.info("Save new book: {}", request); 642 | 643 | log.info("Find author by author id"); 644 | Optional author = authorRepository.findOne(request.getAuthorId()); 645 | if (author.isEmpty()) return ResponseEntity.notFound().build(); 646 | 647 | Book book = Book.builder() 648 | .author(author.get()) 649 | .detail(BookDetail.builder() 650 | .page(request.getPage()) 651 | .weight(request.getWeight()) 652 | .build()) 653 | .title(request.getTitle()) 654 | .price(request.getPrice()) 655 | .build(); 656 | return ResponseEntity.ok().body(bookRepository.save(book)); 657 | } 658 | 659 | public ResponseEntity getAllBook() { 660 | return ResponseEntity.ok().body(bookRepository.findAll()); 661 | } 662 | 663 | public ResponseEntity getBookDetail(Long bookId) { 664 | log.info("Find book detail by book id: {}", bookId); 665 | Optional bookDetail = bookDetailRepository.findOne(bookId); 666 | if (bookDetail.isEmpty()) return ResponseEntity.badRequest().body(Map.ofEntries(Map.entry("message", "Data not found"))); 667 | 668 | return ResponseEntity.ok().body(bookDetail.get()); 669 | } 670 | 671 | public ResponseEntity deleteBook(Long bookId) { 672 | log.info("Find book detail by book id for delete: {}", bookId); 673 | try { 674 | bookDetailRepository.delete(bookId); 675 | bookRepository.delete(bookId); 676 | } catch (EmptyResultDataAccessException e) { 677 | log.error("Data not found. Error: {}", e.getMessage()); 678 | return ResponseEntity.badRequest().body(Map.ofEntries(Map.entry("message", "Data not found"))); 679 | } 680 | return ResponseEntity.ok().body(Map.ofEntries(Map.entry("message", "ok"))); 681 | } 682 | 683 | public ResponseEntity updatePrice(BookRequest request, Long bookId) { 684 | log.info("Update price: {}", request); 685 | Optional book = bookRepository.findOne(bookId); 686 | if (book.isEmpty()) return ResponseEntity.badRequest().body(Map.ofEntries(Map.entry("message", "Data not found"))); 687 | 688 | book.get().setPrice(request.getPrice()); 689 | bookRepository.save(book.get()); 690 | return ResponseEntity.ok().body(book.get()); 691 | } 692 | 693 | } 694 | ``` 695 | 696 | *BookController* 697 | 698 | ```java 699 | @RestController 700 | @RequestMapping(value = "/book", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) 701 | public class BookController { 702 | 703 | private final BookService bookService; 704 | 705 | public BookController(BookService bookService) { 706 | this.bookService = bookService; 707 | } 708 | 709 | @PostMapping(value = "") 710 | public ResponseEntity addBook(@RequestBody BookRequest request) { 711 | return bookService.addBook(request); 712 | } 713 | 714 | @GetMapping(value = "") 715 | public ResponseEntity getAllBooks() { 716 | return bookService.getAllBook(); 717 | } 718 | 719 | @GetMapping(value = "/detail/{id}") 720 | public ResponseEntity getBookDetail(@PathVariable(value = "id") Long bookId) { 721 | return bookService.getBookDetail(bookId); 722 | } 723 | 724 | @DeleteMapping(value = "/{id}") 725 | public ResponseEntity deleteBook(@PathVariable(value = "id") Long bookId) { 726 | return bookService.deleteBook(bookId); 727 | } 728 | 729 | @PostMapping(value = "/{id}") 730 | public ResponseEntity updatePrice(@PathVariable(value = "id") Long bookId, 731 | @RequestBody BookRequest request) { 732 | return bookService.updatePrice(request, bookId); 733 | } 734 | 735 | } 736 | ``` 737 | 738 | **One-to-Many and Many-to-Many** 739 | 740 | Let's see `T_TRANSACTION` and `T_TRANSACTION_DETAIL`, they have `One-to-Many` relationship between `T_TRANSACTION` and `T_TRANSACTION_DETAIL`. Also `T_TRANSACTION_DETAIL` have `Many-To-Many` relationship between `T_TRANSACTION` and `M_BOOK` that means `T_TRANSACTION_DETAIL` have two primary keys, namely `transaction_id` and `book_id`. 741 | 742 | **Entity and DTO Request Class** 743 | 744 | *Transaction* 745 | 746 | ```java 747 | @EqualsAndHashCode(callSuper = true) 748 | @Data 749 | @Entity 750 | @SuperBuilder 751 | @NoArgsConstructor 752 | @AllArgsConstructor 753 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 754 | @Table(name = "T_TRANSACTION") 755 | public class Transaction extends BaseEntity { 756 | 757 | private static final long serialVersionUID = 6417258128520039672L; 758 | 759 | @Id 760 | @GeneratedValue(strategy = GenerationType.IDENTITY) 761 | private Long id; 762 | 763 | @Column(name = "customer_name", nullable = false) 764 | private String customerName; 765 | 766 | @Column(name = "transaction_date", nullable = false) 767 | private LocalDateTime transactionDate; 768 | 769 | @Column(name = "total_price", nullable = false) 770 | private Integer totalPrice; 771 | 772 | @Column(name = "total_qty", nullable = false) 773 | private Integer totalQty; 774 | 775 | @JsonIgnore 776 | @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "transaction") 777 | private List transactionDetails; 778 | 779 | } 780 | ``` 781 | 782 | *Book* 783 | 784 | Add this method to mapping a book have transaction details. 785 | 786 | ```java 787 | @JsonIgnore 788 | @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "book") 789 | private List transactionDetails; 790 | ``` 791 | 792 | *TransactionDetail* 793 | 794 | ```java 795 | @EqualsAndHashCode(callSuper = true) 796 | @Data 797 | @Entity 798 | @SuperBuilder 799 | @NoArgsConstructor 800 | @AllArgsConstructor 801 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 802 | @Table(name = "T_TRANSACTION_DETAIL") 803 | @IdClass(TransactionDetail.TransactionDetailId.class) 804 | public class TransactionDetail extends BaseEntity { 805 | 806 | private static final long serialVersionUID = -2700555234966165635L; 807 | 808 | @Data 809 | @NoArgsConstructor 810 | @AllArgsConstructor 811 | public static class TransactionDetailId implements Serializable { 812 | private static final long serialVersionUID = 2209912596164063361L; 813 | private Long transaction; 814 | private Long book; 815 | } 816 | 817 | @Id 818 | @ManyToOne 819 | @JoinColumn(name = "transaction_id", nullable = false) 820 | private Transaction transaction; 821 | 822 | @Id 823 | @ManyToOne 824 | @JoinColumn(name = "book_id", nullable = false) 825 | private Book book; 826 | 827 | @Column(name = "qty", nullable = false) 828 | private Integer qty; 829 | 830 | @Column(name = "price", nullable = false) 831 | private Integer price; 832 | 833 | } 834 | ``` 835 | 836 | In this case, because `TransactionDetail` have composite primary keys. I should define an id class `TransactionDetail.TransactionDetailId` to map a primary keys and annotate `TransactionDetail` with `@IdClass`. 837 | 838 | *TransactionDetailRequest* 839 | 840 | ```java 841 | @Data 842 | @Builder 843 | @NoArgsConstructor 844 | @AllArgsConstructor 845 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 846 | @JsonIgnoreProperties(ignoreUnknown = true) 847 | public class TransactionDetailRequest implements Serializable { 848 | 849 | private static final long serialVersionUID = 3141178093304012075L; 850 | 851 | private Long bookId; 852 | 853 | private Integer qty; 854 | 855 | } 856 | ``` 857 | 858 | *TransactionRequest* 859 | 860 | ```java 861 | @Data 862 | @Builder 863 | @NoArgsConstructor 864 | @AllArgsConstructor 865 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 866 | @JsonIgnoreProperties(ignoreUnknown = true) 867 | public class TransactionRequest implements Serializable { 868 | 869 | private static final long serialVersionUID = 122662932230379345L; 870 | 871 | private String customerName; 872 | 873 | private Long transactionId; 874 | 875 | private Long bookId; 876 | 877 | private List details; 878 | 879 | } 880 | ``` 881 | 882 | **Repository and Service Class** 883 | 884 | *TransactionRepository* 885 | 886 | ```java 887 | @Repository 888 | public interface TransactionRepository extends SoftDeletesRepository { 889 | } 890 | ``` 891 | 892 | *TransactionDetailRepository* 893 | 894 | ```java 895 | @Repository 896 | public interface TransactionDetailRepository extends SoftDeletesRepository { 897 | 898 | List findAllByTransactionId(Long transactionId); 899 | 900 | } 901 | ``` 902 | 903 | *TransactionService* 904 | 905 | ```java 906 | @Slf4j 907 | @Service 908 | public class TransactionService { 909 | 910 | private final BookRepository bookRepository; 911 | private final TransactionRepository transactionRepository; 912 | private final TransactionDetailRepository transactionDetailRepository; 913 | 914 | @Autowired 915 | public TransactionService(BookRepository bookRepository, TransactionRepository transactionRepository, 916 | TransactionDetailRepository transactionDetailRepository) { 917 | this.bookRepository = bookRepository; 918 | this.transactionRepository = transactionRepository; 919 | this.transactionDetailRepository = transactionDetailRepository; 920 | } 921 | 922 | public ResponseEntity createTransaction(TransactionRequest request) { 923 | Transaction transaction = Transaction.builder() 924 | .transactionDate(LocalDateTime.now()) 925 | .customerName(request.getCustomerName()) 926 | .build(); 927 | List details = new ArrayList<>(); 928 | for (TransactionDetailRequest detailRequest : request.getDetails()) { 929 | log.info("Find book by bookId"); 930 | Optional book = bookRepository.findOne(detailRequest.getBookId()); 931 | if (book.isPresent()) { 932 | Integer price = book.get().getPrice() * detailRequest.getQty(); 933 | details.add(TransactionDetail.builder() 934 | .transaction(transaction) 935 | .book(book.get()) 936 | .price(price) 937 | .qty(detailRequest.getQty()) 938 | .build()); 939 | } 940 | } 941 | transaction.setTotalPrice(details.stream().mapToInt(TransactionDetail::getPrice).sum()); 942 | transaction.setTotalQty(details.stream().mapToInt(TransactionDetail::getQty).sum()); 943 | transaction.setTransactionDetails(details); 944 | transactionRepository.save(transaction); 945 | return ResponseEntity.ok().body(transaction); 946 | } 947 | 948 | public ResponseEntity getTransactionDetails(Long transactionId) { 949 | return ResponseEntity.ok().body(transactionDetailRepository.findAllByTransactionId(transactionId)); 950 | } 951 | 952 | } 953 | ``` 954 | 955 | **Controller** 956 | 957 | *TransactionController* 958 | 959 | ```java 960 | @RestController 961 | @RequestMapping(value = "/transaction", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) 962 | public class TransactionController { 963 | 964 | private final TransactionService transactionService; 965 | 966 | @Autowired 967 | public TransactionController(TransactionService transactionService) { 968 | this.transactionService = transactionService; 969 | } 970 | 971 | @PostMapping(value = "") 972 | public ResponseEntity addTransaction(@RequestBody TransactionRequest request) { 973 | return transactionService.createTransaction(request); 974 | } 975 | 976 | @GetMapping(value = "/{id}") 977 | public ResponseEntity getTransactionDetail(@PathVariable(value = "id") Long transactionId) { 978 | return transactionService.getTransactionDetails(transactionId); 979 | } 980 | 981 | } 982 | ``` 983 | 984 | ## Reference 985 | 986 | - [Working with Spring Data Repositories](https://docs.spring.io/spring-data/data-commons/docs/1.6.1.RELEASE/reference/html/repositories.html) 987 | - [Spring Boot JPA Soft Deletes with Spring Data Rest](https://github.com/dzinot/spring-boot-jpa-data-rest-soft-delete) 988 | - [Composite Primary Keys in JPA](https://www.baeldung.com/jpa-composite-primary-keys) 989 | - [Spring Boot With H2 Database](https://www.baeldung.com/spring-boot-h2-database) -------------------------------------------------------------------------------- /images/custom-soft-deletes-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piinalpin/springboot-data-jpa-soft-delete/d2322f101d38ba25977337fe6a80da9e6a0d1929/images/custom-soft-deletes-1.png -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.6.4 9 | 10 | 11 | com.piinalpin 12 | custom-soft-deletes 13 | 0.0.1-SNAPSHOT 14 | custom-soft-deletes 15 | Demo project for Spring Boot 16 | 17 | 11 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-jpa 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | 31 | org.projectlombok 32 | lombok 33 | true 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-test 39 | test 40 | 41 | 42 | 43 | com.h2database 44 | h2 45 | runtime 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-maven-plugin 55 | 56 | 57 | 58 | org.projectlombok 59 | lombok 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/CustomSoftDeletesApplication.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes; 2 | 3 | import com.piinalpin.customsoftdeletes.config.CustomJpaRepositoryFactoryBean; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 7 | 8 | @SpringBootApplication 9 | @EnableJpaRepositories(repositoryFactoryBeanClass = CustomJpaRepositoryFactoryBean.class) 10 | public class CustomSoftDeletesApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(CustomSoftDeletesApplication.class, args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/config/CustomJpaRepositoryFactoryBean.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.config; 2 | 3 | import com.piinalpin.customsoftdeletes.repository.softdeletes.SoftDeletesRepositoryImpl; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; 6 | import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; 7 | import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation; 8 | import org.springframework.data.repository.core.RepositoryInformation; 9 | import org.springframework.data.repository.core.RepositoryMetadata; 10 | import org.springframework.data.repository.core.support.RepositoryFactorySupport; 11 | 12 | import javax.persistence.EntityManager; 13 | import java.io.Serializable; 14 | 15 | @SuppressWarnings("all") 16 | public class CustomJpaRepositoryFactoryBean, S, ID extends Serializable> 17 | extends JpaRepositoryFactoryBean { 18 | 19 | public CustomJpaRepositoryFactoryBean(Class repositoryInterface) { 20 | super(repositoryInterface); 21 | } 22 | 23 | @Override 24 | protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) { 25 | return new CustomJpaRepositoryFactory(entityManager); 26 | } 27 | 28 | private static class CustomJpaRepositoryFactory extends JpaRepositoryFactory { 29 | 30 | private final EntityManager entityManager; 31 | 32 | CustomJpaRepositoryFactory(EntityManager entityManager) { 33 | super(entityManager); 34 | this.entityManager = entityManager; 35 | } 36 | 37 | @Override 38 | protected JpaRepositoryImplementation getTargetRepository(RepositoryInformation information, EntityManager entityManager) { 39 | return new SoftDeletesRepositoryImpl((Class) information.getDomainType(), this.entityManager); 40 | } 41 | 42 | @Override 43 | protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { 44 | return SoftDeletesRepositoryImpl.class; 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/constant/AppConstant.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.constant; 2 | 3 | public class AppConstant { 4 | 5 | private AppConstant() {} 6 | 7 | public static final String DEFAULT_SYSTEM = "SYSTEM"; 8 | 9 | public enum ResponseCode { 10 | 11 | SUCCESS("Success!"), 12 | DATA_NOT_FOUND("Data not found!"), 13 | UNKNOWN_ERROR("Happened error!"); 14 | 15 | private final String message; 16 | 17 | private ResponseCode(String message) { 18 | this.message = message; 19 | } 20 | 21 | public String getMessage() { 22 | return this.message; 23 | } 24 | 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/entity/Author.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 5 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 6 | import com.piinalpin.customsoftdeletes.entity.base.BaseEntityWithDeletedAt; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.NoArgsConstructor; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | import javax.persistence.*; 14 | import java.util.List; 15 | 16 | @EqualsAndHashCode(callSuper = true) 17 | @Data 18 | @Entity 19 | @SuperBuilder 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 23 | @Table(name = "M_AUTHOR") 24 | public class Author extends BaseEntityWithDeletedAt { 25 | 26 | private static final long serialVersionUID = 5703123232205376654L; 27 | 28 | @Id 29 | @GeneratedValue(strategy = GenerationType.IDENTITY) 30 | private Long id; 31 | 32 | @Column(name = "full_name", nullable = false) 33 | private String fullName; 34 | 35 | @JsonIgnore 36 | @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "author") 37 | private List books; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/entity/Book.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 5 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 6 | import com.piinalpin.customsoftdeletes.entity.base.BaseEntityWithDeletedAt; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.NoArgsConstructor; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | import javax.persistence.*; 14 | import java.util.List; 15 | 16 | @EqualsAndHashCode(callSuper = true) 17 | @Data 18 | @Entity 19 | @SuperBuilder 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 23 | @Table(name = "M_BOOK") 24 | public class Book extends BaseEntityWithDeletedAt { 25 | 26 | private static final long serialVersionUID = 3000665212891573963L; 27 | 28 | @Id 29 | @GeneratedValue(strategy = GenerationType.IDENTITY) 30 | private Long id; 31 | 32 | @ManyToOne 33 | @JoinColumn(name = "author_id", nullable = false) 34 | private Author author; 35 | 36 | @Column(name = "title", nullable = false) 37 | private String title; 38 | 39 | @Column(name = "price", nullable = false) 40 | private Integer price; 41 | 42 | @JsonIgnore 43 | @OneToOne(cascade = CascadeType.ALL) 44 | private BookDetail detail; 45 | 46 | @JsonIgnore 47 | @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "book") 48 | private List transactionDetails; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/entity/BookDetail.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 5 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 6 | import com.piinalpin.customsoftdeletes.entity.base.BaseEntityWithDeletedAt; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.NoArgsConstructor; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | import javax.persistence.*; 14 | 15 | @EqualsAndHashCode(callSuper = true) 16 | @Data 17 | @Entity 18 | @SuperBuilder 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 22 | @Table(name = "M_BOOK_DETAIL") 23 | public class BookDetail extends BaseEntityWithDeletedAt { 24 | 25 | private static final long serialVersionUID = -4930414280222129820L; 26 | 27 | /** 28 | * @Id column should exists for one to one relationship 29 | */ 30 | @Id 31 | @JsonIgnore 32 | @GeneratedValue(strategy = GenerationType.IDENTITY) 33 | private Long bookId; 34 | 35 | @OneToOne(mappedBy = "detail") 36 | @JoinColumn(name = "book_id", nullable = false) 37 | private Book book; 38 | 39 | @Column(name = "page", nullable = false) 40 | private Integer page; 41 | 42 | @Column(name = "weight", nullable = false) 43 | private Integer weight; 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/entity/Transaction.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 5 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 6 | import com.piinalpin.customsoftdeletes.entity.base.BaseEntity; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.NoArgsConstructor; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | import javax.persistence.*; 14 | import java.time.LocalDateTime; 15 | import java.util.List; 16 | 17 | @EqualsAndHashCode(callSuper = true) 18 | @Data 19 | @Entity 20 | @SuperBuilder 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 24 | @Table(name = "T_TRANSACTION") 25 | public class Transaction extends BaseEntity { 26 | 27 | private static final long serialVersionUID = 6417258128520039672L; 28 | 29 | @Id 30 | @GeneratedValue(strategy = GenerationType.IDENTITY) 31 | private Long id; 32 | 33 | @Column(name = "customer_name", nullable = false) 34 | private String customerName; 35 | 36 | @Column(name = "transaction_date", nullable = false) 37 | private LocalDateTime transactionDate; 38 | 39 | @Column(name = "total_price", nullable = false) 40 | private Integer totalPrice; 41 | 42 | @Column(name = "total_qty", nullable = false) 43 | private Integer totalQty; 44 | 45 | @JsonIgnore 46 | @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "transaction") 47 | private List transactionDetails; 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/entity/TransactionDetail.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.entity; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import com.piinalpin.customsoftdeletes.entity.base.BaseEntity; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.NoArgsConstructor; 10 | import lombok.experimental.SuperBuilder; 11 | 12 | import javax.persistence.*; 13 | import java.io.Serializable; 14 | 15 | @EqualsAndHashCode(callSuper = true) 16 | @Data 17 | @Entity 18 | @SuperBuilder 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 22 | @Table(name = "T_TRANSACTION_DETAIL") 23 | @IdClass(TransactionDetail.TransactionDetailId.class) 24 | public class TransactionDetail extends BaseEntity { 25 | 26 | private static final long serialVersionUID = -2700555234966165635L; 27 | 28 | @Data 29 | @NoArgsConstructor 30 | @AllArgsConstructor 31 | public static class TransactionDetailId implements Serializable { 32 | private static final long serialVersionUID = 2209912596164063361L; 33 | private Long transaction; 34 | private Long book; 35 | } 36 | 37 | @Id 38 | @ManyToOne 39 | @JoinColumn(name = "transaction_id", nullable = false) 40 | private Transaction transaction; 41 | 42 | @Id 43 | @ManyToOne 44 | @JoinColumn(name = "book_id", nullable = false) 45 | private Book book; 46 | 47 | @Column(name = "qty", nullable = false) 48 | private Integer qty; 49 | 50 | @Column(name = "price", nullable = false) 51 | private Integer price; 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/entity/base/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.entity.base; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import com.piinalpin.customsoftdeletes.constant.AppConstant; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import lombok.experimental.SuperBuilder; 9 | 10 | import javax.persistence.Column; 11 | import javax.persistence.MappedSuperclass; 12 | import javax.persistence.PrePersist; 13 | import javax.persistence.PreUpdate; 14 | import java.io.Serializable; 15 | import java.time.LocalDateTime; 16 | 17 | @Data 18 | @SuperBuilder 19 | @MappedSuperclass 20 | @NoArgsConstructor 21 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 22 | public abstract class BaseEntity implements Serializable { 23 | 24 | private static final long serialVersionUID = 346886977546599767L; 25 | 26 | @Column(name = "created_at", nullable = false) 27 | private LocalDateTime createdAt; 28 | 29 | @Column(name = "created_by", nullable = false) 30 | private String createdBy; 31 | 32 | @Column(name = "updated_at") 33 | private LocalDateTime updatedAt; 34 | 35 | @PrePersist 36 | void onCreate() { 37 | this.createdAt = LocalDateTime.now(); 38 | if (createdBy == null) createdBy = AppConstant.DEFAULT_SYSTEM; 39 | } 40 | 41 | @PreUpdate 42 | void onUpdate() { 43 | this.updatedAt = LocalDateTime.now(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/entity/base/BaseEntityWithDeletedAt.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.entity.base; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 5 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.SuperBuilder; 10 | 11 | import javax.persistence.Column; 12 | import javax.persistence.MappedSuperclass; 13 | import java.time.LocalDateTime; 14 | 15 | @EqualsAndHashCode(callSuper = true) 16 | @Data 17 | @SuperBuilder 18 | @MappedSuperclass 19 | @NoArgsConstructor 20 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 21 | public abstract class BaseEntityWithDeletedAt extends BaseEntity { 22 | 23 | private static final long serialVersionUID = 8570014337552990877L; 24 | 25 | @JsonIgnore 26 | @Column(name = "deleted_at") 27 | private LocalDateTime deletedAt; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/http/controller/AuthorController.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.http.controller; 2 | 3 | import com.piinalpin.customsoftdeletes.http.dto.AuthorRequest; 4 | import com.piinalpin.customsoftdeletes.service.AuthorService; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | @RestController 10 | @RequestMapping(value = "/author", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) 11 | public class AuthorController { 12 | 13 | private final AuthorService authorService; 14 | 15 | public AuthorController(AuthorService authorService) { 16 | this.authorService = authorService; 17 | } 18 | 19 | @PostMapping(value = "") 20 | public ResponseEntity createAuthor(@RequestBody AuthorRequest request) { 21 | return authorService.save(request); 22 | } 23 | 24 | @GetMapping(value = "") 25 | public ResponseEntity getAllAuthor() { 26 | return authorService.getAll(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/http/controller/BookController.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.http.controller; 2 | 3 | import com.piinalpin.customsoftdeletes.http.dto.BookRequest; 4 | import com.piinalpin.customsoftdeletes.service.BookService; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | @RestController 10 | @RequestMapping(value = "/book", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) 11 | public class BookController { 12 | 13 | private final BookService bookService; 14 | 15 | public BookController(BookService bookService) { 16 | this.bookService = bookService; 17 | } 18 | 19 | @PostMapping(value = "") 20 | public ResponseEntity addBook(@RequestBody BookRequest request) { 21 | return bookService.addBook(request); 22 | } 23 | 24 | @GetMapping(value = "") 25 | public ResponseEntity getAllBooks() { 26 | return bookService.getAllBook(); 27 | } 28 | 29 | @GetMapping(value = "/detail/{id}") 30 | public ResponseEntity getBookDetail(@PathVariable(value = "id") Long bookId) { 31 | return bookService.getBookDetail(bookId); 32 | } 33 | 34 | @DeleteMapping(value = "/{id}") 35 | public ResponseEntity deleteBook(@PathVariable(value = "id") Long bookId) { 36 | return bookService.deleteBook(bookId); 37 | } 38 | 39 | @PostMapping(value = "/{id}") 40 | public ResponseEntity updatePrice(@PathVariable(value = "id") Long bookId, 41 | @RequestBody BookRequest request) { 42 | return bookService.updatePrice(request, bookId); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/http/controller/TransactionController.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.http.controller; 2 | 3 | import com.piinalpin.customsoftdeletes.http.dto.TransactionRequest; 4 | import com.piinalpin.customsoftdeletes.service.TransactionService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | @RestController 11 | @RequestMapping(value = "/transaction", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) 12 | public class TransactionController { 13 | 14 | private final TransactionService transactionService; 15 | 16 | @Autowired 17 | public TransactionController(TransactionService transactionService) { 18 | this.transactionService = transactionService; 19 | } 20 | 21 | @PostMapping(value = "") 22 | public ResponseEntity addTransaction(@RequestBody TransactionRequest request) { 23 | return transactionService.createTransaction(request); 24 | } 25 | 26 | @GetMapping(value = "/{id}") 27 | public ResponseEntity getTransactionDetail(@PathVariable(value = "id") Long transactionId) { 28 | return transactionService.getTransactionDetails(transactionId); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/http/dto/AuthorRequest.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.http.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 5 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | import java.io.Serializable; 12 | 13 | @Data 14 | @Builder 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 18 | @JsonIgnoreProperties(ignoreUnknown = true) 19 | public class AuthorRequest implements Serializable { 20 | 21 | private static final long serialVersionUID = 2120677063776280918L; 22 | 23 | private String fullName; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/http/dto/BookRequest.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.http.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 5 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | import java.io.Serializable; 12 | 13 | @Data 14 | @Builder 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 18 | @JsonIgnoreProperties(ignoreUnknown = true) 19 | public class BookRequest implements Serializable { 20 | 21 | private static final long serialVersionUID = 7993247371386533518L; 22 | 23 | private Long authorId; 24 | 25 | private String title; 26 | 27 | private Integer price; 28 | 29 | private Integer page; 30 | 31 | private Integer weight; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/http/dto/TransactionDetailRequest.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.http.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 5 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | import java.io.Serializable; 12 | 13 | @Data 14 | @Builder 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 18 | @JsonIgnoreProperties(ignoreUnknown = true) 19 | public class TransactionDetailRequest implements Serializable { 20 | 21 | private static final long serialVersionUID = 3141178093304012075L; 22 | 23 | private Long bookId; 24 | 25 | private Integer qty; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/http/dto/TransactionRequest.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.http.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 5 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | import java.io.Serializable; 12 | import java.util.List; 13 | 14 | @Data 15 | @Builder 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 19 | @JsonIgnoreProperties(ignoreUnknown = true) 20 | public class TransactionRequest implements Serializable { 21 | 22 | private static final long serialVersionUID = 122662932230379345L; 23 | 24 | private String customerName; 25 | 26 | private Long transactionId; 27 | 28 | private Long bookId; 29 | 30 | private List details; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/http/dto/base/BaseResponse.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.http.dto.base; 2 | 3 | import java.io.Serializable; 4 | import java.time.LocalDateTime; 5 | 6 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 7 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 8 | 9 | import lombok.AllArgsConstructor; 10 | import lombok.Builder; 11 | import lombok.NoArgsConstructor; 12 | import lombok.Data; 13 | 14 | @Data 15 | @Builder 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 19 | public class BaseResponse implements Serializable { 20 | 21 | private static final long serialVersionUID = -395801934596215889L; 22 | 23 | private LocalDateTime timestamp; 24 | 25 | private String responseCode; 26 | 27 | private String message; 28 | 29 | private T data; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/repository/AuthorRepository.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.repository; 2 | 3 | import com.piinalpin.customsoftdeletes.entity.Author; 4 | import com.piinalpin.customsoftdeletes.repository.softdeletes.SoftDeletesRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface AuthorRepository extends SoftDeletesRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/repository/BookDetailRepository.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.repository; 2 | 3 | import com.piinalpin.customsoftdeletes.entity.BookDetail; 4 | import com.piinalpin.customsoftdeletes.repository.softdeletes.SoftDeletesRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface BookDetailRepository extends SoftDeletesRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/repository/BookRepository.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.repository; 2 | 3 | import com.piinalpin.customsoftdeletes.entity.Book; 4 | import com.piinalpin.customsoftdeletes.repository.softdeletes.SoftDeletesRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface BookRepository extends SoftDeletesRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/repository/TransactionDetailRepository.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.repository; 2 | 3 | import com.piinalpin.customsoftdeletes.entity.TransactionDetail; 4 | import com.piinalpin.customsoftdeletes.repository.softdeletes.SoftDeletesRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.List; 8 | 9 | @Repository 10 | public interface TransactionDetailRepository extends SoftDeletesRepository { 11 | 12 | List findAllByTransactionId(Long transactionId); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/repository/TransactionRepository.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.repository; 2 | 3 | import com.piinalpin.customsoftdeletes.entity.Transaction; 4 | import com.piinalpin.customsoftdeletes.repository.softdeletes.SoftDeletesRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface TransactionRepository extends SoftDeletesRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/repository/softdeletes/SoftDeletesRepository.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.repository.softdeletes; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.domain.Sort; 6 | import org.springframework.data.jpa.repository.Modifying; 7 | import org.springframework.data.repository.NoRepositoryBean; 8 | import org.springframework.data.repository.PagingAndSortingRepository; 9 | 10 | import javax.transaction.Transactional; 11 | import java.io.Serializable; 12 | import java.util.Optional; 13 | 14 | @SuppressWarnings("java:S119") 15 | @Transactional 16 | @NoRepositoryBean 17 | public interface SoftDeletesRepository extends PagingAndSortingRepository { 18 | 19 | @Override 20 | Iterable findAll(); 21 | 22 | @Override 23 | Iterable findAll(Sort sort); 24 | 25 | @Override 26 | Page findAll(Pageable page); 27 | 28 | Optional findOne(ID id); 29 | 30 | @Modifying 31 | void delete(ID id); 32 | 33 | @Override 34 | @Modifying 35 | void delete(T entity); 36 | 37 | void hardDelete(T entity); 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/repository/softdeletes/SoftDeletesRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.repository.softdeletes; 2 | 3 | import org.springframework.dao.EmptyResultDataAccessException; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.domain.Sort; 7 | import org.springframework.data.jpa.domain.Specification; 8 | import org.springframework.data.jpa.repository.support.JpaEntityInformation; 9 | import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport; 10 | import org.springframework.data.jpa.repository.support.SimpleJpaRepository; 11 | import org.springframework.util.Assert; 12 | 13 | import javax.persistence.EntityManager; 14 | import javax.persistence.criteria.*; 15 | import javax.transaction.Transactional; 16 | import java.io.Serializable; 17 | import java.time.LocalDateTime; 18 | import java.util.List; 19 | import java.util.Objects; 20 | import java.util.Optional; 21 | 22 | @SuppressWarnings("java:S119") 23 | public class SoftDeletesRepositoryImpl extends SimpleJpaRepository 24 | implements SoftDeletesRepository { 25 | 26 | private final JpaEntityInformation entityInformation; 27 | private final EntityManager em; 28 | private final Class domainClass; 29 | private static final String DELETED_FIELD = "deletedAt"; 30 | 31 | public SoftDeletesRepositoryImpl(Class domainClass, EntityManager em) { 32 | super(domainClass, em); 33 | this.em = em; 34 | this.domainClass = domainClass; 35 | this.entityInformation = JpaEntityInformationSupport.getEntityInformation(domainClass, em); 36 | } 37 | 38 | @Override 39 | public List findAll(){ 40 | if (isFieldDeletedAtExists()) return super.findAll(notDeleted()); 41 | return super.findAll(); 42 | } 43 | 44 | @Override 45 | public List findAll(Sort sort){ 46 | if (isFieldDeletedAtExists()) return super.findAll(notDeleted(), sort); 47 | return super.findAll(sort); 48 | } 49 | 50 | @Override 51 | public Page findAll(Pageable page) { 52 | if (isFieldDeletedAtExists()) return super.findAll(notDeleted(), page); 53 | return super.findAll(page); 54 | } 55 | 56 | @Override 57 | public Optional findOne(ID id) { 58 | if (isFieldDeletedAtExists()) 59 | return super.findOne(Specification.where(new ByIdSpecification<>(entityInformation, id)).and(notDeleted())); 60 | return super.findOne(Specification.where(new ByIdSpecification<>(entityInformation, id))); 61 | } 62 | 63 | @Override 64 | @Transactional 65 | public void delete(ID id) { 66 | softDelete(id, LocalDateTime.now()); 67 | } 68 | 69 | @Override 70 | @Transactional 71 | public void delete(T entity) { 72 | softDelete(entity, LocalDateTime.now()); 73 | } 74 | 75 | @Override 76 | public void hardDelete(T entity) { 77 | super.delete(entity); 78 | } 79 | 80 | private boolean isFieldDeletedAtExists() { 81 | try { 82 | domainClass.getSuperclass().getDeclaredField(DELETED_FIELD); 83 | return true; 84 | } catch (NoSuchFieldException e) { 85 | return false; 86 | } 87 | } 88 | 89 | private void softDelete(ID id, LocalDateTime localDateTime) { 90 | Assert.notNull(id, "The given id must not be null!"); 91 | 92 | Optional entity = findOne(id); 93 | 94 | if (entity.isEmpty()) 95 | throw new EmptyResultDataAccessException( 96 | String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1); 97 | 98 | softDelete(entity.get(), localDateTime); 99 | } 100 | 101 | private void softDelete(T entity, LocalDateTime localDateTime) { 102 | Assert.notNull(entity, "The entity must not be null!"); 103 | 104 | CriteriaBuilder cb = em.getCriteriaBuilder(); 105 | 106 | CriteriaUpdate update = cb.createCriteriaUpdate(domainClass); 107 | 108 | Root root = update.from(domainClass); 109 | 110 | update.set(DELETED_FIELD, localDateTime); 111 | 112 | update.where( 113 | cb.equal( 114 | root.get(Objects.requireNonNull(entityInformation.getIdAttribute()).getName()), 115 | entityInformation.getId(entity) 116 | ) 117 | ); 118 | 119 | em.createQuery(update).executeUpdate(); 120 | } 121 | 122 | private static final class ByIdSpecification implements Specification { 123 | 124 | private static final long serialVersionUID = 6523470832851906115L; 125 | private final transient JpaEntityInformation entityInformation; 126 | private final transient ID id; 127 | 128 | ByIdSpecification(JpaEntityInformation entityInformation, ID id) { 129 | this.entityInformation = entityInformation; 130 | this.id = id; 131 | } 132 | 133 | @Override 134 | public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) { 135 | return cb.equal(root.get(Objects.requireNonNull(entityInformation.getIdAttribute()).getName()), id); 136 | } 137 | } 138 | 139 | private static final class DeletedIsNUll implements Specification { 140 | 141 | private static final long serialVersionUID = -940322276301888908L; 142 | 143 | @Override 144 | public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) { 145 | return criteriaBuilder.isNull(root.get(DELETED_FIELD)); 146 | } 147 | 148 | } 149 | 150 | private static Specification notDeleted() { 151 | return Specification.where(new DeletedIsNUll<>()); 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/service/AuthorService.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.service; 2 | 3 | import com.piinalpin.customsoftdeletes.constant.AppConstant; 4 | import com.piinalpin.customsoftdeletes.entity.Author; 5 | import com.piinalpin.customsoftdeletes.http.dto.AuthorRequest; 6 | import com.piinalpin.customsoftdeletes.repository.AuthorRepository; 7 | import com.piinalpin.customsoftdeletes.util.ResponseUtil; 8 | 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.stereotype.Service; 14 | 15 | @Slf4j 16 | @Service 17 | public class AuthorService { 18 | 19 | private final AuthorRepository authorRepository; 20 | 21 | @Autowired 22 | public AuthorService(AuthorRepository authorRepository) { 23 | this.authorRepository = authorRepository; 24 | } 25 | 26 | public ResponseEntity save(AuthorRequest request) { 27 | log.info("Save new author: {}", request); 28 | Author author = Author.builder() 29 | .fullName(request.getFullName()) 30 | .build(); 31 | try { 32 | author = authorRepository.save(author); 33 | return ResponseUtil.build(AppConstant.ResponseCode.SUCCESS, author, HttpStatus.OK); 34 | } catch (Exception e) { 35 | return ResponseUtil.build(AppConstant.ResponseCode.UNKNOWN_ERROR, null, HttpStatus.INTERNAL_SERVER_ERROR); 36 | } 37 | } 38 | 39 | public ResponseEntity getAll() { 40 | log.info("Get all author"); 41 | return ResponseEntity.ok().body(authorRepository.findAll()); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/service/BookService.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.service; 2 | 3 | import com.piinalpin.customsoftdeletes.entity.Author; 4 | import com.piinalpin.customsoftdeletes.entity.Book; 5 | import com.piinalpin.customsoftdeletes.entity.BookDetail; 6 | import com.piinalpin.customsoftdeletes.http.dto.BookRequest; 7 | import com.piinalpin.customsoftdeletes.repository.AuthorRepository; 8 | import com.piinalpin.customsoftdeletes.repository.BookDetailRepository; 9 | import com.piinalpin.customsoftdeletes.repository.BookRepository; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.dao.EmptyResultDataAccessException; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.util.Map; 17 | import java.util.Optional; 18 | 19 | @Slf4j 20 | @Service 21 | public class BookService { 22 | 23 | private final AuthorRepository authorRepository; 24 | private final BookRepository bookRepository; 25 | private final BookDetailRepository bookDetailRepository; 26 | 27 | @Autowired 28 | public BookService(AuthorRepository authorRepository, BookRepository bookRepository, 29 | BookDetailRepository bookDetailRepository) { 30 | this.authorRepository = authorRepository; 31 | this.bookRepository = bookRepository; 32 | this.bookDetailRepository = bookDetailRepository; 33 | } 34 | 35 | public ResponseEntity addBook(BookRequest request) { 36 | log.info("Save new book: {}", request); 37 | 38 | log.info("Find author by author id"); 39 | Optional author = authorRepository.findOne(request.getAuthorId()); 40 | if (author.isEmpty()) return ResponseEntity.notFound().build(); 41 | 42 | Book book = Book.builder() 43 | .author(author.get()) 44 | .detail(BookDetail.builder() 45 | .page(request.getPage()) 46 | .weight(request.getWeight()) 47 | .build()) 48 | .title(request.getTitle()) 49 | .price(request.getPrice()) 50 | .build(); 51 | return ResponseEntity.ok().body(bookRepository.save(book)); 52 | } 53 | 54 | public ResponseEntity getAllBook() { 55 | return ResponseEntity.ok().body(bookRepository.findAll()); 56 | } 57 | 58 | public ResponseEntity getBookDetail(Long bookId) { 59 | log.info("Find book detail by book id: {}", bookId); 60 | Optional bookDetail = bookDetailRepository.findOne(bookId); 61 | if (bookDetail.isEmpty()) return ResponseEntity.badRequest().body(Map.ofEntries(Map.entry("message", "Data not found"))); 62 | 63 | return ResponseEntity.ok().body(bookDetail.get()); 64 | } 65 | 66 | public ResponseEntity deleteBook(Long bookId) { 67 | log.info("Find book detail by book id for delete: {}", bookId); 68 | try { 69 | bookDetailRepository.delete(bookId); 70 | bookRepository.delete(bookId); 71 | } catch (EmptyResultDataAccessException e) { 72 | log.error("Data not found. Error: {}", e.getMessage()); 73 | return ResponseEntity.badRequest().body(Map.ofEntries(Map.entry("message", "Data not found"))); 74 | } 75 | return ResponseEntity.ok().body(Map.ofEntries(Map.entry("message", "ok"))); 76 | } 77 | 78 | public ResponseEntity updatePrice(BookRequest request, Long bookId) { 79 | log.info("Update price: {}", request); 80 | Optional book = bookRepository.findOne(bookId); 81 | if (book.isEmpty()) return ResponseEntity.badRequest().body(Map.ofEntries(Map.entry("message", "Data not found"))); 82 | 83 | book.get().setPrice(request.getPrice()); 84 | bookRepository.save(book.get()); 85 | return ResponseEntity.ok().body(book.get()); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/service/TransactionService.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.service; 2 | 3 | import com.piinalpin.customsoftdeletes.entity.Book; 4 | import com.piinalpin.customsoftdeletes.entity.Transaction; 5 | import com.piinalpin.customsoftdeletes.entity.TransactionDetail; 6 | import com.piinalpin.customsoftdeletes.http.dto.TransactionDetailRequest; 7 | import com.piinalpin.customsoftdeletes.http.dto.TransactionRequest; 8 | import com.piinalpin.customsoftdeletes.repository.BookRepository; 9 | import com.piinalpin.customsoftdeletes.repository.TransactionDetailRepository; 10 | import com.piinalpin.customsoftdeletes.repository.TransactionRepository; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.time.LocalDateTime; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.Optional; 20 | 21 | @Slf4j 22 | @Service 23 | public class TransactionService { 24 | 25 | private final BookRepository bookRepository; 26 | private final TransactionRepository transactionRepository; 27 | private final TransactionDetailRepository transactionDetailRepository; 28 | 29 | @Autowired 30 | public TransactionService(BookRepository bookRepository, TransactionRepository transactionRepository, 31 | TransactionDetailRepository transactionDetailRepository) { 32 | this.bookRepository = bookRepository; 33 | this.transactionRepository = transactionRepository; 34 | this.transactionDetailRepository = transactionDetailRepository; 35 | } 36 | 37 | public ResponseEntity createTransaction(TransactionRequest request) { 38 | Transaction transaction = Transaction.builder() 39 | .transactionDate(LocalDateTime.now()) 40 | .customerName(request.getCustomerName()) 41 | .build(); 42 | List details = new ArrayList<>(); 43 | for (TransactionDetailRequest detailRequest : request.getDetails()) { 44 | log.info("Find book by bookId"); 45 | Optional book = bookRepository.findOne(detailRequest.getBookId()); 46 | if (book.isPresent()) { 47 | Integer price = book.get().getPrice() * detailRequest.getQty(); 48 | details.add(TransactionDetail.builder() 49 | .transaction(transaction) 50 | .book(book.get()) 51 | .price(price) 52 | .qty(detailRequest.getQty()) 53 | .build()); 54 | } 55 | } 56 | transaction.setTotalPrice(details.stream().mapToInt(TransactionDetail::getPrice).sum()); 57 | transaction.setTotalQty(details.stream().mapToInt(TransactionDetail::getQty).sum()); 58 | transaction.setTransactionDetails(details); 59 | transactionRepository.save(transaction); 60 | return ResponseEntity.ok().body(transaction); 61 | } 62 | 63 | public ResponseEntity getTransactionDetails(Long transactionId) { 64 | return ResponseEntity.ok().body(transactionDetailRepository.findAllByTransactionId(transactionId)); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/piinalpin/customsoftdeletes/util/ResponseUtil.java: -------------------------------------------------------------------------------- 1 | package com.piinalpin.customsoftdeletes.util; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import com.piinalpin.customsoftdeletes.constant.AppConstant; 6 | import com.piinalpin.customsoftdeletes.http.dto.base.BaseResponse; 7 | 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | 11 | public class ResponseUtil { 12 | 13 | private ResponseUtil() {} 14 | 15 | public static ResponseEntity build(AppConstant.ResponseCode responseCode, T data, HttpStatus httpStatus) { 16 | return new ResponseEntity<>(build(responseCode, data), httpStatus); 17 | } 18 | 19 | private static BaseResponse build(AppConstant.ResponseCode responseCode, T data) { 20 | return BaseResponse.builder() 21 | .timestamp(LocalDateTime.now()) 22 | .responseCode(responseCode.name()) 23 | .message(responseCode.getMessage()) 24 | .data(data) 25 | .build(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | {"properties": [{ 2 | "name": "springdoc.swagger-ui.path", 3 | "type": "java.lang.String", 4 | "description": "A description for 'springdoc.swagger-ui.path'" 5 | }]} -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8080 2 | spring.application.name=custom-soft-deletes 3 | server.servlet.context-path=/api 4 | 5 | spring.datasource.url=jdbc:h2:mem:db; 6 | spring.datasource.driverClassName=org.h2.Driver 7 | spring.datasource.username=sa 8 | spring.datasource.password=password 9 | spring.jpa.database-platform=org.hibernate.dialect.H2Dialect 10 | spring.h2.console.enabled=true 11 | spring.jpa.show-sql=true --------------------------------------------------------------------------------