使用JPA实现分层数据结构(固定深度)

时间:2014-02-10 11:28:48

标签: java hibernate postgresql jpa orm

我有一个固定深度为4的分层数据结构。为了更好地理解,我们假设以下(只是一个例子):

  • “根”级别称为 countries
  • 每个国家/地区都包含任意数量的
  • 每个州都会计算任意数量的
  • 每个县都包含任意数量的 cities

因此,各级之间始终存在1-N关系。

一个非常重要的用例 (给定一个国家的ID)是一次加载一个国家/地区的整个“内容”,对性能的影响最小数据库。

在第一个天真的方法中,我在Java中创建了4个授权类,其中实体“Country”包含“State”类型的列表,实体“State”包含“County”类型的列表,依此类推。 .. 但是JPA之后创建的当然不是4个表,而是7个(4个实体+ 3个用于1-N之间的连接)。我不知道这是否是一个很好的解决方案,因为很多人都在加入。

我还试图将子类型映射到它们的父类型(一个城市属于一个县,一个县属于一个州,一个州属于一个国家)。这导致4个表,但是从应用程序的角度来看,一次检索所有数据变得更加困难。如果我没错,我需要4个不同的请求,而不是一个。

我怎么能解决这个问题?有没有办法将简单的表格布局(有四个表,而不是七个)与易于使用的实体类(父类型应该知道它的子节点)相结合? 如果没有,你怎么会意识到这一点?

我正在使用JPA和Hibernate以及PostgreSQL。

10 个答案:

答案 0 :(得分:3)

您可以使用@JoinColumn注释而不是我怀疑您正在使用的@JoinTable注释来避免使用3个额外的映射表。

所以例如,

<强> COUNTRY

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy="country")
private List<State> stateList;

<强> STATE

@ManyToOne
@JoinColumn(name="country_id")
private Country country

数据库表如下:

<强>国家

country_id => primary key

<强>国家

state_id => primary key
country_id => foriegn key

这样就可以避免所有4个实体之间的映射表。

答案 1 :(得分:2)

您已经有了解决方案:四个表是可行的方式,具有双向关系(在每个关系的非拥有方使用mappedBy属性)。如果关系为EAGER - 获取,则会自动加载所有实体。如果您想使用LAZY抓取,您可以尝试使用命名查询来加载加载了所有关系的实体:

SELECT DISTINCT c FROM Country c LEFT JOIN FETCH c.states s LEFT JOIN FETCH s.counties co...

答案 2 :(得分:2)

使用双向映射非常简单。浏览该链接

How to delete Child or Parent objects from Relationship?

进行如下修改

国家/地区实体:

------

@OneToMany(mappedBy="Country ",cascade = CascadeType.ALL)
private List<States > states;

@OneToMany(mappedBy="Country ",cascade = CascadeType.ALL)
private List<Counties> counties;

@OneToMany(mappedBy="Country ",cascade = CascadeType.ALL)
private List<Cities> cities;
-------
setters & getters

国家实体:

-----
@ManyToOne(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@JoinColumn(name="countryId")      
private Country country ;
-----

县实体:

--------
 @ManyToOne(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
 @JoinColumn(name="countryId")      
 private Country country ;
 -------

城市实体:


@ManyToOne(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@JoinColumn(name="countryId")      
private Country country ;
---------

编译完所有实体后进行插入。只有4将使用Country对象id创建和读取您的数据。

答案 3 :(得分:2)

您可以使用JPQL轻松实现此目的:

SELECT DISTINCT country
FROM Country country
JOIN FETCH country.states states
JOIN FETCH states.counties counties
JOIN FETCH counties.cities cities
WHERE country.id = :countryId

fetchType = FetchType.EAGER / @OneToMany上使用@ManyToOne(相信默认情况下已经EAGER)将获得类似的结果。

答案 4 :(得分:1)

您是否尝试使用第二种方法明确地声明关系的获取类型以急切(默认为懒惰,这就是您必须进行四次查询的原因)。

E.g。

@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@JoinColumn ...
private ...;

见这里:http://www.concretepage.com/hibernate/fetch_hibernate_annotation

答案 5 :(得分:1)

以下是您的实体的外观:(如果您愿意,也可以使用EAGER Loading代替LAZY)

实体:国家/地区

@Id
private Integer id;

@OneToMany(orphanRemoval=true fetch=FetchType.LAZY)
@JoinColumn(name="COUNTRY_ID") 
private List<State> stateList;

实体:国家

此表具有COUNTRY_ID,即国家/地区的外键

@Id
private Integer id;

@OneToMany(orphanRemoval=true fetch=FetchType.LAZY)
@JoinColumn(name="STATE_ID") 
private List<County> countyList;

@Column(name="COUNTRY_ID")
private Integer countryId;

实体:县

此表具有STATE_ID,即状态的外键

@Id
private Integer id;

@OneToMany(orphanRemoval=true fetch=FetchType.LAZY)
@JoinColumn(name="COUNTY_ID") 
private List<City> cityList;

@Column(name="STATE_ID")
private Integer stateId;

实体:城市

此表的COUNTY_ID是县的外键

@Id
private Integer id;

@Column(name="COUNTY_ID")
private Integer countyId;

您的JPQL将是:

Select o from Country o where o.id=10

这将选择国家实体以及下面的所有映射。

Country
  Holding List of States
    Each States Holding List of Counties
      Each Counties Holding LIst of Cities 

答案 6 :(得分:1)

对于像你这样的要求,我建议使用树状结构来维护分层位置数据。实施起来比较容易。维护并且更具可扩展性可扩展的。

为了实现树,您需要有2个表LOCATION_NODE(位置ID,位置名称,位置类型[国家,州,县,城市])&amp; LOCATION_REL(关系ID,父ID,子ID)。以下是树构思的基本实现。

public class LocationRel<T> {
    private LocationNode<T> root;

    public LocationRel(T rootData) {
        root = new LocationNode<T>();
        root.data = rootData;
        root.children = new ArrayList<LocationNode<T>>();
    }

    public static class LocationNode<T> {
        private T data;
        private LocationNode<T> parent;
        private List<LocationNode<T>> children;
    }
}

这是树的基本构建块。您可能需要添加添加,删除,遍历和构造函数的方法。但是,一旦实现,您可以自由添加任何新的位置类型,更改层次结构,添加节点,删除节点等与您的分层数据。

开箱即用。

Shishir

答案 7 :(得分:1)

如果您需要性能,我建议您对表进行反规范化并创建具有以下属性(列)的4个实体:

  1. 国家/地区:id,name
  2. 州:id,countryId,name
  3. 县:id,countryId,stateId,name
  4. 城市:id,countryId,stateId,countyId,name
  5. (映射很明显)

    然后,您将能够构建一个简单的SQL查询。 如果需要性能,则首选命名查询,因为它们是在初始化时编译的。 例如。按国家/地区选择所有城市:“SELECT id,name FROM city WHERE country_id =?”

    您甚至可能不使用@ManyToOne声明实体之间的引用,而只是声明一个简单的@Columns。 API调用很可能会接受ID(countryId,stateId),因此您最好将这些ID作为参数传递给DAO。最有可能的是,您通过sql脚本填充了一次位置表,并且不应修改数据。创建外键以保证数据完整性。

    你真的需要记忆中的树状结构吗?如果是这样,手动创建,它不是很复杂。

答案 8 :(得分:0)

在线搜索,我发现了一些关于JPQL的链接,我认为这可能有所帮助。

Link 1

Link 2

反正,

JPQL是实现此目的的最佳方法之一,请尝试此查询

SELECT DISTINCT country FROM Country country JOIN FETCH country.states states JOIN FETCH states.counties counties JOIN FETCH counties.cities cities WHERE country.id = :countryId

答案 9 :(得分:0)

如果您的关系仅指向其父级,那么有用的解决方案如下:

有记录:

@Entity
public class Country
{
    @Id
    private Long id;
}

@Entity
public class State
{
    @Id
    private Long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = "country_id", referencedColumnName = "id", nullable = false) 
    Country country;
}

@Entity
public class County
{
    @Id
    private Long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = "state_id", referencedColumnName = "id", nullable = false) 
    State state;
}

@Entity
public class City
{
    @Id
    private Long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = "county_id", referencedColumnName = "id", nullable = false) 
    County county;
}

您可以通过以下方式获取一个国家/地区的所有城市:

public interface CityRepository extends JpaRepository<City, Long>
{
    List<City> findByCounty(County county);    // county is a direct field of City

    @Query("SELECT c FROM City c WHERE c.county.state.country = ?1")
    List<City> findByCountry(Country country);
}