draft of hexagonal repository

This commit is contained in:
Michał Michaluk
2017-12-04 11:19:21 +01:00
parent 5da34c5f95
commit 8a91308560
15 changed files with 479 additions and 0 deletions

95
app-monolith/pom.xml Normal file
View 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>

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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) {
}
}

View File

@@ -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) {
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View 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

View 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

View File

@@ -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)
}
}

View 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