【问题标题】:JPA / EclipseLink Eager Fetch leaving data unpopulated (during multiple concurrent queries)JPA / EclipseLink Eager Fetch 未填充数据(在多个并发查询期间)
【发布时间】:2011-05-19 01:37:07
【问题描述】:

我们正在尝试在启动时将一些实体加载到我们的类中。我们正在加载的实体(LocationGroup 实体)与另一个实体(Location 实体)具有@ManyToMany 关系,由连接表(LOCATION_GROUP_MAP)定义。应该急切地获取这种关系。

这适用于单个线程,或者当执行 JPA 查询的方法同步时。但是,当有多个线程都异步执行 JPA 查询(都通过同一个 Singleton DAO 类)时,我们会得到位置集合数据,这些数据应该是急切获取的,在某些情况下会保留为 NULL。

我们在 Glassfish v3.0.1 中使用 EclipseLink。

我们的数据库表(在 Oracle 数据库中)如下所示:

LOCATION_GROUP
location_group_id | location_group_type
------------------+--------------------
GROUP_A           | MY_GROUP_TYPE
GROUP_B           | MY_GROUP_TYPE

LOCATION_GROUP_MAP
location_group_id | location_id
------------------+------------
GROUP_A           | LOCATION_01
GROUP_A           | LOCATION_02
GROUP_A           | ...
GROUP_B           | LOCATION_10
GROUP_B           | LOCATION_11
GROUP_B           | ...

LOCATION
location_id
-----------
LOCATION_01
LOCATION_02
...

我们的 Java 代码看起来像这样(我从 Entities 中省略了 getter/setter 和 hashCode、equals、toString - Entities 是通过 NetBeans 从 DB 生成的,然后稍作修改,所以我认为没有任何问题和他们一起):

LocationGroup.java:

@Entity
@Table(name = "LOCATION_GROUP")
@NamedQueries({
    @NamedQuery(name = "LocationGroup.findAll", query = "SELECT a FROM LocationGroup a"),
    @NamedQuery(name = "LocationGroup.findByLocationGroupId", query = "SELECT a FROM LocationGroup a WHERE a.locationGroupId = :locationGroupId"),
    @NamedQuery(name = "LocationGroup.findByLocationGroupType", query = "SELECT a FROM LocationGroup a WHERE a.locationGroupType = :locationGroupType")})
public class LocationGroup implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @Basic(optional = false)
    @Column(name = "LOCATION_GROUP_ID")
    private String locationGroupId;

    @Basic(optional = false)
    @Column(name = "LOCATION_GROUP_TYPE")
    private String locationGroupType;

    @JoinTable(name = "LOCATION_GROUP_MAP",
        joinColumns = { @JoinColumn(name = "LOCATION_GROUP_ID", referencedColumnName = "LOCATION_GROUP_ID")},
        inverseJoinColumns = { @JoinColumn(name = "LOCATION_ID", referencedColumnName = "LOCATION_ID")})
    @ManyToMany(fetch = FetchType.EAGER)
    private Collection<Location> locationCollection;

    public LocationGroup() {
    }

    public LocationGroup(String locationGroupId) {
        this.locationGroupId = locationGroupId;
    }

    public LocationGroup(String locationGroupId, String locationGroupType) {
        this.locationGroupId = locationGroupId;
        this.locationGroupType = locationGroupType;
    }

    public enum LocationGroupType {
        MY_GROUP_TYPE("MY_GROUP_TYPE");

        private String locationGroupTypeString;

        LocationGroupType(String value) {
            this.locationGroupTypeString = value;
        }

        public String getLocationGroupTypeString() {
            return this.locationGroupTypeString;
        }
    }
}

位置.java

@Entity
@Table(name = "LOCATION")
public class Location implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @Basic(optional = false)
    @Column(name = "LOCATION_ID")
    private String locationId;

    public Location() {
    }

    public Location(String locationId) {
        this.locationId = locationId;
    }

}

LocationGroupDaoLocal.java

@Local
public interface LocationGroupDaoLocal {
    public List<LocationGroup> getLocationGroupList();
    public List<LocationGroup> getLocationGroupList(LocationGroupType groupType);
}

LocationGroupDao.java

@Singleton
@LocalBean
@Startup
@Lock(READ)
public class LocationGroupDao implements LocationGroupDaoLocal {
    @PersistenceUnit(unitName = "DataAccess-ejb")
    protected EntityManagerFactory factory;

    protected EntityManager entityManager;

    @PostConstruct
    public void setUp() {
        entityManager = factory.createEntityManager();
        entityManager.setFlushMode(FlushModeType.COMMIT);
    }

    @PreDestroy
    public void shutdown() {
        entityManager.close();
        factory.close();
    }

    @Override
    public List<LocationGroup> getLocationGroupList() {
        TypedQuery query = entityManager.createNamedQuery("LocationGroup.findAll", LocationGroup.class);
        return query.getResultList();
    }

    @Override
    public List<LocationGroup> getLocationGroupList(LocationGroupType groupType) {
        System.out.println("LOGGING-" + Thread.currentThread().getName() + ": Creating Query for groupType [" + groupType + "]");
        TypedQuery query = entityManager.createNamedQuery("LocationGroup.findByLocationGroupType", LocationGroup.class);
        query.setParameter("locationGroupType", groupType.getLocationGroupTypeString());
        System.out.println("LOGGING-" + Thread.currentThread().getName() + ": About to Execute Query for groupType [" + groupType + "]");
        List<LocationGroup> results = query.getResultList();
        System.out.println("LOGGING-" + Thread.currentThread().getName() + ": Executed Query for groupType [" + groupType + "] and got [" + results.size() + "] results");
        return results;
    }
}

Manager.java

@Singleton
@Startup
@LocalBean
public class Manager {
    @EJB private LocationGroupDaoLocal locationGroupDao;

    @PostConstruct
    public void startup() {
        System.out.println("LOGGING: Starting!");

        // Create all our threads
        Collection<GroupThread> threads = new ArrayList<GroupThread>();
        for (int i=0; i<20; i++) {
            threads.add(new GroupThread());
        }

        // Start each thread
        for (GroupThread thread : threads) {
            thread.start();
        }
    }

    private class GroupThread extends Thread {
        @Override
        public void run() {
            System.out.println("LOGGING-" + this.getName() + ": Getting LocationGroups!");
            List<LocationGroup> locationGroups = locationGroupDao.getLocationGroupList(LocationGroup.LocationGroupType.MY_GROUP_TYPE);
            for (LocationGroup locationGroup : locationGroups) {
                System.out.println("LOGGING-" + this.getName() + ": Group [" + locationGroup.getLocationGroupId() +
                        "], Found Locations: [" + locationGroup.getLocationCollection() + "]");

                try {
                    for (Location loc : locationGroup.getLocationCollection()) {
                        System.out.println("LOGGING-" + this.getName() + ": Group [" + locationGroup.getLocationGroupId()
                                + "], Found location [" + loc.getLocationId() + "]");
                    }
                } catch (NullPointerException npe) {
                    System.out.println("LOGGING-" + this.getName() + ": Group [" + locationGroup.getLocationGroupId()
                            + "], NullPointerException while looping over locations");
                }

                try {
                    System.out.println("LOGGING-" + this.getName() + ": Group [" + locationGroup.getLocationGroupId()
                        + "], Found [" + locationGroup.getLocationCollection().size() + "] Locations");
                } catch (NullPointerException npe) {
                    System.out.println("LOGGING-" + this.getName() + ": Group [" + locationGroup.getLocationGroupId()
                            + "], NullPointerException while printing Size of location collection");
                }
            }
        }
    }
}

所以我们的管理器启动,然后创建 20 个线程,所有这些线程都同时调用 Singleton LocationGroupDao,尝试加载 MY_GROUP_TYPE 类型的 LocationGroups。始终返回两个 LocationGroup。但是,当返回 LocationGroup 实体时,LocationGroup 上的 Location 集合(由 @ManyToMany 关系定义)有时为 NULL。

如果我们使 LocationGroupDao.getLocationGroupList(LocationGroupType groupType) 方法同步一切都很好(我们从未看到指示发生 NullPointerException 的输出行),同样如果您将 Manager.startup() 中的 for 循环更改为只有一个单次迭代(因此只创建/执行一个线程)。

但是,使用原样的代码,我们确实会得到带有 NullPointerException 的输出行,例如(仅过滤掉其中一个线程的行):

LOGGING-Thread-172: Getting LocationGroups!
LOGGING-Thread-172: Creating Query for groupType [MY_GROUP_TYPE]
LOGGING-Thread-172: About to Execute Query for groupType [MY_GROUP_TYPE]
LOGGING-Thread-172: Executed Query for groupType [MY_GROUP_TYPE] and got [2] results
LOGGING-Thread-172: Group [GROUP_A], Found Locations: [null]
LOGGING-Thread-172: Group [GROUP_A], NullPointerException while looping over locations
LOGGING-Thread-172: Group [GROUP_A], NullPointerException while printing Size of location collection
LOGGING-Thread-172: Group [GROUP_B], Found Locations: [null]
LOGGING-Thread-172: Group [GROUP_B], NullPointerException while looping over locations
LOGGING-Thread-172: Group [GROUP_B], NullPointerException while printing Size of location collection

但是,在同一运行期间稍后完成执行的线程没有 NullPointerExceptions:

LOGGING-Thread-168: Getting LocationGroups!
LOGGING-Thread-168: Creating Query for groupType [MY_GROUP_TYPE]
LOGGING-Thread-168: About to Execute Query for groupType [MY_GROUP_TYPE]
LOGGING-Thread-168: Executed Query for groupType [MY_GROUP_TYPE] and got [2] results
LOGGING-Thread-168: Group [GROUP_A], Found Locations: [...]
LOGGING-Thread-168: Group [GROUP_A], Found location [LOCATION_01]
LOGGING-Thread-168: Group [GROUP_A], Found location [LOCATION_02]
LOGGING-Thread-168: Group [GROUP_A], Found location [LOCATION_03]
...
LOGGING-Thread-168: Group [GROUP_A], Found [8] Locations
LOGGING-Thread-168: Group [GROUP_B], Found Locations: [...]
LOGGING-Thread-168: Group [GROUP_B], Found location [LOCATION_10]
LOGGING-Thread-168: Group [GROUP_B], Found location [LOCATION_11]
LOGGING-Thread-168: Group [GROUP_B], Found location [LOCATION_12]
...
LOGGING-Thread-168: Group [GROUP_B], Found [11] Locations

这显然是一个并发问题,但我不明白为什么要返回 LocationGroup 实体,除非它们的所有 Eagerly Fetched 相关实体都已加载。

附带说明一下,我也尝试过使用 Lazy fetching 进行此操作 - 我遇到了类似的问题,执行的前几个线程显示 Location 集合未初始化,然后在某个时候它被初始化并且所有后续线程都可以工作正如预期的那样。

【问题讨论】:

    标签: java jpa concurrency many-to-many eclipselink


    【解决方案1】:

    我认为从多个线程访问单个应用程序管理的EntityManager 无效。

    要么使其成为容器管理的事务范围:

    @PersistenceContext(unitName = "DataAccess-ejb") 
    protected EntityManager entityManager; 
    

    或为每个线程创建一个单独的EntityManager(在getLocationGroupList() 内)。

    编辑: 默认情况下,EntityManager 不是线程安全的。唯一的例外是容器管理的事务范围EntityManager,即EntityManager 通过@PersistenceContext 注入而没有scope = EXTENDED。在这种情况下,EntityManager 充当实际线程本地EntityManagers 的代理,因此它可以在多个线程中使用。

    有关详细信息,请参阅JPA Specification 的第 3.3 节。

    【讨论】:

    • 您能解释一下@PersistenceContext 是如何防止这个问题的吗?由于只注入了一个 EntityManager,我假设这意味着一次只在 EntityManager 上执行一个查询,这可以防止上面看到的问题(类似于添加同步或使方法 @Lock(WRITE))。我假设使这个过程完全并发的唯一方法是沿着拥有多个 EntityManager 实例的路线走下去?再次感谢您的帮助!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-07-30
    • 2022-01-13
    • 2014-06-13
    • 2020-12-21
    • 1970-01-01
    • 2012-06-06
    • 2019-10-08
    相关资源
    最近更新 更多