draft of hexagonal repository
This commit is contained in:
95
app-monolith/pom.xml
Normal file
95
app-monolith/pom.xml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>pl.com.bottega</groupId>
|
||||
<artifactId>app-monolith</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>1.5.8.RELEASE</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>pl.com.bottega</groupId>
|
||||
<artifactId>shared-kernel-model</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>pl.com.bottega</groupId>
|
||||
<artifactId>demand-forecasting-model</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>pl.com.bottega</groupId>
|
||||
<artifactId>shortages-prediction-model</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.16.18</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-rest</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.1.4</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.spockframework</groupId>
|
||||
<artifactId>spock-core</artifactId>
|
||||
<version>1.1-groovy-2.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.spockframework</groupId>
|
||||
<artifactId>spock-spring</artifactId>
|
||||
<version>1.1-groovy-2.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy</artifactId>
|
||||
<version>1.7.9</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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<DemandEntity, Long> {
|
||||
|
||||
List<DemandEntity> findByProductRefNoAndDateGreaterThanEqual(String refNo, LocalDate now);
|
||||
|
||||
}
|
||||
@@ -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, Long> {
|
||||
|
||||
ProductDemandEntity findById(Long id);
|
||||
|
||||
ProductDemandEntity findByRefNo(String refNo);
|
||||
}
|
||||
@@ -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<T> implements AttributeConverter<T, String> {
|
||||
|
||||
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<T> type;
|
||||
|
||||
public JsonConverter(Class<T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
12
app-monolith/src/main/resources/application.properties
Normal file
12
app-monolith/src/main/resources/application.properties
Normal file
@@ -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
|
||||
37
app-monolith/src/main/resources/tasks.txt
Normal file
37
app-monolith/src/main/resources/tasks.txt
Normal file
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
13
app-monolith/src/test/resources/application.properties
Normal file
13
app-monolith/src/test/resources/application.properties
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user