samedi 24 août 2019

Propagate and persist changes in a tree in Spring JPA/Boot

I am struggeling with a design - persistence issue . I have a tree and all elements in this tree are subclasses of the same abstract superclass. The tree has a root object with a list of children which themselves can have children, and so on. Changes to attributes of a children would need to be propagated to all parent levels. To make it easier to illustrate, let's say we deal with products which would have multiple releases, having multiple development phases. Each of these would have a lifetime. So we would have something like

@Entity
@Access(AccessType.PROPERTY)    // JPA reading and writing attributes through their getter and setter methods
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
        discriminatorType = DiscriminatorType.STRING,
        name = "lifetime_level",
        length=10
        )

public abstract class LifeTime implements Serializable {

    // all classes are incomplete for sake of brevity

    protected Integer version;                  // Optimistic locking support

    protected LocalDate plannedBegin;           // The calculated, potentially shifted, begin resulting from children assignments
    protected LocalDate plannedEnd;

    private LifeTime parent;

    protected List<LifeTime> children;

    @ManyToOne
    public LifeTime getParent() {
        return parent;
    }

    public void setParent(LifeTime parent) {
        this.parent = parent;
    }

    @OneToMany(mappedBy="parent")
    public List<LifeTime> getChildren() {
        return children;
    }

    private void setChildren(List<LifeTime> children) {
        this.children = FXCollections.observableList(children);
    }

    public void addChild(LifeTime child) {
        children.add(child);
    }

    public LocalDate getPlannedBegin() {        
        return plannedBegin;
    }


    public void setPlannedBegin(LocalDate aBegin) {
        this.plannedBegin = aBegin;
        adjustParentBegin(parent, this);
    }

    public LocalDate getPlannedEnd() {
        return plannedEnd;
    }


    public void setPlannedEnd(LocalDate aEnd) {
        this.plannedEnd = aEnd;
        adjustParentEnd(parent, this);
    }

    protected void adjustParentBegin(LifeTime parent, LifeTime child) {
        if(child.getPlannedBegin.isBefore(parent.getPlannedBegin)) {
            parent.setPlannedBegin(child.getPlannedBegin);
        }
    }

    protected void adjustParentEnd(LifeTime parent, LifeTime child) {
        if(child.getPlannedEnd.isAfter(parent.getPlannedEnd)) {
            parent.setPlannedEnd(child.getPlannedEnd);
        }
    }

    @Version
    public Integer getVersion() {
        return version;
    }

    private void setVersion(Integer version) {
        this.version = version;
    }

}

We would have the concrete classes Product, Release and Phase. All would extend LifeTime. As root object we have a product. The product has children representing releases of the product and each release would have several development phases. We would also have baselines, iterations that we ignore for the moment. Next, we have somewhere a service class that cares for handling the business logic:

@Service
public class LifeTimeService {

    @Autowired ProductRepository productRepository;
    @Autowired ReleaseRepository releaseRepository;
    @Autowired PhaseRepository phaseRepository;

    public Product createProduct() {
        Product product = new Product();
        release.setPlannedBegin(LocalDate.now());
        release.setPlannedEnd(LocalDate.now());

        product = productRepository.save(product);  // without will throw TransientPropertyValueException: object references an unsaved transient instance when saving release

        createRelease(product);

        return productRepository.save(product);
    }

    public Release createRelease(Product product) {
        Release release = new Release();
        product.addChild(release);
        release.setParent(product);

        release.setPlannedBegin(product.getPlannedBegin());     // initial values
        release.setPlannedEnd(product.getPlannedEnd());

        release = releaseRepository.save(release);      // without will throw TransientPropertyValueException: object references an unsaved transient instance when saving phases

        addPhases(release);     // add phases and adjust begin and end of the release

        return releaseRepository.save(release);
    }

    protected void addPhases(Release release) {

        LocalDate date = release.getPlannedBegin;

        Phase phase1 = new Phase();
        release.addChild(phase1);
        phase1.setParent(release);

        phase1.setPlannedBegin(date);
        date = date.plusMonth(3);
        phase1.setPlannedEnd(date);

        phaseRepository.save(phase1);

        phase2 = new Phase();
        release.addChild(phase2);
        phase2.setParent(release);

        phase2.setPlannedBegin(date);
        date = date.plusMonth(3);
        phase2.setPlannedEnd(date);

        phaseRepository.save(phase2);
    }

}

Let's say we have Controller class, that makes use of the Service

@Controller
public class MyController {

    @Autowired LifeTimeService service;

    protected Product product;

    public void myTest() {
        Product product = service.createProduct();  // this creates a product with an initial release and all phases
        Release release = service.createRelease(product);   // now the product has a later plannedEnd than the corresponding database row
    }
}

Obviously, I want that the createRelease method creates and returns a release. The issue is that it alters all levels of the tree: It creates phases but also changes the parent product begin and end date. I would need to save the product after I call createRelease to persist this change. This approach doesn't scale if the tree has more levels. Otherwise, if I save the product within createRelease, the product in the myTest method would have the wrong version. Or createRelease returns the saved parent - what is counter intuitive - and I have to code a method which return last created release. This is all unsatisfying.

While the above class example follows the Domain Driven Design, whereby each object is in charge of securing its integrity, I was although thinking about Anemic Domain Model and moving the two adjustment methods to the service class and make them recursive. But I don't see how it changes or fixes the above issue.

In reality my tree goes over at least 5 hierarchical levels. So whatever solution you propose, it should scale to multiple levels.

Aucun commentaire:

Enregistrer un commentaire