├── .gitignore ├── README.md ├── pom.xml ├── sql └── TestData.sql └── src ├── main ├── java │ └── com │ │ └── ill │ │ └── test │ │ └── sqltx │ │ ├── SqlrxApplication.java │ │ ├── controller │ │ └── AgentController.java │ │ ├── repository │ │ ├── AgentRepository.java │ │ └── AgentRow.java │ │ └── service │ │ └── AgentService.java └── resources │ └── application.properties └── test ├── java └── com │ └── ill │ └── test │ └── sqltx │ ├── SqlrxApplicationTests.java │ ├── controller │ └── AgentControllerTest.java │ └── repository │ └── AgentRepositoryTest.java └── resources └── example.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | HELP.md 3 | target/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | !**/src/main/**/target/ 6 | !**/src/test/**/target/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | !**/src/main/**/build/ 31 | !**/src/test/**/build/ 32 | 33 | ### VS Code ### 34 | .vscode/ 35 | mvnw 36 | mvnw.cmd 37 | commitfix.sh 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQL RX 2 | 3 | A working Java project that uses Spring Boot and R2DBC to return data reactively from a MySQL 8 database. Configurable with application properties and with build-time tests. 4 | 5 | Read more about this project at [Medium.com](https://robinedwardellis.medium.com/reactive-mysql-with-spring-boot-1b184b9ea58a) 6 | 7 | ## Test data 8 | 9 | There is a SQL script in sql folder that will drop the schema if it exists and create it again. Remember to apply a user to the schema and update the username and password in application.properties 10 | 11 | Test data is the agent list from the static data dump of EVE Online, May 2022. 12 | 13 | ## Building 14 | 15 | `mvn clean install` 16 | 17 | ## Endpoints 18 | 19 | - `/agents` - get all agents 20 | - `/agents/101` - get an agent by ID (range is 3008416 to 3019501) 21 | - `/agents/ids` - get IDs for all agents (integer) 22 | - `/agents/corp/id` - get agents for the given corporation ID (range is 1000002 to 1000182) 23 | - `/agents/location/id` - get agents for the given location ID (range is 60000004 to 60015146) 24 | 25 | ### Examples 26 | 27 | `curl -i http://localhost:8080/agents` - get all agents 28 | 29 | `curl -i http://localhost:8080/agents/3019483` - get a single agent 30 | 31 | `curl -i http://localhost:8080/agents/101` - agent not found 32 | 33 | 34 | ## CCP hf. Copyright Notice 35 | 36 | EVE Online, the EVE logo, EVE and all associated logos and designs are the intellectual property of CCP hf. All artwork, screenshots, characters, vehicles, storylines, world facts or other recognizable features of the intellectual property relating to these trademarks are likewise the intellectual property of CCP hf. EVE Online and the EVE logo are the registered trademarks of CCP hf. All rights are reserved worldwide. All other trademarks are the property of their respective owners. CCP hf. has granted permission to Robin Ellis to use EVE Online and all associated logos and designs for promotional and information purposes but does not endorse, and is not in any way affiliated with, Robin Ellis or this project. CCP is in no way responsible for the content on or functioning of this project, nor can it be liable for any damage arising from the use of this project. 37 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 3.1.5 10 | 11 | 12 | 13 | com.ill.test 14 | sqlrx 15 | 0.0.1-SNAPSHOT 16 | sqlrx 17 | MySQL Rx Test 18 | 19 | 20 | UTF-8 21 | UTF-8 22 | 17 23 | 24 | 1.0.1 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-webflux 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-data-r2dbc 36 | 37 | 38 | 39 | io.asyncer 40 | r2dbc-mysql 41 | ${mysql-driver.version} 42 | 43 | 44 | 45 | org.projectlombok 46 | lombok 47 | true 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-devtools 53 | runtime 54 | true 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-test 60 | test 61 | 62 | 63 | 64 | io.projectreactor 65 | reactor-test 66 | test 67 | 68 | 69 | 70 | 71 | clean package 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-enforcer-plugin 77 | 78 | 79 | enforce-maven 80 | 81 | enforce 82 | 83 | 84 | 85 | 86 | 87 | 3.6.3 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | org.apache.maven.plugins 97 | maven-compiler-plugin 98 | 99 | ${project.build.sourceEncoding} 100 | ${java.version} 101 | ${java.version} 102 | -Xlint:all 103 | true 104 | true 105 | 106 | 107 | 108 | 109 | org.springframework.boot 110 | spring-boot-maven-plugin 111 | 112 | 113 | 114 | org.projectlombok 115 | lombok 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/main/java/com/ill/test/sqltx/SqlrxApplication.java: -------------------------------------------------------------------------------- 1 | package com.ill.test.sqltx; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SqlrxApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(SqlrxApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/ill/test/sqltx/controller/AgentController.java: -------------------------------------------------------------------------------- 1 | package com.ill.test.sqltx.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import com.ill.test.sqltx.repository.AgentRow; 11 | import com.ill.test.sqltx.service.AgentService; 12 | import reactor.core.publisher.Flux; 13 | import reactor.core.publisher.Mono; 14 | 15 | @RestController 16 | @RequestMapping("/agents") 17 | public class AgentController { 18 | 19 | @Autowired 20 | private AgentService service; 21 | 22 | @GetMapping("") 23 | public Flux getAll() { 24 | return service.getAll(); 25 | } 26 | 27 | @GetMapping("/ids") 28 | public Flux getAllIds() { 29 | return service.getAll().map(AgentRow::getAgentId); 30 | } 31 | 32 | @GetMapping("/corp/{corpId}") 33 | public Flux getForCorp(@PathVariable int corpId) { 34 | return service.getForCorp(corpId); 35 | } 36 | 37 | @GetMapping("/location/{locationId}") 38 | public Flux getForLocation(@PathVariable int locationId) { 39 | return service.getForLocation(locationId); 40 | } 41 | 42 | @GetMapping("/{agentId}") 43 | public Mono> getAgent(@PathVariable int agentId) { 44 | return service 45 | .getAgent(agentId) 46 | .map(agent -> new ResponseEntity<>(agent, HttpStatus.OK)) 47 | .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND)); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/ill/test/sqltx/repository/AgentRepository.java: -------------------------------------------------------------------------------- 1 | package com.ill.test.sqltx.repository; 2 | 3 | import org.springframework.data.r2dbc.repository.R2dbcRepository; 4 | import reactor.core.publisher.Flux; 5 | 6 | public interface AgentRepository extends R2dbcRepository { 7 | 8 | Flux findAllByCorporationId(int corpId); 9 | 10 | Flux findAllByLocationId(int locationId); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ill/test/sqltx/repository/AgentRow.java: -------------------------------------------------------------------------------- 1 | package com.ill.test.sqltx.repository; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.relational.core.mapping.Column; 5 | import org.springframework.data.relational.core.mapping.Table; 6 | 7 | import lombok.Data; 8 | 9 | @Table("agtAgents") 10 | @Data 11 | public class AgentRow { 12 | 13 | @Id 14 | @Column("agentID") 15 | private Integer agentId; 16 | 17 | @Column("divisionID") 18 | private int divisionId; 19 | 20 | @Column("corporationID") 21 | private int corporationId; 22 | 23 | @Column("locationID") 24 | private int locationId; 25 | 26 | @Column("level") 27 | private int level; 28 | 29 | @Column("agentTypeID") 30 | private int agentTypeId; 31 | 32 | @Column("isLocator") 33 | private boolean locator; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/ill/test/sqltx/service/AgentService.java: -------------------------------------------------------------------------------- 1 | package com.ill.test.sqltx.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | 6 | import com.ill.test.sqltx.repository.AgentRepository; 7 | import com.ill.test.sqltx.repository.AgentRow; 8 | 9 | import reactor.core.publisher.Flux; 10 | import reactor.core.publisher.Mono; 11 | 12 | @Service 13 | public class AgentService { 14 | 15 | @Autowired 16 | private AgentRepository repo; 17 | 18 | public Flux getAll() { 19 | return repo.findAll(); 20 | } 21 | 22 | public Flux getForCorp(int corpId) { 23 | return repo.findAllByCorporationId(corpId); 24 | } 25 | 26 | public Flux getForLocation(int locationId) { 27 | return repo.findAllByLocationId(locationId); 28 | } 29 | 30 | public Mono getAgent(int agentId) { 31 | return repo.findById(agentId); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # dev properties 2 | spring.r2dbc.url=r2dbc:pool:mysql://localhost:3306/SQL_RX_TEST?zeroDateTimeBehavior=convertToNull&useSSL=false&useServerPrepareStatement=true 3 | spring.r2dbc.username=sqlrx 4 | spring.r2dbc.password=sqlrx 5 | 6 | logging.level.org.springframework.data.repository=DEBUG 7 | logging.level.org.springframework.r2dbc.core=DEBUG 8 | -------------------------------------------------------------------------------- /src/test/java/com/ill/test/sqltx/SqlrxApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ill.test.sqltx; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class SqlrxApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/ill/test/sqltx/controller/AgentControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.ill.test.sqltx.controller; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; 6 | import org.springframework.boot.test.mock.mockito.MockBean; 7 | import org.springframework.test.web.reactive.server.WebTestClient; 8 | import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; 9 | import com.ill.test.sqltx.repository.AgentRow; 10 | import com.ill.test.sqltx.service.AgentService; 11 | import static org.mockito.Mockito.when; 12 | import reactor.core.publisher.Flux; 13 | import reactor.core.publisher.Mono; 14 | import reactor.test.StepVerifier; 15 | 16 | @WebFluxTest(controllers = { AgentController.class }) 17 | class AgentControllerTest { 18 | 19 | private static final String AGENTS_URI = "http://localhost:8080/agents"; 20 | private static final int EXPECTED_AGENT_ID = 101; 21 | 22 | @MockBean 23 | private AgentService mockAgentService; 24 | 25 | @Autowired 26 | private WebTestClient webTestClient; 27 | 28 | @Test 29 | void getAllAgents() { 30 | // GIVEN we have data 31 | final AgentRow a1 = new AgentRow(); 32 | final AgentRow a2 = new AgentRow(); 33 | when(mockAgentService.getAll()).thenReturn(Flux.just(a1, a2)); 34 | 35 | // WHEN the endpoint is called 36 | final ResponseSpec response = webTestClient 37 | .get().uri(AGENTS_URI) 38 | .exchange(); 39 | 40 | // THEN we get an OK response with list content 41 | checkAgentList(response, 2); 42 | } 43 | 44 | @Test 45 | void getAllAgentsIds() { 46 | // GIVEN we have data 47 | final AgentRow a1 = new AgentRow(); 48 | a1.setAgentId(303); 49 | final AgentRow a2 = new AgentRow(); 50 | a2.setAgentId(808); 51 | when(mockAgentService.getAll()).thenReturn(Flux.just(a1, a2)); 52 | 53 | // WHEN the endpoint is called 54 | final ResponseSpec response = webTestClient 55 | .get().uri(AGENTS_URI + "/ids") 56 | .exchange(); 57 | 58 | // THEN we get an OK response with list content 59 | final Flux flux = response 60 | .expectStatus().isOk() 61 | .returnResult(Integer.class) 62 | .getResponseBody(); 63 | StepVerifier.create(flux.collectList()) 64 | .expectNextMatches(list -> list.size() == 2) 65 | .verifyComplete(); 66 | } 67 | 68 | @Test 69 | void getAgentsForCorp() { 70 | // GIVEN we have data 71 | final AgentRow a1 = new AgentRow(); 72 | final AgentRow a2 = new AgentRow(); 73 | when(mockAgentService.getForCorp(101)).thenReturn(Flux.just(a1, a2)); 74 | 75 | // WHEN the endpoint is called 76 | final ResponseSpec response = webTestClient 77 | .get().uri(AGENTS_URI + "/corp/101") 78 | .exchange(); 79 | 80 | // THEN we get an OK response with list content 81 | checkAgentList(response, 2); 82 | } 83 | 84 | @Test 85 | void getAgentsForLocation() { 86 | // GIVEN we have data 87 | final AgentRow a1 = new AgentRow(); 88 | final AgentRow a2 = new AgentRow(); 89 | when(mockAgentService.getForLocation(111)).thenReturn(Flux.just(a1, a2)); 90 | 91 | // WHEN the endpoint is called 92 | final ResponseSpec response = webTestClient 93 | .get().uri(AGENTS_URI + "/location/111") 94 | .exchange(); 95 | 96 | // THEN we get an OK response with list content 97 | checkAgentList(response, 2); 98 | } 99 | 100 | @Test 101 | void getSingleAgent() { 102 | // GIVEN we have data 103 | final AgentRow agentRow = new AgentRow(); 104 | agentRow.setAgentId(EXPECTED_AGENT_ID); 105 | when(mockAgentService.getAgent(EXPECTED_AGENT_ID)).thenReturn(Mono.just(agentRow)); 106 | 107 | // WHEN the endpoint is called 108 | final ResponseSpec response = webTestClient 109 | .get().uri(AGENTS_URI + "/" + EXPECTED_AGENT_ID) 110 | .exchange(); 111 | 112 | // THEN we get the agent 113 | final Mono mono = response 114 | .expectStatus().isOk() 115 | .returnResult(AgentRow.class) 116 | .getResponseBody() 117 | .single(); 118 | StepVerifier.create(mono) 119 | .expectNextMatches(agent -> agent.getAgentId() == EXPECTED_AGENT_ID) 120 | .verifyComplete(); 121 | } 122 | 123 | @Test 124 | void getUnknownAgent() { 125 | // GIVEN we have no data 126 | when(mockAgentService.getAgent(EXPECTED_AGENT_ID)).thenReturn(Mono.empty()); 127 | 128 | // WHEN the endpoint is called 129 | final ResponseSpec response = webTestClient 130 | .get().uri(AGENTS_URI + "/" + EXPECTED_AGENT_ID) 131 | .exchange(); 132 | 133 | // THEN we get a 404 134 | response.expectStatus().isNotFound(); 135 | } 136 | 137 | private void checkAgentList(final ResponseSpec response, int expectedCount) { 138 | final Flux flux = response 139 | .expectStatus().isOk() 140 | .returnResult(AgentRow.class) 141 | .getResponseBody(); 142 | StepVerifier.create(flux.collectList()) 143 | .expectNextMatches(list -> list.size() == expectedCount) 144 | .verifyComplete(); 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/test/java/com/ill/test/sqltx/repository/AgentRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.ill.test.sqltx.repository; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | import reactor.test.StepVerifier; 8 | 9 | @SpringBootTest 10 | class AgentRepositoryTest { 11 | 12 | @Autowired 13 | private AgentRepository repo; 14 | 15 | @Test 16 | void checkCount_direct() { 17 | StepVerifier 18 | .create(repo.count()) 19 | .expectNext(10871L) 20 | .verifyComplete(); 21 | } 22 | 23 | @Test 24 | void checkCount_list() { 25 | StepVerifier 26 | .create(repo.findAll().collectList()) 27 | .expectNextMatches(list -> list.size() == 10871) 28 | .verifyComplete(); 29 | } 30 | 31 | @Test 32 | void checkCorpFedNavy() { 33 | StepVerifier 34 | .create(repo.findAllByCorporationId(1000120).collectList()) 35 | .expectNextMatches(list -> list.size() == 144) 36 | .verifyComplete(); 37 | } 38 | 39 | @Test 40 | void checkLocation() { 41 | StepVerifier 42 | .create(repo.findAllByLocationId(60008368).collectList()) 43 | .expectNextMatches(list -> list.size() == 18) 44 | .verifyComplete(); 45 | } 46 | 47 | @Test 48 | void checkSingleAgent() { 49 | StepVerifier 50 | .create(repo.findById(3015958)) 51 | .expectNextMatches(agent -> agent.getCorporationId() == 1000148 && agent.getLevel() == 4) 52 | .verifyComplete(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/resources/example.sh: -------------------------------------------------------------------------------- 1 | curl 'http://localhost:8080/agents/ids' ; 2 | 3 | curl 'http://localhost:8080/agents' ; 4 | 5 | # caldari business tribunal 6 | curl 'http://localhost:8080/agents/corp/1000033'; 7 | 8 | # location 9 | curl 'http://localhost:8080/agents/location/60008368'; 10 | --------------------------------------------------------------------------------