简体   繁体   中英

How to ignore field from Spring Data JPA projection mapping

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

The response looks like this

 {
    "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();
}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM