04tudor / loyalty-campaign-service

Java project Loyalty Campaign Service using Hexagonal Architecture
0 stars 0 forks source link

Create Persistence module #35

Closed DemiusAcademius closed 5 months ago

DemiusAcademius commented 6 months ago

Materials: https://www.baeldung.com/database-migrations-with-flyway https://flywaydb.org/ https://database-rider.github.io/database-rider/

pom.xm examplel

<?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>

    <parent>
        <groupId>md.maib.retail</groupId>
        <artifactId>loyalty-campaign</artifactId>
        <version>${sha1}</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>

    <artifactId>loyalty-campaign-infrastructure-persistence</artifactId>
    <name>Infrastructure Persistence</name>

    <dependencies>
        <dependency>
            <groupId>md.maib.retail</groupId>
            <artifactId>loyalty-campaign-domain</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>

        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </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-cache</artifactId>
        </dependency>

        <dependency>
            <groupId>md.maib.retail</groupId>
            <artifactId>loyalty-campaign-domain</artifactId>
            <version>${project.version}</version>
            <classifier>tests</classifier>
            <type>test-jar</type>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.github.database-rider</groupId>
            <artifactId>rider-spring</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.playtika.testcontainers</groupId>
            <artifactId>embedded-postgresql</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>jakarta.el</artifactId>
            <version>4.0.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Caffeine used for cashing Flyway for migrations com.playtika.testcontainers for integration testing with containers Rider also for integration testing

DemiusAcademius commented 6 months ago

module-structure-2024-05-23_09-45

This is only an example

DemiusAcademius commented 6 months ago

from main pom.xml


    <properties>
        <caffeine.version>3.1.8</caffeine.version>
        <rider.version>1.42.0</rider.version>
        <spring-cloud.version>2023.0.1</spring-cloud.version>
        <testcontainers-spring-boot.version>3.1.5</testcontainers-spring-boot.version>
    </properties>
DemiusAcademius commented 6 months ago

from main pom.xml


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.github.ben-manes.caffeine</groupId>
                <artifactId>caffeine</artifactId>
                <version>${caffeine.version}</version>
            </dependency>
            <dependency>
                <groupId>com.playtika.testcontainers</groupId>
                <artifactId>testcontainers-spring-boot-bom</artifactId>
                <version>${testcontainers-spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.github.database-rider</groupId>
                <artifactId>rider-spring</artifactId>
                <version>${rider.version}</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
DemiusAcademius commented 6 months ago

Dependency order:

  1. runtime
  2. standard
  3. test

In each group alphabetic order

DemiusAcademius commented 6 months ago

Lets concentrate only on campaigns. Event types will be hardcoded

example of migration script

V1__create_campaigns_schema.sql

CREATE TABLE campaigns.campaign (
   id              UUID,
   name            TEXT NOT NULL,
   interval_start  TIMESTAMPTZ NOT NULL,
   interval_end    TIMESTAMPTZ NOT NULL,
   loyalty_event_type_id UUID NOT NULL,
   created_at      TIMESTAMPTZ NOT NULL,
   is_active       BOOLEAN NOT NULL,
   metainfo        JSONB NOT NULL,
   CONSTRAINT pk_campaign_id PRIMARY KEY (id)
);

CREATE INDEX idx_campaign_interval_start ON campaigns.campaign(interval_start);

CREATE TABLE campaigns.rule (
   id           UUID,
   campaign_id  UUID NOT NULL,
   conditions   JSONB NOT NULL,
   effects      JSONB NOT NULL,
   CONSTRAINT pk_rule_id PRIMARY KEY (id),
   CONSTRAINT fk_rule_campaign_id FOREIGN KEY (campaign_id) REFERENCES campaigns.campaign(id)
);

metainfo, conditions and effects will be stored as jsonb

DemiusAcademius commented 6 months ago

example of working with jsonb

package md.maib.retail.loyalty.campaign.adapter.persistence;

import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import md.maib.retail.loyalty.campaign.domain.model.common.CampaignId;
import md.maib.retail.loyalty.campaign.domain.model.manage.CampaignMetadataEntity;
import md.maib.retail.loyalty.campaign.domain.model.manage.Metadata;
import org.hibernate.annotations.ColumnTransformer;
import org.springframework.data.domain.Persistable;

import java.util.Map;
import java.util.UUID;

@Entity
@Table(name = "campaign_metadata", schema = "campaigns")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class CampaignMetadataRecord implements Persistable<UUID> {

    @Id
    @EqualsAndHashCode.Include
    @Column(name = "campaign_id", nullable = false)
    private UUID id;

    @Column(name = "metadata", nullable = false)
    @Convert(converter = EntityFieldJsonConverter.class)
    @ColumnTransformer(write = "?::jsonb")
    private Map<String, Object> metadata;

    @Transient
    private boolean isNew;

    @Override
    public boolean isNew() {
        return isNew;
    }

    public static CampaignMetadataRecord of(CampaignMetadataEntity entity) {
        return new CampaignMetadataRecord(entity.id().value(), entity.metadata().properties(), true);
    }

    public CampaignMetadataEntity toEntity() {
        return new CampaignMetadataEntity(new CampaignId(this.id), new Metadata(this.metadata));
    }
}
DemiusAcademius commented 6 months ago

Converter jsonb <-> Map

package md.maib.retail.loyalty.campaign.adapter.persistence;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import lombok.SneakyThrows;

import java.util.Map;

@Converter(autoApply = true)
public class EntityFieldJsonConverter implements AttributeConverter<Map<String, Object>, String> {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    @SneakyThrows
    public String convertToDatabaseColumn(Map<String, Object> attribute) {
        return objectMapper.writeValueAsString(attribute);
    }

    @Override
    @SneakyThrows
    public Map<String, Object> convertToEntityAttribute(String dbData) {
        return objectMapper.readValue(dbData, new TypeReference<>() {
        });
    }
}
04tudor commented 6 months ago

can you show classes md.maib.retail.loyalty.campaign.domain.model.manage.CampaignMetadataEntity; md.maib.retail.loyalty.campaign.domain.model.manage.Metadata;