Search⌘ K
AI Features

Selecting a Linked Entity View

Explore how to implement entity selection and linking in Thymeleaf and Spring Boot applications. Understand creating Team entities, managing many-to-one relationships with User as coach, and building forms using DTOs. Learn to handle selection inputs in HTML and ensure functionality with integration tests using Cypress.

We'll cover the following...

Implementation

A common requirement in an application is the ability to select an entity from a list of entities to link that entity to another entity. Let’s make it practical.

We’ll create a Team entity. Each Team has a coach. When we create a form to create or edit a Team, we will have a combobox to select a coach. That combobox will contain all users of the application.

Creating a team will look like this:

We’ll start by creating our Team entity using JPearl:

mvn jpearl:generate -Dentity=Team

By expanding on that generated code, we have our Team entity like this:

Java
package com.tamingthymeleaf.application.team;
import com.tamingthymeleaf.application.user.User;
import io.github.wimdeblauwe.jpearl.AbstractVersionedEntity;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ManyToOne;
import javax.validation.constraints.NotBlank;
@Entity
public class Team extends AbstractVersionedEntity<TeamId> {
@NotBlank
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private User coach;
/**
* Default constructor for JPA
*/
protected Team() {
}
public Team(TeamId id,
String name,
User coach) {
super(id);
this.name = name;
this.coach = coach;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User getCoach() {
return coach;
}
public void setCoach(User coach) {
this.coach = coach;
}
}

On lines 17 and 18, we’ll create a link between Team and User by using the many-to-one relationship. This allows for a single coach to coach different teams.

    @ManyToOne(fetch = FetchType.LAZY)
    private User coach; 

To support this Team entity, we need to create a database table:

MySQL
CREATE TABLE team
(
id UUID NOT NULL,
version BIGINT NOT NULL,
name VARCHAR NOT NULL,
coach_id UUID NOT NULL,
PRIMARY KEY (id)
);
ALTER TABLE team
ADD CONSTRAINT FK_team_to_user FOREIGN KEY (coach_id) REFERENCES tt_user;

We will also create a TeamService:

Java
package com.tamingthymeleaf.application.team;
import com.tamingthymeleaf.application.user.User;
import com.tamingthymeleaf.application.user.UserId;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.Optional;
public interface TeamService {
Page<TeamSummary> getTeams(Pageable pageable);
Team createTeam(String name, User coach);
Team createTeam(String name, UserId coachId);
Optional<Team> getTeam(TeamId teamId);
Team editTeam(TeamId teamId, long version, String name, UserId coachId);
void deleteTeam(TeamId teamId);
void deleteAllTeams();
}

The TeamServiceImpl will use the TeamRepository for the database interaction:

Java
package com.tamingthymeleaf.application.team;
import com.tamingthymeleaf.application.user.User;
import com.tamingthymeleaf.application.user.UserId;
import com.tamingthymeleaf.application.user.UserNotFoundException;
import com.tamingthymeleaf.application.user.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
@Transactional
public class TeamServiceImpl implements TeamService {
private static final Logger LOGGER = LoggerFactory.getLogger(TeamServiceImpl.class);
private final TeamRepository repository;
private final UserService userService;
public TeamServiceImpl(TeamRepository repository, UserService userService) {
this.repository = repository;
this.userService = userService;
}
@Override
@Transactional(readOnly = true)
public Page<TeamSummary> getTeams(Pageable pageable) {
return repository.findAllSummary(pageable);
}
@Override
public Team createTeam(String name, User coach) {
LOGGER.info("Creating team {} with coach {} ({})", name, coach.getUserName().getFullName(), coach.getId());
return repository.save(new Team(repository.nextId(), name, coach));
}
@Override
public Team createTeam(String name, UserId coachId) {
User coach = getCoach(coachId);
return createTeam(name, coach);
}
@Override
public Optional<Team> getTeam(TeamId teamId) {
return repository.findById(teamId);
}
@Override
public Team editTeam(TeamId teamId, long version, String name, UserId coachId) {
Team team = getTeam(teamId)
.orElseThrow(() -> new TeamNotFoundException(teamId));
if (team.getVersion() != version) {
throw new ObjectOptimisticLockingFailureException(User.class, team.getId().asString());
}
team.setName(name);
team.setCoach(getCoach(coachId));
return team;
}
@Override
public void deleteTeam(TeamId teamId) {
repository.deleteById(teamId);
}
@Override
public void deleteAllTeams() {
repository.deleteAll();
}
private User getCoach(UserId coachId) {
return userService.getUser(coachId)
.orElseThrow(() -> new UserNotFoundException(coachId));
}
}

The TeamRepository is a normal CrudRepository, but it ...