how to map Many to Many with composite key

爷,独闯天下 提交于 2019-12-05 18:47:00

Okay, look. What you have is a design problem, not really a general problem. First, as I understand it, you want to make a set of unique TrainingplanExercise's. For that, you have this Entity:

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

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

The difference between the above Entity and your original Entity is that I have made the ID an EmbeddableId. In order to insure that only unique exercises are put into the TrainingplanExercise's, you have a compositeKey that was defined as a separate 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
}

Here, I have made the class Embeddable so that it can be used as an ID. The way you were trying to declare a compositeKey didn't make any sense; you were trying to declare each individual field in the TrainingplanExercise Entity as an ID, but you can only have one ID.

What is different in this model is that the TrainingplanExerciseId compositeKey does not include a reference back to a TrainingPlan. If you are trying to get a list of TrainingPlan's that use any specific TrainingplanExercise, then you would need a Bidirectional instead of a Unidirectional relationship, but that's a different issue. Otherwise, I don't know why you want to refer back to a TrainingPlan from a TrainingplanExercise. Further, you were putting a reference to the TrainingPlan into the TrainingplanExerciseId compositeKey, which would require the TrainingPlan to be serialized, which really wouldn't work as a unique Id.

Now you can put individual exercises into the table:

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

After that, you want to have any number of TrainingPlan's that use the possible TrainingplanExercise's, which you do with this 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, 
}

You have a ManyToMany relationship because a TrainingPlan refers to many TrainingplanExercise's and a TrainingplanExercise is used by many TrainingPlan's. You don't need any special annotation besides ManyToMany, the JPA provider will create a link table, putting the key from each Entity into a row, like this:

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

If you declare it as a OneToMany relationship, then the JPA provider will put an additional constraint on the link table insuring that a TrainingplanExercise cannot be linked to more than one TrainingPlan, so you don't want that. Just for example's sake, this is what the constraint would look like.

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

Creating and updating TrainingPlans is straight forward:

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

Now, you can create TrainingplanExercises and TrainingPlans, and add the exercises to the training plans and update them.

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

Also note that your application has the challenge of insuring that only unique TrainingplanExercises are created by users. If a TrainingplanExercise with a duplicate exercise and parameter is attempted to be created you will get a Unique index or primary key violation exception and the transaction will be rolled back.

EDIT: For reading the TrainingPlans, something like this can be used:

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

Note that since the List<TrainingplanExercise> trainingplanExercises is set to FetchType.EAGER this particular query will pull in the entire database. FetchType.EAGER probably isn't a problem for reading a single TrainingPlan, but if you only wanted a list of the TrainingPlan's without getting all of the details, then you would need to work out how FetchType.LAZY should be implemented.

Did you tried using many-to-one mapping instead because it's what you have with a foreign key anyway. You could then try something like:

@Id
@ManyToOne( cascade = {CascadeType.PERSIST}, targetEntity=Trainingplan.class )
@JoinColumn(name = "trainingplan")
public Training getTraining() {}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!