I'm tryingto build an insert, update and delete forms for a many-to-many relationship with one extra attribute, but I'm having some problems. When I use a simple @ManyToMany
annotation everything works since Spring MVC and Thyemeleaf automatically take care of everything. Now, I need to have an extra attribute in the join table/entity. This attribute will be managed by the business logic and it doesn't need to be present in the view. I'm implementing my scenario creating manually the join entity rather than using the @Embedded
annotation.
What I have so far will be shown below. Note that I'm omitting the import statements and I'm reproducing a very simpler version that I actually have.
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode( onlyExplicitlyIncluded = true )
public class Race {
@Id
@GeneratedValue( strategy = GenerationType.IDENTITY )
@EqualsAndHashCode.Include
private Long id;
@NotNull
@NotEmpty
private String name;
@OneToMany( mappedBy = "race", cascade = CascadeType.ALL, orphanRemoval = true )
private Set<TruckFromRace> trucksFromRace;
}
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode( onlyExplicitlyIncluded = true )
public class Truck {
@Id
@GeneratedValue( strategy = GenerationType.IDENTITY )
@EqualsAndHashCode.Include
private Long id;
@NotNull
@Min( value = 1 )
private Integer number;
@OneToMany( mappedBy = "truck", cascade = CascadeType.ALL, orphanRemoval = true )
private Set<TruckFromRace> trucksFromRace;
}
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode( onlyExplicitlyIncluded = true )
public class TruckFromRace {
@Id
@GeneratedValue( strategy = GenerationType.IDENTITY )
@EqualsAndHashCode.Include
private Long id;
@ManyToOne
@JoinColumn( name = "truck_id" )
@ToString.Exclude
@JsonProperty( access = Access.WRITE_ONLY )
private Truck truck;
@ManyToOne
@JoinColumn( name = "race_id" )
@ToString.Exclude
@JsonProperty( access = Access.WRITE_ONLY )
private Race race;
@NotNull
private Integer currentLap = 0;
}
public interface RaceRepository extends CrudRepository<Race, Long> {
}
public interface TruckFromRaceRepository extends CrudRepository<TruckFromRace, Long> {
}
public interface TruckRepository extends CrudRepository<Truck, Long> {
}
@Controller
@RequestMapping( "/races" )
public class RaceController {
@Autowired
private RaceRepository raceRepo;
@Autowired
private TruckRepository truckRepo;
@Autowired
private TruckFromRaceRepository trucksFromRaceRepo;
@GetMapping( "/list" )
public String listar( Model model ) {
model.addAttribute( "races", raceRepo.findAll() );
return "/forms/races/list";
}
@GetMapping( "/prepareInsert" )
public String prepareInsert( Race race, Model model ) {
model.addAttribute( "trucks", truckRepo.findAll() );
return "/forms/races/insert";
}
@PostMapping( "/insert" )
public String insert( @Valid Race race,
BindingResult result,
Model model,
@RequestParam( required = false ) Long[] trucksFromRace ) {
if ( result.hasErrors() ) {
model.addAttribute( "trucks", truckRepo.findAll() );
return "/forms/races/insert";
}
raceRepo.save( race );
if ( trucksFromRace != null ) {
for ( Long truckId : trucksFromRace ) {
Truck t = truckRepo.findById( truckId ).get();
TruckFromRace tr = new TruckFromRace();
tr.setTruck( t );
tr.setRace( race );
trucksFromRaceRepo.save( tr );
}
}
return "redirect:/races/list";
}
@GetMapping( "/prepareUpdate/{id}" )
public String prepareUpdate( @PathVariable( "id" ) Long id,
Model model ) {
Race race = raceRepo.findById( id )
.orElseThrow( () -> new IllegalArgumentException( "Invalid id:" + id ) );
model.addAttribute( "trucks", truckRepo.findAll() );
model.addAttribute( "race", race );
return "/forms/races/update";
}
@PostMapping( "/update/{id}" )
public String update( @PathVariable( "id" ) Long id,
@Valid Race race,
BindingResult result,
Model model,
@RequestParam( required = false ) Long[] trucksFromRace ) {
if ( result.hasErrors() ) {
model.addAttribute( "trucks", truckRepo.findAll() );
race.setId( id );
return "/forms/races/update";
}
// remove all trucks from this race
// only works if the if above is commented
race.getTrucksFromRace().clear();
raceRepo.save( race );
// try to associate new ones
/*if ( trucksFromRace != null ) {
for ( Long truckId : trucksFromRace ) {
Truck t = truckRepo.findById( truckId ).get();
TruckFromRace tr = new TruckFromRace();
tr.setTruck( t );
tr.setRace( race );
trucksFromRaceRepo.save( tr );
}
}*/
return "redirect:/races/list";
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Races</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"/>
</head>
<body>
<h4>Races</h4>
<table>
<thead>
<tr>
<th>Name</th>
<th>Update</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<tr th:each="race : ${races}">
<td th:text="${race.name}"></td>
<td><a th:href="@{/races/prepareUpdate/{id}(id=${race.id})}">Update</a></td>
<td><a th:href="@{/races/prepareDelete/{id}(id=${race.id})}">Delete</a></td>
</tr>
</tbody>
</table>
<a href="/races/prepareInsert">New Race</a>
<br/>
<a href="/index">Index</a>
</body>
</html>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>New Race</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"/>
</head>
<body>
<h4>New Race</h4>
<form action="#" th:action="@{/races/insert}" th:object="${race}" method="post">
<div>
<label for="name">Name</label>
<input type="text" th:field="*{name}" id="name" placeholder="Name"/>
<small th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></small>
</div>
<h4>Trucks</h4>
<table>
<thead>
<tr>
<th>Number</th>
<th>Will compete</th>
</tr>
</thead>
<tbody>
<tr th:each="truck : ${trucks}">
<td th:text="${truck.number}"></td>
<td>
<input class="form-check-input" th:field="*{trucksFromRace}" type="checkbox" th:value="${truck.id}"/>
</td>
</tr>
</tbody>
</table>
<input type="submit" value="Save"/>
<br/>
<a href="/races/list">Back</a>
</form>
</body>
</html>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>New Race</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"/>
</head>
<body>
<h4>Update Race</h4>
<form action="#" th:action="@{/races/update/{id}(id=${race.id})}" th:object="${race}" method="post">
<div>
<label for="name">Name</label>
<input type="text" th:field="*{name}" id="name" placeholder="Name"/>
<small th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></small>
</div>
<h4>Trucks</h4>
<table>
<thead>
<tr>
<th>Number</th>
<th>Will compete</th>
</tr>
</thead>
<tbody>
<tr th:each="truck : ${trucks}">
<td th:text="${truck.number}"></td>
<td>
<input class="form-check-input" th:field="*{trucksFromRace}" type="checkbox" th:value="${truck.id}"/>
</td>
</tr>
</tbody>
</table>
<input type="submit" value="Save"/>
<br/>
<a href="/races/list">Back</a>
</form>
</body>
</html>
My insertion is working fine, althoug I think that the controller code could be simpler, but I have two problems with the update operation. I'm omitting the delete operation because it will be based in the update operation.
But, when I try to update it, I have this erroneous result in my view:
So, I would like to know what I need to do to solve this.
update
method in the RaceController
class. What I'm experiencing is described in some comments inside the code. I'm pretty rusty with JPA... My last time doing these kinds of implementations in real scenarios was more than 10 years ago.Thanks!
Many to many link table only take care of two table in many to many , it may not handle extra attribute.
In my way the possible solution is to create/entity Which hold two tables and one attribute.
Previous: Table1 and Table2 in m2m relationship.
Solution Table 3 has one to many relationship with table 1 and table 2 , also table 3 has an other extra attribute.