spring data rest with composite primary key
Asked Answered
P

3

11

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?

Polygamous answered 3/5, 2017 at 14:42 Comment(6)
Did you try this: how to expose embedded IDMechanotherapy
The answer to this question may help you.Cyclothymia
override equals and hashcodeDoubles
hashcode and equals has been overrided, I didn't paste here to show code more clear.Polygamous
implement a converter can work, but I have to create so many converters for each pk class, that's not a good idea.Polygamous
Similar question asked here <br> #43187324Halfbaked
P
-4

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.
Polygamous answered 7/5, 2017 at 14:15 Comment(3)
Ther is a much cleaner way to access spring data repositories by entity type - you could have a look at org.springframework.data.repository.support.Repositories - this class allows you to access a repository by Id class - and also do introspection of the EntityInformation.Maiamaiah
Is there any reference for this? I google it and can't find something related.Polygamous
My purpose is to create auto simple crud rest api from just entity define. And other complex method I will implement with @JpaRepository. "org.springframework.data.repository.support.Repositories" can solve?Polygamous
M
8

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://mcmap.net/q/1159161/-spring-data-rest-idclass-not-recognized

Maiamaiah answered 3/5, 2017 at 20:25 Comment(3)
The BackendIdConverter solution need to hard code entity class (in your url he use Customer class in BackendIdConverter). So when I have many entity I have to write all entity hard code in BackendIdConverter. That's waste of time and not reusable.Polygamous
@kidfruit, of course, you can implement a more generic BackendIdConverter that works for multiple Entity classes. It is just a sample to illustrate the approach. Such a generic approach would heavily depend on your entity classes and Id classes. So it is very hard to show that here.Maiamaiah
github link dont seems to be present any more. Any other example ?Noteworthy
B
0

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

Brainwash answered 28/9, 2022 at 11:50 Comment(0)
P
-4

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.
Polygamous answered 7/5, 2017 at 14:15 Comment(3)
Ther is a much cleaner way to access spring data repositories by entity type - you could have a look at org.springframework.data.repository.support.Repositories - this class allows you to access a repository by Id class - and also do introspection of the EntityInformation.Maiamaiah
Is there any reference for this? I google it and can't find something related.Polygamous
My purpose is to create auto simple crud rest api from just entity define. And other complex method I will implement with @JpaRepository. "org.springframework.data.repository.support.Repositories" can solve?Polygamous

© 2022 - 2024 — McMap. All rights reserved.