在 GAE 的 JPA 中以相同的方法调用多个 JpaRepository 时,id 为“”的对象由不同的对象管理器管理

Object with id "" is managed by a different Object Manager when calling several JpaRepository in the same methods in GAE's JPA

我将 Google Apps Engine (GAE) 与 JPA 的 JpaRepository 接口和 GAE DataStore 的 Data Nucleus JPA 实现一起使用:

package com.appspot.repo;

import com.appspot.model.BusStop;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by eljah32 on 10/8/2017.
 */
public interface BusStopRepository extends JpaRepository<BusStop, String> {

}

和另一个仓库

package com.appspot.repo;

import com.appspot.model.BusNode;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
 * Created by eljah32 on 10/8/2017.
 */
public interface BusNodeRepository extends JpaRepository<BusNode, String> {
    List<BusNode> findTop1ByLatitude(double latitude);
    List<BusNode> findTop1ByLongitude(double longitude);
}

然后模型实体:

package com.appspot.model;

import org.datanucleus.api.jpa.annotations.Extension;

import javax.persistence.*;

/**
 * Created by eljah32 on 10/8/2017.
 */

@Entity
public class BusStop {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true")
    public String key;

    @OneToOne
    public BusNode busNode;

    public String name;
    public String name_ru;
    public String name_tt;
    public String name_en;
}

和另一个模型

package com.appspot.model;

import org.datanucleus.api.jpa.annotations.Extension;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/**
 * Created by eljah32 on 10/8/2017.
 */

@Entity
public class BusNode {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true")
    private String id;
    @org.datanucleus.api.jpa.annotations.Index(unique = "false", name="LAT")
    public double latitude;
    @org.datanucleus.api.jpa.annotations.Index(unique = "false", name="LON")
    public double longitude;

}

然后我在控制器中以相同的方法调用 jpa repo 方法(参见 getBusStops()):

package com.appspot.controller;

import com.appspot.model.BusNode;
import com.appspot.model.BusRoute;
import com.appspot.model.BusStop;
import com.appspot.repo.BusNodeRepository;
import com.appspot.repo.BusRouteRepository;
import com.appspot.repo.BusStopRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.persistence.EntityManager;
import java.util.List;

/**
 * Created by eljah32 on 10/8/2017.
 */

@Controller
@RequestMapping("/bus")
@Transactional
public class BatchStorageController {
    private static Logger logger = LoggerFactory.getLogger(BatchStorageController.class);

    @Autowired
    BusNodeRepository busNodeRepository;

    @Autowired
    BusStopRepository busStopRepository;

    @Autowired
    BusRouteRepository busRouteRepository;


    @RequestMapping(value = "/nodes", method = RequestMethod.GET)
    public @ResponseBody
    List<BusNode> getBusRoutes() {
        BusNode busNode=new BusNode();
        busNode.latitude=50.4;
        busNode.longitude=45.5;
        BusNode busNode2=new BusNode();
        busNode2.latitude=50.5;
        busNode2.longitude=45.3;
        busNodeRepository.save(busNode);
        busNodeRepository.save(busNode2);
        return busNodeRepository.findAll();
    }

    @RequestMapping(value = "/nodes/{latitude}", method = RequestMethod.GET)
    public @ResponseBody
    List<BusNode> getByLatitude(@PathVariable("latitude") double latitude) {
        List<BusNode> busNode=busNodeRepository.findTop1ByLatitude(latitude);
        return busNode;
    }

    @Transactional()
    @RequestMapping(value = "/stops", method = RequestMethod.GET)
    public @ResponseBody
    List<BusStop> getBusStops() {
        BusNode busNode=busNodeRepository.findTop1ByLatitude(50.5).get(0);
        //BusNode busNode3=new BusNode();
        //busNode3.latitude=50.2;
        //busNode3.longitude=45.2;
        //busNodeRepository.save(busNode3);

        BusStop busStop=new BusStop();
        busStop.busNode=busNode;
        busStop.name="Idel";
        busStop.name_en="Idel";
        busStop.name_ru="Idel";
        busStop.name_tt="Idel";

        busStopRepository.save(busStop); // here the exception occurs
        return busStopRepository.findAll();
    }
}

因此,调用该方法后,我收到消息,id 为 "aglidXNyb3V0ZXNyFAsSB0J1c05vZGUYgICAgICAoAgM" 的对象由不同的对象管理器管理;嵌套异常是 javax.persistence.PersistenceException:ID 为 "aglidXNyb3V0ZXNyFAsSB0J1c05vZGUYgICAgICAoAgM" 的对象由不同的对象管理器管理 busStopRepository.save(busStop);

那么如何以相同的方法将从第一个存储库中提取一个实体与将另一个实体存储在第二个存储库中结合起来呢?我在其他 JPA 实现中从未遇到过同样的问题。

更新:

pom.xml中使用的库:

  <appengine.version>1.9.30</appengine.version>
  ...
  <!-- Spring data jpa -->
    <dependency>
        <groupId>com.google.appengine</groupId>
        <artifactId>appengine-api-1.0-sdk</artifactId>
        <version>${appengine.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-jpa</artifactId>
        <version>1.3.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>3.1.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>3.1.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>4.3.4.Final</version>
    </dependency>

    <dependency>
        <groupId>org.datanucleus</groupId>
        <artifactId>datanucleus-enhancer</artifactId>
        <version>3.1.1</version>
    </dependency>
    <dependency>
        <groupId>com.google.appengine.orm</groupId>
        <artifactId>datanucleus-appengine</artifactId>
        <version>2.1.2</version>
    </dependency>
    <dependency>
        <groupId>org.datanucleus</groupId>
        <artifactId>datanucleus-core</artifactId>
        <version>3.1.3</version>
    </dependency>
    <dependency>
        <groupId>org.datanucleus</groupId>
        <artifactId>datanucleus-api-jpa</artifactId>
        <version>3.1.3</version>
    </dependency>

更新 2.

存在当前问题的项目可以在https://github.com/Eljah/busroutes-gae/tree/0.1(已标记)

上找到

尽管我没有了解 Data Nucleus 的内部结构和 Google 的 JPA 实现,但为什么会出现异常,我在上面说过,我已经在全球范围内解决了我面临的问题。如果您遇到相同的异常,则很可能您正在尝试解决在同一事务中读取许多实体并更新少量实体的问题(因为不是事务更新因上述异常而失败)。

问题在体系结构上得到解决:1) 您应该在服务层中创建单独的方法来读取实体或获取计数,而不是使它们成为事务性的 2) 在服务层中创建单独的方法来获取非事务性方法的结果作为参数并更新实体;使这些方法具有事务性 3) 在控制器层或在控制器下方但在服务上方的自定义层中分别调用这些方法!

我的错误是我认为 Spring 的 @Transactional 注释是有意义的,即使从另一个没有 @Transactionl 的方法调用带有 @Transactional 的方法也是如此。这是错误的:由于注释的方面性质,它们只有在从 class 对象外部调用方法时才有意义。因此,在我的示例中,整个调用都在唯一的事务中(实体异常太多)或在 none 事务中(因此获取具有 id ... 的对象由不同的对象管理器管理)。因此,将非事务行为和事务行为分离到不同的方法并从外部调用它们对我很有帮助。