kidfruit
kidfruit

Reputation: 394

spring data rest with composite primary key

I use spring data rest for crud. But when the entity has composite primary keys, I dont know how to to get an entity by giving the primary key.

River class:

@Entity
public class River {
    private RiverPK id;
    private Double length;
    private Timestamp date;
    private String comment;


    @Basic
    @Column(name = "length")
    public Double getLength() {
        return length;
    }

    public void setLength(Double length) {
        this.length = length;
    }

    @Basic
    @Column(name = "date")
    public Timestamp getDate() {
        return date;
    }

    public void setDate(Timestamp date) {
        this.date = date;
    }

    @Basic
    @Column(name = "comment")
    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    @Id
    public RiverPK getId() {
        return id;
    }

    public void setId(RiverPK id) {
        this.id = id;
    }
}

RiverPK class:

@Embeddable
public class RiverPK implements Serializable {
    private String name;
    private int upcode;
    private int downcode;

    @Column(name = "name")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Column(name = "upcode")
    public int getUpcode() {
        return upcode;
    }

    public void setUpcode(int upcode) {
        this.upcode = upcode;
    }

    @Column(name = "downcode")
    public int getDowncode() {
        return downcode;
    }

    public void setDowncode(int downcode) {
        this.downcode = downcode;
    } 

}

RiverDAO class:

@RepositoryRestResource(path = "river")
public interface RiverDAO extends JpaRepository<River, RiverPK> {
}

Then I can get river data by call get http://localhost:8080/river/, and also create new entity to db by call post http://localhost:8080/river/ {river json}

river json is:

id": {

    "name": "1",
    "upcode": 2,
    "downcode": 3

},
"length": 4.4,
"date": 1493740800000,
"comment": "6"
}

In spring data rest doc, it should be able to call get localhost:8080/river/1 (the primary key) to get the entity which primary key is 1. This can work when the entity has only one primary key. But my entity river has composite primary keys as RiverPK. If I call get localhost:8080/river/{name='1',upcode=2,downcode=3}, it returns a error "No converter found capable of converting from type [java.lang.String] to type [com.example.db.entity.RiverPK]", I means spring use {name='1',upcode=2,downcode=3} as a String, but not RiverPK type.

The question is how to call get\put\delete with composite primary keys as other normal entity?

Upvotes: 11

Views: 21360

Answers (3)

im-pratham
im-pratham

Reputation: 188

The github link posted by @mathias-dpunkt is no longer reachable, so pasting here my solution to help the community.

package com.pratham.persistence.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.istack.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

import javax.persistence.EmbeddedId;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Customization of how composite ids are exposed in URIs.
 * The implementation will convert the Ids marked with {@link EmbeddedId} to base64 encoded json
 * in order to expose them properly within URI.
 *
 * @author im-pratham
 */
@Component
@RequiredArgsConstructor
public class EmbeddedBackendIdConverter implements BackendIdConverter {
    private final ObjectMapper objectMapper;

    @Override
    public Serializable fromRequestId(String id, Class<?> entityType) {
        return getFieldWithEmbeddedAnnotation(entityType)
                .map(Field::getType)
                .map(ret -> {
                    try {
                        String decodedId = new String(Base64.getUrlDecoder().decode(id));
                        return (Serializable) objectMapper.readValue(decodedId, (Class) ret);
                    } catch (JsonProcessingException ignored) {
                        return null;
                    }
                })
                .orElse(id);
    }

    @Override
    public String toRequestId(Serializable id, Class<?> entityType) {
        try {
            String json = objectMapper.writeValueAsString(id);
            return Base64.getUrlEncoder().encodeToString(json.getBytes(UTF_8));
        } catch (JsonProcessingException ignored) {
            return id.toString();
        }
    }

    @Override
    public boolean supports(@NonNull Class<?> entity) {
        return isEmbeddedIdAnnotationPresent(entity);
    }

    private boolean isEmbeddedIdAnnotationPresent(Class<?> entity) {
        return getFieldWithEmbeddedAnnotation(entity)
                .isPresent();
    }

    @NotNull
    private static Optional<Field> getFieldWithEmbeddedAnnotation(Class<?> entity) {
        return Arrays.stream(entity.getDeclaredFields())
                .filter(method -> method.isAnnotationPresent(EmbeddedId.class))
                .findFirst();
    }
}

Upvotes: 0

Mathias Dpunkt
Mathias Dpunkt

Reputation: 12184

There is a jira issue you can look at: https://jira.spring.io/browse/DATAREST-598

This comment might be especially interesting for you

https://jira.spring.io/browse/DATAREST-598?focusedCommentId=117740&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-117740

There you find also a github link to a sample project. It uses a BackendIdConverter to convert the composite key to string and back. So the trick is to convert your composite id to a string that can be used as the path segment.

This answer might also be interesting for you https://stackoverflow.com/a/31830586/5371736

Upvotes: 8

kidfruit
kidfruit

Reputation: 394

After learn from Customizing HATEOAS link generation for entities with composite ids, I found a much more generic solution.

First, create a SpringUtil to get bean from spring.

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtil.applicationContext == null) {
            SpringUtil.applicationContext = applicationContext;
        }
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }

    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }

    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }
}

Then, implement BackendIdConverter.

import com.alibaba.fastjson.JSON;
import com.example.SpringUtil;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.stereotype.Component;

import javax.persistence.EmbeddedId;
import javax.persistence.Id;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.net.URLEncoder;

@Component
public class CustomBackendIdConverter implements BackendIdConverter {

    @Override
    public boolean supports(Class<?> delimiter) {
        return true;
    }

    @Override
    public Serializable fromRequestId(String id, Class<?> entityType) {
        if (id == null) {
            return null;
        }

        //first decode url string
        if (!id.contains(" ") && id.toUpperCase().contains("%7B")) {
            try {
                id = URLDecoder.decode(id, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        //deserialize json string to ID object
        Object idObject = null;
        for (Method method : entityType.getDeclaredMethods()) {
            if (method.isAnnotationPresent(Id.class) || method.isAnnotationPresent(EmbeddedId.class)) {
                idObject = JSON.parseObject(id, method.getGenericReturnType());
                break;
            }
        }

        //get dao class from spring
        Object daoClass = null;
        try {
            daoClass = SpringUtil.getBean(Class.forName("com.example.db.dao." + entityType.getSimpleName() + "DAO"));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        //get the entity with given primary key
        JpaRepository simpleJpaRepository = (JpaRepository) daoClass;
        Object entity = simpleJpaRepository.findOne((Serializable) idObject);
        return (Serializable) entity;

    }

    @Override
    public String toRequestId(Serializable id, Class<?> entityType) {
        if (id == null) {
            return null;
        }

        String jsonString = JSON.toJSONString(id);

        String encodedString = "";
        try {
            encodedString = URLEncoder.encode(jsonString, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return encodedString;
    }
}

After that. you can do what you want.

There is a sample below.

  • If the entity has single property pk, you can use localhost:8080/demo/1 as normal. According to my code, suppose the pk has annotation "@Id".
  • If the entity has composed pk, suppose the pk is demoId type, and has annotation "@EmbeddedId", you can use localhost:8080/demo/{demoId json} to get/put/delete. And your self link will be the same.

Upvotes: -4

Related Questions