16.2. Persistent Storage

16.2.1. Database: JPA and Hibernate

Enrich the domain POJOs (solution, entities and problem facts) with JPA annotations to store them in a database.

Note

Do not confuse JPA’s @Entity annotation with Planner’s @PlanningEntity annotation. They can appear both on the same class:

@PlanningEntity // OptaPlanner annotation
@Entity // JPA annotation
public class Talk {...}

Add a dependency to the optaplanner-persistence-jpa JAR to take advantage of these extra integration features:

16.2.1.1. JPA and Hibernate: Persisting a Score

When a Score is persisted into a relational database, JPA and Hibernate will default to Java serializing it to a BLOB column. This has several disadvantages:

  • The Java serialization format of Score classes is currently not backwards compatible. Upgrading to a newer Planner version can break reading an existing database.
  • The score is not easily readable for a query executed in the database console. This is annoying during development.
  • The score cannot be used in a SQL or JPA-QL query to filter the results: for example to query all infeasible schedules.

To avoid these issues, configure it to use 2 INTEGER columns instead by using the appropriate *ScoreHibernateType for your Score type, for example for a HardSoftScore:

@PlanningSolution
@Entity
@TypeDef(defaultForType = HardSoftScore.class, typeClass = HardSoftScoreHibernateType.class)
public class CloudBalance implements Solution<HardSoftScore> {

    @Columns(columns = {@Column(name = "hardScore"), @Column(name = "softScore")})
    protected HardSoftScore score;

    ...
}
Note

Configure the same number of @Column annotations as the number of score levels in the score, otherwise Hibernate will fail fast because a property mapping has the wrong number of columns.

In this case, the DDL will look like this:

CREATE TABLE CloudBalance(
    ...
    hardScore INTEGER,
    softScore INTEGER
);

When using a BigDecimal based Score, specify the precision and scale of the columns to avoid silent rounding:

@PlanningSolution
@Entity
@TypeDef(defaultForType = HardSoftBigDecimalScore.class, typeClass = HardSoftBigDecimalScoreHibernateType.class)
public class CloudBalance implements Solution<HardSoftBigDecimalScore> {

    @Columns(columns = {
            @Column(name = "hardScore", precision = 10, scale = 5),
            @Column(name = "softScore", precision = 10, scale = 5)})
    protected HardSoftBigDecimalScore score;

    ...
}

When using any type of bendable Score, specify the hard and soft level sizes as parameters:

@PlanningSolution
@Entity
@TypeDef(defaultForType = BendableScore.class, typeClass = BendableScoreHibernateType.class, parameters = {
        @Parameter(name = "hardLevelsSize", value = "3"),
        @Parameter(name = "softLevelsSize", value = "2")})
public class Schedule implements Solution<BendableScore> {

    @Columns(columns = {
            @Column(name = "hard0Score"),
            @Column(name = "hard1Score"),
            @Column(name = "hard2Score"),
            @Column(name = "soft0Score"),
            @Column(name = "soft1Score")})
    protected BendableScore score;

    ...
}

All this support is Hibernate specific because currently JPA 2.1’s converters do not support converting to multiple columns.

16.2.1.2. JPA and Hibernate: Planning Cloning

In JPA and Hibernate, there is usually a @ManyToOne relationship from most problem fact classes to the planning solution class. Therefore, the problem fact classes reference the planning solution class, which implies that when the solution is planning cloned, they need to be cloned too. Use an @DeepPlanningClone on each such problem fact class to enforce that:

@PlanningSolution // OptaPlanner annotation
@Entity // JPA annotation
public class Conference {

    @OneToMany(mappedBy = "conference")
    private List<Room> roomList;

    ...
}
@DeepPlanningClone // OptaPlanner annotation: Force the default planning cloner to planning clone this class too
@Entity // JPA annotation
public class Room {

    @ManyToOne
    private Conference conference; // Because of this reference, this problem fact needs to be planning cloned too

}

Neglecting to do this can lead to persisting duplicate solutions, JPA exceptions or other side effects.

16.2.2. XML or JSON: XStream

Enrich the domain POJOs (solution, entities and problem facts) with XStream annotations to serialize them to/from XML or JSON.

Add a dependency to the optaplanner-persistence-xstream JAR to take advantage of these extra integration features:

16.2.2.1. XStream: Marshalling a Score

When a Score is marshalled to XML or JSON by the default XStream configuration, it’s verbose and ugly. To fix that, configure the XStreamScoreConverter and provide the ScoreDefinition as a parameter:

@PlanningSolution
@XStreamAlias("CloudBalance")
public class CloudBalance implements Solution<HardSoftScore> {

    @XStreamConverter(value = XStreamScoreConverter.class, types = {HardSoftScoreDefinition.class})
    private HardSoftScore score;

    ...
}

For example, this will generate a pretty XML:

<CloudBalance>
   ...
   <score>0hard/-200soft</score>
</CloudBalance>

To use this for any type of bendable score, also provide 2 int parameters to define hardLevelsSize and softLevelsSize:

@PlanningSolution
@XStreamAlias("Schedule")
public class Schedule implements Solution<BendableScore> {

    @XStreamConverter(value = XStreamScoreConverter.class, types = {BendableScoreDefinition.class}, ints = {2, 3})
    private BendableScore score;

    ...
}

For example, this will generate:

<Schedule>
   ...
   <score>0/0/-100/-20/-3</score>
</Schedule>

16.2.3. XML or JSON: JAXB

Enrich the domain POJOs (solution, entities and problem facts) with JAXB annotations to serialize them to/from XML or JSON.