Skip to content

Commit dd2b573

Browse files
committed
Micronaut: Add Db integration
1 parent fcc5497 commit dd2b573

File tree

10 files changed

+232
-21
lines changed

10 files changed

+232
-21
lines changed

micronaut-app/build.gradle

+15-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
plugins {
2-
id "io.freefair.lombok" version "6.5.0.3"
32
id("com.github.johnrengelman.shadow") version "7.1.2"
43
id("io.micronaut.application") version "3.5.1"
54
}
@@ -14,18 +13,26 @@ repositories {
1413

1514
dependencies {
1615
implementation project(":common-api")
16+
annotationProcessor("org.projectlombok:lombok")
17+
annotationProcessor("org.mapstruct:mapstruct-processor:1.5.2.Final")
18+
annotationProcessor("io.micronaut.data:micronaut-data-processor")
19+
annotationProcessor("io.micronaut:micronaut-http-validation")
1720
implementation("io.micronaut:micronaut-http-client")
1821
implementation("io.micronaut:micronaut-jackson-databind")
19-
implementation("jakarta.annotation:jakarta.annotation-api")
20-
implementation("io.micronaut:micronaut-validation")
21-
implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")
2222
implementation("io.micronaut:micronaut-management")
23-
24-
annotationProcessor("io.micronaut:micronaut-http-validation")
25-
23+
implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")
24+
implementation("io.micronaut.data:micronaut-data-hibernate-jpa")
25+
implementation("io.micronaut.sql:micronaut-jdbc-hikari")
26+
implementation("jakarta.annotation:jakarta.annotation-api")
27+
implementation("org.mapstruct:mapstruct:1.5.2.Final")
28+
compileOnly("org.projectlombok:lombok")
2629
runtimeOnly("ch.qos.logback:logback-classic")
27-
30+
runtimeOnly("org.postgresql:postgresql")
2831
testImplementation("org.assertj:assertj-core")
32+
testImplementation("org.testcontainers:junit-jupiter")
33+
testImplementation("org.testcontainers:postgresql")
34+
testImplementation("org.testcontainers:testcontainers")
35+
implementation("io.micronaut:micronaut-validation")
2936
}
3037

3138
application {

micronaut-app/src/main/java/bitxon/micronaut/controller/AccountController.java

+49-10
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55

66
import javax.validation.Valid;
77
import java.util.List;
8-
import java.util.Random;
8+
import java.util.Optional;
9+
import java.util.stream.Collectors;
910

1011
import bitxon.api.model.Account;
1112
import bitxon.api.model.MoneyTransfer;
13+
import bitxon.micronaut.db.AccountDao;
14+
import bitxon.micronaut.mapper.AccountMapper;
1215
import io.micronaut.core.annotation.Nullable;
1316
import io.micronaut.http.HttpStatus;
1417
import io.micronaut.http.annotation.Body;
@@ -18,35 +21,71 @@
1821
import io.micronaut.http.annotation.PathVariable;
1922
import io.micronaut.http.annotation.Post;
2023
import io.micronaut.http.annotation.Status;
24+
import io.micronaut.scheduling.TaskExecutors;
25+
import io.micronaut.scheduling.annotation.ExecuteOn;
26+
import io.micronaut.transaction.annotation.ReadOnly;
27+
import io.micronaut.transaction.annotation.TransactionalAdvice;
28+
import lombok.RequiredArgsConstructor;
2129

22-
30+
@ExecuteOn(TaskExecutors.IO)
2331
@Controller("/accounts")
32+
@RequiredArgsConstructor
2433
public class AccountController {
2534

35+
private final AccountDao dao;
36+
private final AccountMapper mapper;
37+
2638
@Get
39+
@ReadOnly
2740
public List<Account> getAll() {
28-
return List.of();
41+
return dao.findAll().stream()
42+
.map(mapper::mapToApi)
43+
.collect(Collectors.toList());
2944
}
3045

3146
@Get("/{id}")
47+
@ReadOnly
3248
public Account getById(@PathVariable("id") Long id) {
33-
return Account.builder().id(id).build();
49+
return dao.findById(id)
50+
.map(mapper::mapToApi)
51+
.orElseThrow(() -> new RuntimeException("Resource not found"));
3452
}
3553

3654
@Post
55+
@TransactionalAdvice
3756
public Account create(@Body @Valid Account account) {
38-
return Account.builder()
39-
.id(new Random().nextLong(0, Long.MAX_VALUE))
40-
.email(account.getEmail())
41-
.currency(account.getCurrency())
42-
.moneyAmount(account.getMoneyAmount())
43-
.build();
57+
return Optional.of(account)
58+
.map(mapper::mapToDb)
59+
.map(dao::save)
60+
.map(mapper::mapToApi)
61+
.get();
4462
}
4563

4664
@Post("/transfers")
4765
@Status(HttpStatus.NO_CONTENT)
66+
@TransactionalAdvice
4867
public void create(@Body @Valid MoneyTransfer transfer,
4968
@Header(value = DIRTY_TRICK_HEADER) @Nullable String dirtyTrick) {
69+
var sender = dao.findById(transfer.getSenderId())
70+
.orElseThrow(() -> new RuntimeException("Sender not found"));
71+
72+
var recipient = dao.findById(transfer.getRecipientId())
73+
.orElseThrow(() -> new RuntimeException("Recipient not found"));
74+
75+
var exchangeRateValue = 1.0d;
76+
//exchangeClient.getExchangeRate(sender.getCurrency()).getRates().getOrDefault(recipient.getCurrency(), 1.0);
77+
78+
sender.setMoneyAmount(sender.getMoneyAmount() - transfer.getMoneyAmount());
79+
dao.save(sender);
80+
81+
if (FAIL_TRANSFER.equals(dirtyTrick)) {
82+
throw new RuntimeException("Error during money transfer");
83+
}
84+
85+
recipient.setMoneyAmount(recipient.getMoneyAmount() + (int) (transfer.getMoneyAmount() * exchangeRateValue));
86+
dao.save(recipient);
5087

5188
}
89+
90+
5291
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package bitxon.micronaut.db;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
6+
import bitxon.micronaut.db.model.Account;
7+
import io.micronaut.data.annotation.Repository;
8+
import io.micronaut.data.repository.CrudRepository;
9+
10+
@Repository
11+
public interface AccountDao extends CrudRepository<Account, Long> {
12+
13+
@Override
14+
List<Account> findAll();
15+
16+
@Override
17+
Optional<Account> findById(Long id);
18+
19+
@Override
20+
Account save(Account entity);
21+
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package bitxon.micronaut.db.model;
2+
3+
import javax.persistence.Column;
4+
import javax.persistence.Entity;
5+
import javax.persistence.GeneratedValue;
6+
import javax.persistence.GenerationType;
7+
import javax.persistence.Id;
8+
import javax.persistence.Table;
9+
10+
import lombok.AllArgsConstructor;
11+
import lombok.Builder;
12+
import lombok.Data;
13+
import lombok.NoArgsConstructor;
14+
15+
@Data
16+
@Builder
17+
@AllArgsConstructor
18+
@NoArgsConstructor
19+
@Entity
20+
@Table(name = "account")
21+
public class Account {
22+
23+
@Id
24+
@GeneratedValue(strategy = GenerationType.IDENTITY)
25+
@Column(name = "id")
26+
private Long id;
27+
@Column(name = "email", nullable = false)
28+
private String email;
29+
@Column(name = "currency", nullable = false)
30+
private String currency;
31+
@Column(name = "money_amount", nullable = false)
32+
private Integer moneyAmount;
33+
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package bitxon.micronaut.mapper;
2+
3+
import bitxon.api.model.Account;
4+
import org.mapstruct.Mapper;
5+
import org.mapstruct.Mapping;
6+
import org.mapstruct.MappingConstants;
7+
import org.mapstruct.ReportingPolicy;
8+
9+
@Mapper(
10+
unmappedTargetPolicy = ReportingPolicy.ERROR,
11+
componentModel = MappingConstants.ComponentModel.JSR330
12+
)
13+
public interface AccountMapper {
14+
15+
@Mapping(target = "id", source = "id")
16+
@Mapping(target = "email", source = "email")
17+
@Mapping(target = "currency", source = "currency")
18+
@Mapping(target = "moneyAmount", source = "moneyAmount")
19+
Account mapToApi(bitxon.micronaut.db.model.Account src);
20+
21+
@Mapping(target = "id", source = "id")
22+
@Mapping(target = "email", source = "email")
23+
@Mapping(target = "currency", source = "currency")
24+
@Mapping(target = "moneyAmount", source = "moneyAmount")
25+
bitxon.micronaut.db.model.Account mapToDb(Account src);
26+
}

micronaut-app/src/main/resources/application.yml

+12-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,15 @@ endpoints:
1010
netty:
1111
default:
1212
allocator:
13-
max-order: 3
13+
max-order: 3
14+
15+
datasources:
16+
default:
17+
url: ${JDBC_URL:`jdbc:postgresql://localhost:5432/postgres`}
18+
username: ${JDBC_USER:postgres}
19+
password: ${JDBC_PASSWORD:postgres}
20+
driverClassName: ${JDBC_DRIVER:org.postgresql.Driver}
21+
jpa:
22+
default:
23+
properties:
24+
hibernate.hbm2ddl.auto: create-drop

micronaut-app/src/test/java/bitxon/micronaut/AbstractMicronautTest.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import io.micronaut.runtime.EmbeddedApplication;
66
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
77
import jakarta.inject.Inject;
8+
import org.testcontainers.junit.jupiter.Testcontainers;
89

9-
@MicronautTest
10+
@Testcontainers
11+
@MicronautTest // default environments is 'test'
1012
class AbstractMicronautTest {
1113

1214
@Inject

micronaut-app/src/test/java/bitxon/micronaut/MoneyTransferMicronautTest.java

+47-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package bitxon.micronaut;
22

3+
import static bitxon.api.constant.Constants.DIRTY_TRICK_HEADER;
4+
import static bitxon.api.constant.Constants.DirtyTrick.FAIL_TRANSFER;
35
import static org.assertj.core.api.Assertions.assertThat;
46
import static org.assertj.core.api.Assertions.catchThrowableOfType;
57

@@ -14,7 +16,6 @@
1416

1517
class MoneyTransferMicronautTest extends AbstractMicronautTest {
1618

17-
1819
@Test
1920
void transfer() {
2021
var transferAmount = 40;
@@ -37,8 +38,53 @@ void transfer() {
3738
var response = client().toBlocking().exchange(request);
3839

3940
assertThat(response.getStatus().getCode()).isEqualTo(204);
41+
assertThat(retrieveUserMoneyAmount(senderId)).as("Check sender result balance")
42+
.isEqualTo(senderMoneyAmountResult);
43+
assertThat(retrieveUserMoneyAmount(recipientId)).as("Check recipient result balance")
44+
.isEqualTo(recipientMoneyAmountResult);
45+
46+
}
47+
48+
@Test
49+
void transferWithError() {
50+
var transferAmount = 40;
51+
// Sender
52+
var senderId = 3L;
53+
var senderMoneyAmountOriginal = 79;
54+
// Recipient
55+
var recipientId = 4L;
56+
var recipientMoneyAmountOriginal = 33;
57+
4058

59+
var requestBody = MoneyTransfer.builder()
60+
.senderId(senderId)
61+
.recipientId(recipientId)
62+
.moneyAmount(transferAmount)
63+
.build();
64+
65+
var request = HttpRequest.POST("/transfers", requestBody)
66+
.header(DIRTY_TRICK_HEADER, FAIL_TRANSFER);
67+
var exception = catchThrowableOfType(
68+
() -> client().toBlocking().exchange(request, Account.class),
69+
HttpClientResponseException.class
70+
);
71+
72+
assertThat(exception).as("Check response error/status")
73+
.extracting(HttpClientResponseException::getResponse)
74+
.extracting(HttpResponse::getStatus)
75+
.extracting(HttpStatus::getCode)
76+
.isEqualTo(500);
77+
assertThat(retrieveUserMoneyAmount(senderId)).as("Check sender result balance")
78+
.isEqualTo(senderMoneyAmountOriginal);
79+
assertThat(retrieveUserMoneyAmount(recipientId)).as("Check recipient result balance")
80+
.isEqualTo(recipientMoneyAmountOriginal);
81+
82+
}
4183

84+
private int retrieveUserMoneyAmount(Long id) {
85+
var request = HttpRequest.GET("/" + id);
86+
var response = client().toBlocking().exchange(request, Account.class);
87+
return response.getBody().get().getMoneyAmount();
4288
}
4389

4490
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
datasources:
2+
default:
3+
url: jdbc:tc:postgresql:14.4:///postgres?TC_INITSCRIPT=file:src/test/resources/sql/db-test-data.sql
4+
driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver
5+
6+
jpa:
7+
default:
8+
properties:
9+
hibernate.hbm2ddl.auto: none
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
DROP TABLE IF EXISTS account;
2+
CREATE TABLE account (
3+
id SERIAL PRIMARY KEY,
4+
email VARCHAR ( 50 ) NOT NULL,
5+
currency VARCHAR ( 50 ) NOT NULL,
6+
money_amount INT NOT NULL
7+
);
8+
9+
INSERT INTO account (id, email, currency, money_amount) VALUES
10+
(DEFAULT, '[email protected]', 'USD', 340),
11+
(DEFAULT, '[email protected]', 'USD', 573),
12+
(DEFAULT, '[email protected]', 'USD', 79),
13+
(DEFAULT, '[email protected]', 'USD', 33),
14+
(DEFAULT, '[email protected]', 'USD', 100),
15+
(DEFAULT, '[email protected]', 'GBP', 80);

0 commit comments

Comments
 (0)