diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 4c940f3..4077ec9 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -20,7 +20,7 @@ } }, { - "id": "7e5ec70badaa86f3", + "id": "ca75671fa123bc88", "type": "leaf", "state": { "type": "markdown", @@ -30,33 +30,9 @@ "source": false } } - }, - { - "id": "92c92033d31ed01c", - "type": "leaf", - "state": { - "type": "markdown", - "state": { - "file": "Post.md", - "mode": "source", - "source": false - } - } - }, - { - "id": "af9b076d6f1ac377", - "type": "leaf", - "state": { - "type": "markdown", - "state": { - "file": "Database.md", - "mode": "source", - "source": false - } - } } ], - "currentTab": 2 + "currentTab": 1 } ], "direction": "vertical" @@ -122,7 +98,7 @@ "state": { "type": "backlink", "state": { - "file": "Post.md", + "file": "Home.md", "collapseAll": false, "extraContext": false, "sortOrder": "alphabetical", @@ -139,7 +115,7 @@ "state": { "type": "outgoing-link", "state": { - "file": "Post.md", + "file": "Home.md", "linksCollapsed": false, "unlinkedCollapsed": true } @@ -162,7 +138,7 @@ "state": { "type": "outline", "state": { - "file": "Post.md" + "file": "Home.md" } } } @@ -183,14 +159,15 @@ "command-palette:Open command palette": false } }, - "active": "92c92033d31ed01c", + "active": "ca75671fa123bc88", "lastOpenFiles": [ + "Pagination.md", "Home.md", + "Database.md", "Post.md", "GET.md", "Get.md", "assets/Pasted image 20230721100304.png", - "Database.md", "IntegrationTests.md", "UnitTests.md", "assets/Pasted image 20230719152007.png", diff --git a/Home.md b/Home.md index 8e684a1..d3b3cdc 100644 --- a/Home.md +++ b/Home.md @@ -223,3 +223,87 @@ cashCard = cashCardRepository.findById(99); You might immediately wonder: where is the implementation of the `CashCardRepository.findById()` method? `CrudRepository` and everything it inherits from is an Interface with no actual code! Well, based on the specific Spring Data framework used, which for us will be Spring Data JDBC, Spring Data takes care of this implementation for us during the IoC container startup time. The Spring runtime will then expose the repository as yet another bean that you can reference wherever needed in your application. As we’ve learned, there are typically trade-offs. For example the `CrudRepository` generates SQL statements to read and write your data, which is useful for many cases, but sometimes you need to write your own custom SQL statements for specific use cases. But for now, we’re happy to take advantage of its convenient, out-of-the-box methods. + +# [[Pagination]] + +We can expect each of our Family Cash Card users to have a few cards, thus we have to implement the “Read Many” endpoint. + +```json +[ + { + "id": 1, + "amount": 123.45 + }, + { + "id": 2, + "amount": 50.00 + } +] +``` + +It turns out that `CrudRepository` has a `findAll` method that can be used to easily fetch all the Cash Cards in the database. + +```java +@GetMapping() +public ResponseEntity> findAll() { + return ResponseEntity.ok(cashCardRepository.findAll()); +} +``` + +However, it turns out there’s a lot more to this operation than just returning all the Cash Cards in the database. Some questions come to mind: + +- How do I return only the Cash Cards that the user owns? + This question discussed in the [SpringSecurity] section. +- What if there are hundreds (or thousands?!) of Cash Cards? Should the API return an unlimited number of results or return them in “chunks”? + This is known as **Pagination**. +- Should the Cash Cards be returned in a particular order i.e. somehow sorted? + +In order to ensure that an API response doesn’t include an astronomically large number of Cash Cards, let’s utilize Spring Data’s pagination functionality. [[Pagination]] in Spring (and many other frameworks) is to specify the page length (e.g. 10 items), and the page index (starting with 0). For example, if a user has 25 Cash Cards, and you elect to request the second page where each page has 10 items, you would request a page of size 10, and page index of 1. + +## Sorting + +There are a few good reasons to opt for ordering (aka sorting paged response) by a specific field: + +- Minimize **cognitive overhead**: Other developers (not to mention users) will probably appreciate a thoughtful ordering when developing it. +- Minimize future errors: What happens when a new version of Spring, or Java, or the database, suddenly causes the “random” order to change overnight? + +Thankfully, Spring Data provides the `PageRequest` and `Sort` classes for pagination. Let’s look at a query to get page 2 with page size 10, _sorting by amount in descending order_ (largest amounts first): + +```java +Page page2 = cashCardRepository.findAll( + PageRequest.of( + 1, // page index for the second page - indexing starts at 0 + 10, // page size (the last page might have fewer items) + Sort.by(new Sort.Order(Sort.Direction.DESC, "amount")))); +``` + +For this code to work, it is important for our repository implementation to support pagination. `src\main\java\djmil\cashcard\CashCardRepository.java`: + +```java +import org.springframework.data.repository.PagingAndSortingRepository; + +public interface + CashCardRepository +extends + CrudRepository, + PagingAndSortingRepository { +} +``` + +But how do we get query configuration data for sorting? + +## The URI + +The HTTP request parameters is used to transfer values that is used to configure pagination query: *(we've omitted the `https://domain` prefix in the following)* + +1. Get the second page + /cashcards**?page=1** +2. …where a page has length of 3 + /cashcards?page=1**&size=3** +3. …sorted by the current Cash Card balance + /cashcards?page=1&size=3**&sort=amount** +4. …in descending order (highest balance first) + /cashcards?page=1&size=3&sort=amount**,desc** + + + diff --git a/Pagination.md b/Pagination.md new file mode 100644 index 0000000..5fd1dc1 --- /dev/null +++ b/Pagination.md @@ -0,0 +1,184 @@ +# Test for Data contract list + +`src/test/resources/djmil/cashcard/list.json` + +```json +[ + { "id": 99, "amount": 123.45 }, + { "id": 100, "amount": 1.0 }, + { "id": 101, "amount": 150.0 } +] +``` + +Now open the `CashCardsJsonTest.java` file. And add `cashCards` class-level variable to contain the following Java array: + +```java +private CashCard[] cashCards; + +@BeforeEach +void setUp() { + cashCards = Arrays.array( + new CashCard(99L, 123.45), + new CashCard(100L, 100.00), + new CashCard(101L, 150.00)); +} +``` + +Than add the new test: + +```java +@Test +void cashCardListSerializationTest() throws IOException { + assertThat(jsonList.write(cashCards)).isStrictlyEqualToJson("list.json"); +} + +@Test +void cashCardListDeserializationTest() throws IOException { + String expected=""" + [ + { "id": 99, "amount": 123.45 }, + { "id": 100, "amount": 100.00 }, + { "id": 101, "amount": 150.00 } + ] + """; + assertThat(jsonList.parse(expected)).isEqualTo(cashCards); +} +``` + +# GET list +## DB data + +It is important to populate our test db with known data, that would reflect desired test case scenario. `src/test/resource/data.sql` + +```sql +INSERT INTO CASH_CARD(ID, AMOUNT) VALUES (99, 123.45); +INSERT INTO CASH_CARD(ID, AMOUNT) VALUES (100, 1.00); +INSERT INTO CASH_CARD(ID, AMOUNT) VALUES (101, 150.00); +``` + +## Endpoint Test + +In `src/test/java/djmil/cashcard/CashCardApplication.test` + +```java +@Test +void shouldReturnAllCashCardsWhenListIsRequested() { + ResponseEntity response = restTemplate.getForEntity("/cashcards", String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + DocumentContext documentContext = JsonPath.parse(response.getBody()); + int cashCardCount = documentContext.read("$.length()"); + assertThat(cashCardCount).isEqualTo(3); + + JSONArray ids = documentContext.read("$..id"); + assertThat(ids).containsExactlyInAnyOrder(99, 100, 101); + + JSONArray amounts = documentContext.read("$..amount"); + assertThat(amounts).containsExactlyInAnyOrder(123.45, 100.0, 150.00); +} +``` + +Check out these new JsonPath expressions! A good place to learn more about JsonPath is [the JsonPath documentation](https://github.com/json-path/JsonPath): +- `documentContext.read("$.length()")` calculates the length of the array. +- `.read("$..id")` retrieves the list of all `id` values returned, while `.read("$..amount")` collects all `amounts` returned. + +## Controller code + +`src/main/java/djmil/cashcard/CashCardController.java` + +```java +@GetMapping() +public ResponseEntity> findAll() { + return ResponseEntity.ok(cashCardRepository.findAll()); +} +``` + +# Test Interaction and @DirtiesContext + +Depending on test execution order schedule, our new `shouldReturnAllCashCardsWhenListIsRequested` test might not pass! Why? + +```bash +[~/exercises] $ ./gradlew test +... + org.opentest4j.AssertionFailedError: + expected: 3 + but was: 4 + ... + at app//example.cashcard.CashCardApplicationTests.shouldReturnAllCashCardsWhenListIsRequested(CashCardApplicationTests.java:70) +``` + +The reason is that one of the other tests is interfering with our new test by creating a new Cash Card. `@DirtiesContext` fixes this problem by causing Spring to start with a clean slate, as if those other tests hadn't been run. Removing it (commenting it out) from the class caused our new test to fail. + +Although you can use `@DirtiesContext` to work around inter-test interaction, you shouldn't use it indiscriminately; you should have a good reason. Our reason here is to clean up after creating a new Cash Card. + +Leave `DirtiesContext` commented out at the class level, and _uncomment_ it on the method which creates a new Cash Card: + +```java +//@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) +class CashCardApplicationTests { + ... + + @Test + @DirtiesContext + void shouldCreateANewCashCard() { + ... +``` + +# GET pagination + +## Test + +Since we have 3 `CashCards` in our [[Pagination#DB data|database]], Let's set up a test to fetch them one at a time (page size of 1), then have their amounts sorted from highest to lowest (descending). + +```java +@Test +void shouldReturnASortedPageOfCashCards() { + ResponseEntity response = restTemplate.getForEntity("/cashcards?page=0&size=1&sort=amount,desc", String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + DocumentContext documentContext = JsonPath.parse(response.getBody()); + JSONArray read = documentContext.read("$[*]"); + assertThat(read.size()).isEqualTo(1); + + double amount = documentContext.read("$[0].amount"); + assertThat(amount).isEqualTo(150.00); +} +``` + +The URI we are requesting contains both pagination and sorting information: `/cashcards?page=0&size=1&sort=amount,desc`: +- `page=0`: Get the first page. Page indexes start at 0. +- `size=1`: Each page has size 1. +- `sort=amount,desc` + +## Controller + +```java +@GetMapping +public ResponseEntity> findAll(Pageable pageable) { + Page page = cashCardRepository.findAll( + PageRequest.of( + pageable.getPageNumber(), + pageable.getPageSize(), + pageable.getSort() + )); + return ResponseEntity.ok(page.getContent()); +} +``` + +## Pagination with defaults + +Spring provides the default `page` and `size` values (they are 0 and 20, respectively). A default of 20 for page size explains why all three of our Cash Cards were returned. Again: we didn't need to explicitly define these defaults. Spring provides them "out of the box". So the test with + +```java +ResponseEntity response = restTemplate.getForEntity("/cashcards", String.class); +``` + +shall work. But there are no reasonable defaults for sorting order possible. So if the one was not provided, query would return items in order defined by DB (most likely in order of creation). To deal with this limitation, we have to define the default `sort` parameter in our own code, by passing a `Sort` object to `getSortOr()`: + +```java +PageRequest.of( + pageable.getPageNumber(), + pageable.getPageSize(), + pageable.getSortOr(Sort.by(Sort.Direction.ASC, "amount")) +)); +``` \ No newline at end of file