Issue
I'm building a REST API for a Q&A web application. It has communities where each community is defined by an id, name, description, members, and others. Right now, I have an endpoint for fetching the given community data /api/v1/communities/{id}
.
{
"name": "VIP",
"id": 5,
"displayName": "VIP",
"subtopics": [
"Sports"
],
"primaryTopic": "Gaming",
"about": "",
"isPublic": false,
"isPrivate": true,
"isRestricted": false
}
The response is a DTO that's mapped automatically using Spring Data Projection. Now, I want to add a new field to the DTO. The field called isMember
and it should store whether the currently authenticated user is a member of the given community.
public interface CommunityResponse {
Long getId();
String getName();
String getDisplayName();
String getAbout();
@Value("#{target.primaryTopic.getDisplayName()}")
String getPrimaryTopic();
@Value("#{target.getSubtopicsDisplayNames()}")
Set<String> getSubtopics();
@Value("#{target.isPublic()}")
@JsonProperty("isPublic")
boolean isPublic();
@Value("#{target.isPrivate()}")
@JsonProperty("isPrivate")
boolean isPrivate();
@Value("#{target.isRestricted()}")
@JsonProperty("isRestricted")
boolean isRestricted();
// boolean isMember();
}
I thought about using Class-based projection. I converted the interface into a class and added the isMember
field to the DTO and called setIsMember()
inside the service class, but I got an error that says no property 'isMember' found for type 'CommunityResponse'
As far as I understand, each field in a projection class or interface must either share the same name as a field in the entity or it should be derived from other fields using Spring’s expression language.
The problem is that isMember
is not a field from the entity and I don't know how to derive it using Spring’s expression language. The logic for initializing isMember
looks like this:
public boolean isMember(Long communityId, Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userAccountRepository.findByEmail(userDetails.getUsername()).map(user -> {
return user.getJoinedCommunities().stream().anyMatch(community -> community.getId() == communityId);
}).orElse(false);
}
Other classes
UserAccount.java
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Data
public class UserAccount {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@NotBlank(message = "Firstname is required")
private String firstname;
@NotBlank(message = "Lastname is required")
private String lastname;
@NotBlank(message = "Username is required")
private String username;
@NotBlank(message = "Email is required")
private String email;
@NotBlank(message = "Password is required")
private String hashedPassword;
private Instant creationDate;
private Boolean activated;
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "members")
private Set<Community> joinedCommunities;
}
Community.java
@Entity
@Table(name = "community")
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Community {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Enumerated(EnumType.STRING)
private CommunityType type;
@Column(unique = true)
private String name;
private String displayName;
private String about;
@Enumerated(EnumType.STRING)
private Topic primaryTopic;
@CollectionTable(name = "subtopic")
@ElementCollection(targetClass = Topic.class)
@Enumerated(EnumType.STRING)
@Column(name = "topic")
private Set<Topic> subtopicSet;
@ManyToMany
@JoinTable(name = "community_members",
joinColumns = { @JoinColumn(name = "community_id") },
inverseJoinColumns = { @JoinColumn(name = "member_id") })
private Set<UserAccount> members;
public String getDisplayName() {
return displayName;
}
public boolean isPublic() {
return type == CommunityType.PUBLIC;
}
public boolean isPrivate() {
return type == CommunityType.PRIVATE;
}
public boolean isRestricted() {
return type == CommunityType.RESTRICTED;
}
public Set<String> getSubtopicsDisplayNames() {
return getSubtopicSet().stream().map(Topic::getDisplayName).collect(Collectors.toSet());
}
}
CommunityRepository.java
public interface CommunityRepository extends JpaRepository<Community, Long> {
Optional<CommunityResponse> findCommunityById(Long id);
List<CommunityResponse> findCommunityResponseBy();
}
Solution
I thought about using Class-based projection. I converted the interface into a class and added the isMember field to the DTO
You could try to derive isMember
within JPQL query constructing class-based projection:
@Query("select new com.my.project.DTO(c.id, c.name, (:user member of c.members)) from Community c ...")
List<CommunityResponse> findCommunityResponseBy(UserAccount user);
Alternatively, in interface-based projection you could try
@Value("#{target.members.contains(#user)}")
boolean isMember;
where #user
is the argument of findBy
method.
Answered By - Sergey Tsypanov
Answer Checked By - Katrina (JavaFixing Volunteer)