From 8a913085600aeb8942439b021a23b3e436d846a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Michaluk?= Date: Mon, 4 Dec 2017 11:19:21 +0100 Subject: [PATCH] draft of hexagonal repository --- app-monolith/pom.xml | 95 ++++++++++++++++++ .../com/bottega/factory/AppConfiguration.java | 25 +++++ .../demand/forecasting/DemandEntity.java | 25 +++++ .../forecasting/DemandEventsMapping.java | 13 +++ .../demand/forecasting/DemandRepository.java | 25 +++++ .../demand/forecasting/DemandService.java | 24 +++++ .../forecasting/ProductDemandEntity.java | 23 +++++ .../forecasting/persistence/DemandDao.java | 15 +++ .../persistence/ProductDemandDao.java | 13 +++ .../pl/com/bottega/tools/JsonConverter.java | 45 +++++++++ .../pl/com/bottega/tools/TechnicalId.java | 18 ++++ .../src/main/resources/application.properties | 12 +++ app-monolith/src/main/resources/tasks.txt | 37 +++++++ .../forecasting/DemandRepositoryTest.groovy | 96 +++++++++++++++++++ .../src/test/resources/application.properties | 13 +++ 15 files changed, 479 insertions(+) create mode 100644 app-monolith/pom.xml create mode 100644 app-monolith/src/main/java/pl/com/bottega/factory/AppConfiguration.java create mode 100644 app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEntity.java create mode 100644 app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsMapping.java create mode 100644 app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandRepository.java create mode 100644 app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandService.java create mode 100644 app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandEntity.java create mode 100644 app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DemandDao.java create mode 100644 app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/ProductDemandDao.java create mode 100644 app-monolith/src/main/java/pl/com/bottega/tools/JsonConverter.java create mode 100644 app-monolith/src/main/java/pl/com/bottega/tools/TechnicalId.java create mode 100644 app-monolith/src/main/resources/application.properties create mode 100644 app-monolith/src/main/resources/tasks.txt create mode 100644 app-monolith/src/test/groovy/pl/com/bottega/factory/demand/forecasting/DemandRepositoryTest.groovy create mode 100644 app-monolith/src/test/resources/application.properties diff --git a/app-monolith/pom.xml b/app-monolith/pom.xml new file mode 100644 index 0000000..bbd9082 --- /dev/null +++ b/app-monolith/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + + pl.com.bottega + app-monolith + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 1.5.8.RELEASE + + + + + pl.com.bottega + shared-kernel-model + 1.0-SNAPSHOT + + + pl.com.bottega + demand-forecasting-model + 1.0-SNAPSHOT + + + pl.com.bottega + shortages-prediction-model + 1.0-SNAPSHOT + + + + org.projectlombok + lombok + 1.16.18 + provided + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-rest + + + org.springframework.boot + spring-boot-starter-test + test + + + org.postgresql + postgresql + 42.1.4 + runtime + + + org.spockframework + spock-core + 1.1-groovy-2.4 + test + + + org.spockframework + spock-spring + 1.1-groovy-2.4 + test + + + net.bytebuddy + byte-buddy + 1.7.9 + + + + + 1.8 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/AppConfiguration.java b/app-monolith/src/main/java/pl/com/bottega/factory/AppConfiguration.java new file mode 100644 index 0000000..3e3eeb2 --- /dev/null +++ b/app-monolith/src/main/java/pl/com/bottega/factory/AppConfiguration.java @@ -0,0 +1,25 @@ +package pl.com.bottega.factory; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Bean; +import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters; + +import java.time.Clock; + +@SpringBootApplication +@EntityScan( + basePackageClasses = {AppConfiguration.class, Jsr310JpaConverters.class} +) +public class AppConfiguration { + + public static void main(String[] args) { + SpringApplication.run(AppConfiguration.class, args); + } + + @Bean + public Clock clock() { + return Clock.systemDefaultZone(); + } +} diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEntity.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEntity.java new file mode 100644 index 0000000..4fdac11 --- /dev/null +++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEntity.java @@ -0,0 +1,25 @@ +package pl.com.bottega.factory.demand.forecasting; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDate; + +@Data +@Entity(name = "Demand") +@NoArgsConstructor +@EqualsAndHashCode(of = "id") +public class DemandEntity { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + Long id; + + @ManyToOne + ProductDemandEntity product; + @Column + LocalDate date; + +} diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsMapping.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsMapping.java new file mode 100644 index 0000000..e5a7e46 --- /dev/null +++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsMapping.java @@ -0,0 +1,13 @@ +package pl.com.bottega.factory.demand.forecasting; + +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +@Lazy +@Component +class DemandEventsMapping implements DemandEvents { + + @Override + public void emit(DemandedLevelsChanged event) { + } +} diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandRepository.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandRepository.java new file mode 100644 index 0000000..304fcf7 --- /dev/null +++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandRepository.java @@ -0,0 +1,25 @@ +package pl.com.bottega.factory.demand.forecasting; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; +import pl.com.bottega.factory.demand.forecasting.persistence.DemandDao; +import pl.com.bottega.factory.demand.forecasting.persistence.ProductDemandDao; + +import java.time.Clock; + +@Component +@AllArgsConstructor +class DemandRepository { + + private Clock clock; + private DemandEventsMapping events; + private ProductDemandDao rootDao; + private DemandDao demandDao; + + ProductDemand get(String refNo) { + return null; + } + + void save(ProductDemand model) { + } +} diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandService.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandService.java new file mode 100644 index 0000000..db35df4 --- /dev/null +++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandService.java @@ -0,0 +1,24 @@ +package pl.com.bottega.factory.demand.forecasting; + +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +public class DemandService { + + private DemandRepository repository; + + public void process(Document document) { + ProductDemand model = repository.get(document.getRefNo()); + model.process(document); + repository.save(model); + } + + public void adjust(AdjustDemand adjustDemand) { + ProductDemand model = repository.get(adjustDemand.getRefNo()); + model.adjust(adjustDemand); + repository.save(model); + } +} diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandEntity.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandEntity.java new file mode 100644 index 0000000..60fccbc --- /dev/null +++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandEntity.java @@ -0,0 +1,23 @@ +package pl.com.bottega.factory.demand.forecasting; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Data +@Entity(name = "ProductDemand") +@NoArgsConstructor +@EqualsAndHashCode(of = "id") +public class ProductDemandEntity { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + Long id; + @Version + Long version; + @Column + String refNo; + +} diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DemandDao.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DemandDao.java new file mode 100644 index 0000000..9c34bff --- /dev/null +++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DemandDao.java @@ -0,0 +1,15 @@ +package pl.com.bottega.factory.demand.forecasting.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import pl.com.bottega.factory.demand.forecasting.DemandEntity; + +import java.time.LocalDate; +import java.util.List; + +@Repository +public interface DemandDao extends JpaRepository { + + List findByProductRefNoAndDateGreaterThanEqual(String refNo, LocalDate now); + +} diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/ProductDemandDao.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/ProductDemandDao.java new file mode 100644 index 0000000..95c2d65 --- /dev/null +++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/ProductDemandDao.java @@ -0,0 +1,13 @@ +package pl.com.bottega.factory.demand.forecasting.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import pl.com.bottega.factory.demand.forecasting.ProductDemandEntity; + +@Repository +public interface ProductDemandDao extends JpaRepository { + + ProductDemandEntity findById(Long id); + + ProductDemandEntity findByRefNo(String refNo); +} diff --git a/app-monolith/src/main/java/pl/com/bottega/tools/JsonConverter.java b/app-monolith/src/main/java/pl/com/bottega/tools/JsonConverter.java new file mode 100644 index 0000000..b77cef9 --- /dev/null +++ b/app-monolith/src/main/java/pl/com/bottega/tools/JsonConverter.java @@ -0,0 +1,45 @@ +package pl.com.bottega.tools; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import javax.persistence.AttributeConverter; +import java.io.IOException; + +public abstract class JsonConverter implements AttributeConverter { + + private static final ObjectMapper objectMapper = new ObjectMapper() + .setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE) + .setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .setVisibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.ANY) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID); + + private final Class type; + + public JsonConverter(Class type) { + this.type = type; + } + + @Override + public String convertToDatabaseColumn(T object) { + try { + return objectMapper.writeValueAsString(object); + } catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public T convertToEntityAttribute(String data) { + try { + return objectMapper.readValue(data, type); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/app-monolith/src/main/java/pl/com/bottega/tools/TechnicalId.java b/app-monolith/src/main/java/pl/com/bottega/tools/TechnicalId.java new file mode 100644 index 0000000..ee568df --- /dev/null +++ b/app-monolith/src/main/java/pl/com/bottega/tools/TechnicalId.java @@ -0,0 +1,18 @@ +package pl.com.bottega.tools; + +public interface TechnicalId { + + Long getId(); + + default boolean isPersisted() { + return getId() != null; + } + + static Long get(Object id) { + return (id instanceof TechnicalId) ? ((TechnicalId) id).getId() : null; + } + + static boolean isPersisted(Object id) { + return (id instanceof TechnicalId) && ((TechnicalId) id).isPersisted(); + } +} diff --git a/app-monolith/src/main/resources/application.properties b/app-monolith/src/main/resources/application.properties new file mode 100644 index 0000000..9bd460b --- /dev/null +++ b/app-monolith/src/main/resources/application.properties @@ -0,0 +1,12 @@ +spring.main.banner-mode=off + +spring.jpa.database=default +spring.jpa.generate-ddl=false + +spring.datasource.url=jdbc:postgresql://localhost:5432/postgres +spring.datasource.username=postgres +spring.datasource.password= +spring.datasource.driver-class-name=org.postgresql.Driver + +logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n +logging.level.=error diff --git a/app-monolith/src/main/resources/tasks.txt b/app-monolith/src/main/resources/tasks.txt new file mode 100644 index 0000000..78cadc1 --- /dev/null +++ b/app-monolith/src/main/resources/tasks.txt @@ -0,0 +1,37 @@ + +Rest: +1) crud for json document +2) query calculated +2') query for pre-calculated projection +3) naive REST aggregate commands +4) command based rest +5) application domain + +6) Eventual consistency + Read your own writes (E-tag, Expect, Retry-After) + + +Aggregate Persistence: +1) optimistic force increment +2) normalisation of aggregate entities (Daily Demand) +3) mapper (Daily Demand) +4) wrapper (Product Demand) +5) event sourcing (Product Demand) + +http://pillopl.github.io/eventual-consistency-and-rest/ +https://github.com/pilloPl/orders-manager +http://groovy-lang.org/json.html + + + + + +vattenfall: + +why status is active when card is EXPIRED? +rfid: 04938C9AF74D80, point: EVB-P1552169 2 (EVB-P1552169_2) EXPIRED Revoked(authId=04938C9AF74D80, status=ACTIVE) + +exceptions: +null pointer +more results for query single card + diff --git a/app-monolith/src/test/groovy/pl/com/bottega/factory/demand/forecasting/DemandRepositoryTest.groovy b/app-monolith/src/test/groovy/pl/com/bottega/factory/demand/forecasting/DemandRepositoryTest.groovy new file mode 100644 index 0000000..79cb027 --- /dev/null +++ b/app-monolith/src/test/groovy/pl/com/bottega/factory/demand/forecasting/DemandRepositoryTest.groovy @@ -0,0 +1,96 @@ +package pl.com.bottega.factory.demand.forecasting + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.annotation.Commit +import pl.com.bottega.factory.demand.forecasting.persistence.DemandDao +import pl.com.bottega.factory.demand.forecasting.persistence.ProductDemandDao +import spock.lang.PendingFeature +import spock.lang.Specification + +import javax.transaction.Transactional +import java.time.Clock +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId + +@SpringBootTest +@Transactional +@Commit +class DemandRepositoryTest extends Specification { + + def clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()) + def events = Mock(DemandEventsMapping) + @Autowired + ProductDemandDao rootDao + @Autowired + DemandDao demandDao + + DemandRepository repository + + final def today = LocalDate.now(clock) + + def setup() { + demandDao.deleteAllInBatch() + rootDao.deleteAllInBatch() + repository = new DemandRepository(clock, events, rootDao, demandDao) + } + + @PendingFeature + def "persists new demand"() { + given: + rootDao.save(new ProductDemandEntity("3009000")) + + when: + def object = repository.get("3009000") + object.adjust(new AdjustDemand("3009000", [ + (today): Adjustment.strong(Demand.of(2000)) + ])) + repository.save(object) + + then: + demandDao.findAll().size() == 1 + } + + @PendingFeature + def "updates existing demand"() { + given: + def root = rootDao.save(new ProductDemandEntity("3009000")) + def demand = new DemandEntity(root, today) + demand.setDemand(Demand.of(1000)) + demandDao.save(demand) + + when: + def object = repository.get("3009000") + object.adjust(new AdjustDemand("3009000", [ + (today): Adjustment.strong(Demand.of(2000)) + ])) + repository.save(object) + + then: + def demands = demandDao.findAll() + demands.size() == 1 + demand.every { it.getAdjustmentLevel() == 2000 } + } + + @PendingFeature + def "doesn't fetch historical data"() { + given: + def root = rootDao.save(new ProductDemandEntity("3009000")) + def old = new DemandEntity(root, today.minusDays(1)) + old.setDemand(Demand.of(10000)) + demandDao.save(old) + + def todays = new DemandEntity(root, today) + todays.setDemand(Demand.of(1000)) + demandDao.save(todays) + + when: + def demands = demandDao.findByProductRefNoAndDateGreaterThanEqual("3009000", today) + + then: + demands.size() == 1 + demands.contains(todays) + !demands.contains(old) + } +} diff --git a/app-monolith/src/test/resources/application.properties b/app-monolith/src/test/resources/application.properties new file mode 100644 index 0000000..9c8fff7 --- /dev/null +++ b/app-monolith/src/test/resources/application.properties @@ -0,0 +1,13 @@ +spring.main.banner-mode=off + +spring.jpa.database=default +spring.jpa.generate-ddl=true + +spring.datasource.url=jdbc:postgresql://localhost:5432/postgres +spring.datasource.username=postgres +spring.datasource.password= +spring.datasource.driver-class-name=org.postgresql.Driver + +logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n +logging.level.org.hibernate.SQL=debug +logging.level.=error