如何使用复合键映射多对多

how to map Many to Many with composite key

我有以下表格

Trainingplan
    TrainingplanID int(11) AI PK
    Trainer int(11)
    Client int(11)
    validFrom date
    validTo date
    type int(11)

TrainingplanExercises
    trainingplan int(11) PK
    exercise int(11) PK
    parameter int(11) PK
    value varchar(45)

不,我在将它们与 Hibernate 连接时遇到问题。我做了以下事情: 包豆;

@Entity
@Table(name = "Trainingplan")
public class Training {

    private IntegerProperty id;
    private ObjectProperty<Person> client;
    private ObjectProperty<Person> trainer;
    private ObjectProperty<Date> validFrom;
    private ObjectProperty<Date> validTo;
    private ObjectProperty<TrainingplanType> type;
    private List<TrainingplanExercise> exercises;

    public Training(int id, Person client, Person trainer, Date validFrom, Date validTo, TrainingplanType type) {
        this.id = new SimpleIntegerProperty(id);
        this.client = new SimpleObjectProperty<>(client);
        this.trainer = new SimpleObjectProperty<>(trainer);
        this.validFrom = new SimpleObjectProperty<>(validFrom);
        this.validTo = new SimpleObjectProperty<>(validTo);
        this.type = new SimpleObjectProperty<>(type);
        exercises = FXCollections.observableArrayList();
    }

    public Training(Person client, Person trainer, Date validFrom, Date validTo, TrainingplanType type){
        this(0, client, trainer, validFrom, validTo, type);
    }

    public Training(){
        this(0, null,null,null,null, null);
    }

    @OneToOne
    @JoinColumn(name = "client")
    public Person getClient() {
        return client.get();
    }

    public ObjectProperty<Person> clientProperty() {
        return client;
    }

    public void setClient(Person client) {
        this.client.set(client);
    }

    @OneToOne
    @JoinColumn(name = "trainer")
    public Person getTrainer() {
        return trainer.get();
    }

    public ObjectProperty<Person> trainerProperty() {
        return trainer;
    }

    public void setTrainer(Person trainer) {
        this.trainer.set(trainer);
    }

    @Column
    public Date getValidFrom() {
        return validFrom.get();
    }

    public ObjectProperty<Date> validFromProperty() {
        return validFrom;
    }

    public void setValidFrom(Date validFrom) {
        this.validFrom.set(validFrom);
    }

    @Column
    public Date getValidTo() {
        return validTo.get();
    }

    public ObjectProperty<Date> validTillProperty() {
        return validTo;
    }

    public void setValidTo(Date validTill) {
        this.validTo.set(validTill);
    }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "TrainingplanID")
    public int getId() {
        return id.get();
    }

    public IntegerProperty idProperty() {
        return id;
    }

    public void setId(int id) {
        this.id.set(id);
    }

    @OneToOne
    @JoinColumn(name = "type")
    public TrainingplanType getType() {
        return type.get();
    }

    public ObjectProperty<TrainingplanType> typeProperty() {
        return type;
    }

    public void setType(TrainingplanType type) {
        this.type.set(type);
    }

    @ManyToMany()
    @JoinTable(name="TrainingplanExercises",
            joinColumns={@JoinColumn(name="trainingplan")},
            inverseJoinColumns={@JoinColumn(name="trainingplan"), @JoinColumn(name="exercise"), @JoinColumn(name="parameter")})
    public List<TrainingplanExercise> getExercises() {
        return exercises;
    }

    public void setExercises(List<TrainingplanExercise> exercises) {
        this.exercises = exercises;
    }

    @Override
    public String toString() {
        return "Training{" +
                "id=" + getId() +
                ", client=" + getClient() +
                ", trainer=" + getTrainer() +
                ", validFrom=" + getValidFrom() +
                ", validTill=" + getValidTo() +
                ", type=" + getType() +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Training training = (Training) o;

        return id != null ? id.equals(training.id) : training.id == null;

    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }
}

TrainingplanExercise.java

@Entity
@Table(name = "TrainingplanExercises")
@IdClass(TrainingplanExerciseId.class)
public class TrainingplanExercise {

    private ObjectProperty<Exercise> exercise;
    private ObjectProperty<Training> training;
    private ObjectProperty<String> value;
    private ObjectProperty<Parameter> parameter;

    public TrainingplanExercise(Exercise exercise, Training training, String value, Parameter parameter){
        this.exercise = new SimpleObjectProperty<>(exercise);
        this.training = new SimpleObjectProperty<>(training);
        this.value = new SimpleObjectProperty<>(value);
        this.parameter = new SimpleObjectProperty<>(parameter);
    }

    public TrainingplanExercise(){
        this(null,null,null,null);
    }

    @Id
    @OneToOne
    @JoinColumn(name = "parameter")
    public Parameter getParameter() {
        return parameter.get();
    }

    public ObjectProperty<Parameter> parameterProperty() {
        return parameter;
    }

    public void setParameter(Parameter parameter) {
        this.parameter.set(parameter);
    }

    @Id
    @OneToOne
    @JoinColumn(name = "exercise")
    public Exercise getExercise() {
        return exercise.get();
    }

    public ObjectProperty<Exercise> exerciseProperty() {
        return exercise;
    }

    public void setExercise(Exercise exercise) {
        this.exercise.set(exercise);
    }

    @Id
    @OneToOne
    @JoinColumn(name = "trainingplan")
    public Training getTraining() {
        return training.get();
    }

    public ObjectProperty<Training> trainingProperty() {
        return training;
    }

    public void setTraining(Training training) {
        this.training.set(training);
    }

    @Column(name = "value")
    public String getValue(){
        return value.get();
    }

    public ObjectProperty<String> valueProperty() {
        return value;
    }

    public void setValue(String value) {
        this.value.set(value);
    }

    @Override
    public String toString() {
        return "TrainingplanExercise{" + "exercise=" + exercise + ", training=" + training + ", value=" + value + '}';
    }

}

 class TrainingplanExerciseId implements Serializable{
     protected ObjectProperty<Exercise> exercise;
     protected ObjectProperty<Training> training;
     protected ObjectProperty<Parameter> parameter;

     public TrainingplanExerciseId() {
         if(exercise == null)
             exercise = new SimpleObjectProperty<>(null);

         if(training == null)
             training = new SimpleObjectProperty<>(null);

         if(parameter == null)
             parameter = new SimpleObjectProperty<>(null);
     }

     public TrainingplanExerciseId(ObjectProperty<Exercise> exercise, ObjectProperty<Training> training, ObjectProperty<Parameter> parameter) {
         this.exercise = exercise;
         this.training = training;
         this.parameter = parameter;
     }

     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;

         TrainingplanExerciseId that = (TrainingplanExerciseId) o;

         if (exercise != null ? !exercise.equals(that.exercise) : that.exercise != null) return false;
         if (training != null ? !training.equals(that.training) : that.training != null) return false;
         return parameter != null ? parameter.equals(that.parameter) : that.parameter == null;

     }

     @Override
     public int hashCode() {
         int result = exercise != null ? exercise.hashCode() : 0;
         result = 31 * result + (training != null ? training.hashCode() : 0);
         result = 31 * result + (parameter != null ? parameter.hashCode() : 0);
         return result;
     }

     public Exercise getExercise() {
         return exercise.get();
     }

     public ObjectProperty<Exercise> exerciseProperty() {
         return exercise;
     }

     public void setExercise(Exercise exercise) {
         this.exercise.set(exercise);
     }

     public Training getTraining() {
         return training.get();
     }

     public ObjectProperty<Training> trainingProperty() {
         return training;
     }

     public void setTraining(Training training) {
         this.training.set(training);
     }

     public Parameter getParameter() {
         return parameter.get();
     }

     public ObjectProperty<Parameter> parameterProperty() {
         return parameter;
     }

     public void setParameter(Parameter parameter) {
         this.parameter.set(parameter);
     }
 }

现在当我想保存一个新的训练时,我得到这个错误:

Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 'TrainingplanID' in 'field list'

因为这个SQL:

Hibernate: insert into TrainingplanExercises (TrainingplanID, trainingplan, exercise, parameter) values (?, ?, ?, ?)

我该如何解决这个问题? 如果我将 joinColumn 更改为 "trainingplan",我会收到错误消息,指出有两个相同的列。如果我从反向列中删除 "trainingplan",我会收到一个错误,因为外部约束需要 3 列

编辑: 从评论中尝试一些东西。我试过 OneToMany/ManyToOne:

@Id
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "trainingplan", nullable = false)
public Training getTraining() {
    return training.get();
}

@OneToMany(fetch = FetchType.EAGER, mappedBy = "training")
public List<TrainingplanExercise> getExercises() {
    return exercises;
}

如果我现在尝试将训练保存到数据库,它会起作用。 假设我想从数据库中获取一个 Trainingplan,并添加新的 TrainingplanExercises。我会使用这个代码:

Exercise ex = (Exercise) db.getAll(Exercise.class).get(1);


Training t = (Training) db.getAll(Training.class).get(0);


TrainingplanExercise te = new TrainingplanExercise(ex, t, "asdf", ex.getParameters().get(0));
TrainingplanExercise te1 = new TrainingplanExercise(ex, t, "asdf", ex.getParameters().get(1));
TrainingplanExercise te2 = new TrainingplanExercise(ex, t, "asdf", ex.getParameters().get(2));
TrainingplanExercise te3 = new TrainingplanExercise(ex, t, "asdf", ex.getParameters().get(3));



t.getExercises().clear();
t.getExercises().add(te);
t.getExercises().add(te1);
t.getExercises().add(te2);
t.getExercises().add(te3);

db.updateObj(t);

我得到这个异常:

Exception in thread "main" org.hibernate.exception.LockTimeoutException: could not execute statement
    at org.hibernate.dialect.MySQLDialect.convert(MySQLDialect.java:447)
    at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:49)
    at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:126)
    at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:112)
    at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:211)
    at org.hibernate.engine.jdbc.batch.internal.NonBatchingBatch.addToBatch(NonBatchingBatch.java:62)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3124)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3581)
    at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:104)
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:465)
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:351)
    at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:350)
    at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:56)
    at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1258)
    at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:425)
    at org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.beforeTransactionCommit(JdbcTransaction.java:101)
    at org.hibernate.engine.transaction.spi.AbstractTransactionImpl.commit(AbstractTransactionImpl.java:177)
    at db.Database.updateObj(Database.java:100)
    at db.Database.main(Database.java:171)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: java.sql.SQLException: Lock wait timeout exceeded; try restarting transaction
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:998)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3835)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3771)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2435)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2582)
    at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2535)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1911)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2145)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2081)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2066)
    at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:208)
    ... 19 more

您是否尝试过使用多对一映射来代替,因为无论如何您都拥有外键。然后你可以尝试类似的东西:

@Id
@ManyToOne( cascade = {CascadeType.PERSIST}, targetEntity=Trainingplan.class )
@JoinColumn(name = "trainingplan")
public Training getTraining() {}

好的,看。您遇到的是设计问题,而不是一般问题。首先,据我了解,您想制作一组独特的 TrainingplanExercise's。为此,你有这个 Entity:

@Entity
public class TrainingplanExercise implements Serializable {
    @EmbeddedId private TrainingplanExerciseId trainingplanExerciseId;

    public TrainingplanExercise() {}        
    public TrainingplanExercise(TrainingplanExerciseId trainingplanExerciseId) {
        this.trainingplanExerciseId = trainingplanExerciseId;
    }
    ... other fields ...   
}

上面的Entity和你原来的Entity的区别是我把ID变成了EmbeddableId。为了确保只有独特的练习被放入 TrainingplanExercise's,你有一个 compositeKey 被定义为一个单独的 class:

@Embeddable
public class TrainingplanExerciseId implements Serializable {
    private String exercise;
    private String parameter;

    public TrainingplanExerciseId() {}
    public TrainingplanExerciseId(String exercise, String parameter) {
        this.exercise = exercise;
        this.parameter = parameter;
    }

    ... getters, setters, hashCode, and equals
}

在这里,我制作了class Embeddable,以便它可以用作ID。您尝试声明 compositeKey 的方式没有任何意义;您试图将 TrainingplanExercise Entity 中的每个字段声明为 ID,但您只能有一个 ID

model 的不同之处在于 TrainingplanExerciseId compositeKey 不包含对 TrainingPlan 的引用。如果您尝试获取使用任何特定 TrainingplanExerciseTrainingPlan's 列表,那么您将需要一个 Bidirectional instead of a Unidirectional relationship,但这是一个不同的问题。否则,我不知道你为什么要从一个TrainingplanExercise引用回一个TrainingPlan。此外,您将对 TrainingPlan 的引用放入 TrainingplanExerciseId compositeKey,这将需要对 TrainingPlan 进行序列化,这实际上不能用作唯一 ID .

现在您可以将个人练习放入 table:

public TrainingplanExercise createExercise(String exercise, String parameter) {
    TrainingplanExercise trainingplanExercise = new TrainingplanExercise(new TrainingplanExerciseId(exercise, parameter));
    em.persist( trainingplanExercise );
    return trainingplanExercise;
}

在那之后,你想要使用可能的 TrainingplanExercise's 的任意数量的 TrainingPlan's,你用这个 Entity:

@Entity
public class TrainingPlan implements Serializable {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    @ManyToMany(fetch=FetchType.EAGER)
    private List<TrainingplanExercise> trainingplanExercises = new ArrayList<TrainingplanExercise>();

    ... getters, setters, 
}

您有 ManyToMany 关系,因为 TrainingPlan 引用许多 TrainingplanExercise's,而 TrainingplanExercise 被许多 TrainingPlan's 使用。除了 ManyToMany 之外,您不需要任何特殊注释,JPA 提供程序将创建一个 link table,将每个 Entity 中的键放入一行,如下所示:

create table TrainingPlan_TrainingplanExercise (
    TrainingPlan_id bigint not null,
    trainingplanExercises_exercise varchar(255) not null,
    trainingplanExercises_parameter varchar(255) not null
);

如果您将其声明为 OneToMany 关系,则 JPA 提供者将在 link table 上添加一个额外的 constraint 以确保 TrainingplanExercise不能链接到多个 TrainingPlan,因此您不希望这样。举例来说,这就是约束的样子。

alter table TrainingPlan_TrainingplanExercise 
    add constraint UK_t0ku26ydvjkrme5ycrnlechgi  unique (trainingplanExercises_exercise, trainingplanExercises_parameter);

创建和更新 TrainingPlans 很简单:

public TrainingPlan createTrainingPlan() {
    TrainingPlan trainingPlan = new TrainingPlan();
    em.persist(trainingPlan);
    return trainingPlan;
}
public TrainingPlan updateTrainingPlan(TrainingPlan trainingPlan) {
    return em.merge(trainingPlan);
}

现在,您可以创建TrainingplanExercisesTrainingPlans,并将练习添加到训练计划中并进行更新。

TrainingplanExercise squats20 = trainingService.createExercise("Squats", "20");
TrainingplanExercise lifts10 = trainingService.createExercise("Lifts", "10");
TrainingplanExercise crunches50 = trainingService.createExercise("Crunches", "50");

TrainingPlan trainingPlan = trainingService.createTrainingPlan();
trainingPlan.getTrainingplanExercises().add( squats20 );
trainingPlan.getTrainingplanExercises().add( lifts10 );
trainingService.updateTrainingPlan(trainingPlan);

trainingPlan = trainingService.createTrainingPlan();
trainingPlan.getTrainingplanExercises().add( lifts10 );
trainingPlan.getTrainingplanExercises().add( crunches50 );
trainingService.updateTrainingPlan(trainingPlan);

另请注意,您的应用程序面临着确保只有唯一 TrainingplanExercises 由用户创建的挑战。如果尝试创建具有重复 exerciseparameterTrainingplanExercise,您将收到 Unique index or primary key violation 异常并且事务将被回滚。

编辑:为了阅读 TrainingPlans,可以使用类似这样的内容:

public List<TrainingPlan> listTrainingPlans() {
    CriteriaQuery<TrainingPlan> criteria = em.getCriteriaBuilder().createQuery(TrainingPlan.class);
    criteria.select(criteria.from(TrainingPlan.class));
    List<TrainingPlan> trainingPlans = em.createQuery(criteria).getResultList();
    return trainingPlans;
}

请注意,由于 List<TrainingplanExercise> trainingplanExercises 设置为 FetchType.EAGER,此特定查询将拉入整个数据库。 FetchType.EAGER 可能不是阅读单个 TrainingPlan 的问题,但如果您只想要 TrainingPlan's 的列表而不获取所有详细信息,那么您需要弄清楚如何FetchType.LAZY应该实施。