Skip to content

Commit b93794e

Browse files
✨ feat: Creature Recruitment | Dwelling read model (#3)
1 parent 0d1675e commit b93794e

12 files changed

Lines changed: 386 additions & 5 deletions

File tree

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@
122122
<dependency>
123123
<groupId>org.axonframework</groupId>
124124
<artifactId>axon-test</artifactId>
125+
<scope>test</scope>
126+
</dependency>
127+
<dependency>
128+
<groupId>org.assertj</groupId>
129+
<artifactId>assertj-core</artifactId>
130+
<version>3.27.3</version>
131+
<scope>test</scope>
125132
</dependency>
126133
</dependencies>
127134

src/main/java/com/dddheroes/heroesofddd/armies/write/Army.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import static org.axonframework.modelling.command.AggregateLifecycle.apply;
2323

24+
// todo: probably we should model events ArmyEstablished and ArmyDestroyed, more on that on Event Model
2425
@Aggregate
2526
class Army {
2627

@@ -29,7 +30,7 @@ class Army {
2930
private final Map<CreatureId, Amount> creatureStacks = new HashMap<>();
3031

3132
@CommandHandler
32-
@CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING)
33+
@CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING) // performance downside in comparison to constructor
3334
void handle(AddCreatureToArmy command) {
3435
new CanHaveMax7CreatureStacksInArmy(command.creatureId(), creatureStacks).verify();
3536

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.dddheroes.heroesofddd.creaturerecruitment.read;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.Id;
6+
import jakarta.persistence.Table;
7+
import org.hibernate.annotations.JdbcTypeCode;
8+
import org.hibernate.type.SqlTypes;
9+
10+
import java.util.Map;
11+
12+
@Entity
13+
@Table(name = "read_model_dwelling")
14+
public class DwellingReadModel {
15+
16+
@Id
17+
private String dwellingId;
18+
19+
private String creatureId;
20+
21+
@JdbcTypeCode(SqlTypes.JSON)
22+
@Column(columnDefinition = "jsonb")
23+
private Map<String, Integer> costPerTroop;
24+
25+
private Integer availableCreatures;
26+
27+
DwellingReadModel(String dwellingId,
28+
String creatureId,
29+
Map<String, Integer> costPerTroop,
30+
Integer availableCreatures
31+
) {
32+
this.dwellingId = dwellingId;
33+
this.creatureId = creatureId;
34+
this.costPerTroop = costPerTroop;
35+
this.availableCreatures = availableCreatures;
36+
}
37+
38+
DwellingReadModel withAvailableCreatures(Integer availableCreatures) {
39+
this.availableCreatures = availableCreatures;
40+
return this;
41+
}
42+
43+
DwellingReadModel withAvailableCreaturesDecreasedBy(Integer decreasedBy) {
44+
this.availableCreatures = this.availableCreatures - decreasedBy;
45+
return this;
46+
}
47+
48+
public String getDwellingId() {
49+
return dwellingId;
50+
}
51+
52+
public String getCreatureId() {
53+
return creatureId;
54+
}
55+
56+
public Map<String, Integer> getCostPerTroop() {
57+
return costPerTroop;
58+
}
59+
60+
public Integer getAvailableCreatures() {
61+
return availableCreatures;
62+
}
63+
64+
protected DwellingReadModel() {
65+
// Required by JPA
66+
}
67+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.dddheroes.heroesofddd.creaturerecruitment.read;
2+
3+
import com.dddheroes.heroesofddd.creaturerecruitment.write.builddwelling.DwellingBuilt;
4+
import com.dddheroes.heroesofddd.creaturerecruitment.write.changeavailablecreatures.AvailableCreaturesChanged;
5+
import com.dddheroes.heroesofddd.creaturerecruitment.write.recruitcreature.CreatureRecruited;
6+
import org.axonframework.eventhandling.EventHandler;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
class DwellingReadModelProjector {
11+
12+
private final DwellingReadModelRepository repository;
13+
14+
DwellingReadModelProjector(DwellingReadModelRepository repository) {
15+
this.repository = repository;
16+
}
17+
18+
@EventHandler
19+
void on(DwellingBuilt event) {
20+
var state = new DwellingReadModel(
21+
event.dwellingId(),
22+
event.creatureId(),
23+
event.costPerTroop(),
24+
0
25+
);
26+
repository.save(state);
27+
}
28+
29+
@EventHandler
30+
void on(AvailableCreaturesChanged event) {
31+
repository.findById(event.dwellingId())
32+
.map(state -> state.withAvailableCreatures(event.changedTo()))
33+
.ifPresent(repository::save);
34+
}
35+
36+
@EventHandler
37+
void on(CreatureRecruited event) {
38+
repository.findById(event.dwellingId())
39+
.map(state -> state.withAvailableCreaturesDecreasedBy(event.quantity()))
40+
.ifPresent(repository::save);
41+
}
42+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.dddheroes.heroesofddd.creaturerecruitment.read;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.springframework.stereotype.Repository;
5+
6+
@Repository
7+
public interface DwellingReadModelRepository extends JpaRepository<DwellingReadModel, String> {
8+
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.dddheroes.heroesofddd.creaturerecruitment.read.getdwellingbyid;
2+
3+
import com.dddheroes.heroesofddd.creaturerecruitment.write.DwellingId;
4+
5+
public record GetDwellingById(DwellingId dwellingId) {
6+
7+
public static GetDwellingById query(String dwellingId) {
8+
return new GetDwellingById(DwellingId.of(dwellingId));
9+
}
10+
11+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.dddheroes.heroesofddd.creaturerecruitment.read.getdwellingbyid;
2+
3+
import com.dddheroes.heroesofddd.creaturerecruitment.read.DwellingReadModel;
4+
import com.dddheroes.heroesofddd.creaturerecruitment.read.DwellingReadModelRepository;
5+
import org.axonframework.queryhandling.QueryHandler;
6+
import org.springframework.stereotype.Component;
7+
8+
import java.util.Optional;
9+
10+
@Component
11+
class GetDwellingByIdQueryHandler {
12+
private final DwellingReadModelRepository dwellingReadModelRepository;
13+
14+
GetDwellingByIdQueryHandler(DwellingReadModelRepository dwellingReadModelRepository) {
15+
this.dwellingReadModelRepository = dwellingReadModelRepository;
16+
}
17+
18+
@QueryHandler
19+
DwellingReadModel handle(GetDwellingById query){
20+
return dwellingReadModelRepository.findById(query.dwellingId().raw()).orElse(null);
21+
}
22+
}

src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.dddheroes.heroesofddd.shared.Amount;
1313
import com.dddheroes.heroesofddd.shared.Cost;
1414
import com.dddheroes.heroesofddd.shared.CreatureId;
15+
import com.dddheroes.heroesofddd.shared.DomainRule;
1516
import org.axonframework.commandhandling.CommandHandler;
1617
import org.axonframework.eventsourcing.EventSourcingHandler;
1718
import org.axonframework.modelling.command.AggregateCreationPolicy;
@@ -31,7 +32,7 @@ class Dwelling {
3132
private Amount availableCreatures;
3233

3334
@CommandHandler
34-
@CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING)
35+
@CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING) // performance downside in comparison to constructor
3536
void handle(BuildDwelling command) {
3637
new OnlyNotBuiltBuildingCanBeBuild(dwellingId).verify();
3738

@@ -72,7 +73,11 @@ void on(AvailableCreaturesChanged event) {
7273
}
7374

7475
@CommandHandler
76+
// @CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING)
7577
void handle(RecruitCreature command) {
78+
// if(dwellingId == null){
79+
// throw new DomainRule.ViolatedException("Only not built building can be build");
80+
// }
7681
new RecruitCreaturesNotExceedAvailableCreatures(
7782
creatureId,
7883
availableCreatures,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.dddheroes.heroesofddd;
2+
3+
import io.axoniq.axonserver.connector.AxonServerConnection;
4+
import org.axonframework.axonserver.connector.AxonServerConfiguration;
5+
import org.axonframework.axonserver.connector.AxonServerConnectionManager;
6+
import org.axonframework.springboot.service.connection.AxonServerConnectionDetails;
7+
import org.axonframework.test.server.AxonServerContainer;
8+
import org.junit.jupiter.api.*;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.boot.test.context.SpringBootTest;
11+
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
12+
import org.springframework.context.annotation.Import;
13+
import org.testcontainers.junit.jupiter.Container;
14+
15+
import java.time.Duration;
16+
17+
import static org.awaitility.Awaitility.await;
18+
import static org.junit.jupiter.api.Assertions.*;
19+
20+
@Import(TestcontainersConfiguration.class)
21+
@SpringBootTest
22+
class TestAxonServerConnection {
23+
24+
@Autowired
25+
private AxonServerContainer axonServer;
26+
27+
@Autowired
28+
private AxonServerConfiguration axonServerConfiguration;
29+
30+
@Autowired
31+
private AxonServerConnectionDetails connectionDetails;
32+
33+
@Autowired
34+
private AxonServerConnectionManager axonServerConnectionManager;
35+
36+
@Test
37+
void verifyApplicationStartsNormallyWithAxonServerInstance() {
38+
assertTrue(axonServer.isRunning());
39+
assertNotNull(connectionDetails);
40+
assertTrue(connectionDetails.routingServers().endsWith("" + axonServer.getGrpcPort()));
41+
assertNotNull(axonServerConfiguration);
42+
43+
assertNotEquals("localhost:8024", axonServerConfiguration.getServers());
44+
45+
AxonServerConnection connection = axonServerConnectionManager.getConnection();
46+
47+
await().atMost(Duration.ofSeconds(5))
48+
.untilAsserted(() -> assertTrue(connection.isConnected()));
49+
}
50+
}
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
package com.dddheroes.heroesofddd;
22

3+
import org.axonframework.test.server.AxonServerContainer;
34
import org.springframework.boot.test.context.TestConfiguration;
45
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
56
import org.springframework.context.annotation.Bean;
67
import org.testcontainers.containers.PostgreSQLContainer;
78
import org.testcontainers.utility.DockerImageName;
89

910
@TestConfiguration(proxyBeanMethods = false)
10-
class TestcontainersConfiguration {
11+
public class TestcontainersConfiguration {
1112

1213
@Bean
1314
@ServiceConnection
1415
PostgreSQLContainer<?> postgresContainer() {
1516
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
1617
}
18+
19+
@Bean
20+
@ServiceConnection
21+
AxonServerContainer axonServerContainer() {
22+
return new AxonServerContainer().withDevMode(true);
23+
}
1724
}

0 commit comments

Comments
 (0)