简体   繁体   中英

JPA2 - Getting child entities of joined entities recursively using Criteria API as Dynamic Query

I want to understand how to apply certain conditions using Criteria API instead of JPQL, particularly if Criteria can be used to recursively get child entities from Joins in the same way that JPQL can, through Join hierarchies.

UPDATE

The comments from Tiny and Chris prompted me to first be clear what i'm trying to achieve:

My example has 4 entities as per the below diagram. Enitemshas a ManyToOne relationship with Ensources. Entopics has OneToMany relationship with Enitems. Entopics has a OneToMany relationship with SegmentsNew.

在此处输入图片说明

I am building a search page where the user can find a chosen item by entering as many or as little in the search criteria. In the below example a search for "Corporate Law" should return all the items in the Corporate Law segment (even if nothing else is entered).

在此处输入图片说明

My entities:

Enitems:

@Entity
@Table(name = "enitem")

private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "itemid")
private Integer itemid;
@Size(max = 500)
@Column(name = "itemname")
private String itemname;
@Column(name = "daterec")
@Temporal(TemporalType.DATE)
private Date daterec;
@Lob
@Size(max = 65535)
@Column(name = "itemdetails")
private String itemdetails;
private String enteredby;
@OneToMany(mappedBy = "items")
private Collection<Endatamaster> endatamasterCollection;
@JoinColumn(name = "topicid", referencedColumnName = "topicid")
@ManyToOne
private Entopic topics;
@JoinColumn(name = "sourceid", referencedColumnName = "sourceid")
@ManyToOne
private Ensource source;

Entopics:

public class Entopic implements Serializable {

private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "topicid")
private Integer topicid;
@Size(max = 500)
@Column(name = "topicname")
private String topicname;
@Size(max = 500)
@Column(name = "description")
private String description;
@Column(name = "locksec")
private boolean locksec;
@JoinColumn(name = "segmentid", referencedColumnName = "SEGMENTID")
@ManyToOne
private Segmentnew segments;
@JoinColumn(name = "marketid", referencedColumnName = "marketid")
@ManyToOne
private Enmarkets markets;
@OneToMany(mappedBy = "topics")
private Collection<Enitem> enitemCollection;

Ensource:

@Entity
@Table(name = "ensource")
@NamedQueries({
@NamedQuery(name = "Ensource.findAll", query = "SELECT e FROM Ensource e")})
public class Ensource implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "sourceid")
private Integer sourceid;
@Size(max = 500)
@Column(name = "sourcename")
private String sourcename;
@Size(max = 500)
@Column(name = "description")
private String description;

@JoinColumn(name = "typeid", referencedColumnName = "typeid")
@ManyToOne
private Enitemtype entype;

Segmentsnew:

public class Segmentnew implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Basic(optional = false)
@NotNull
@Column(name = "SEGMENTID")
private Integer segmentid;
@Size(max = 255)
@Column(name = "SEGMENTNAME")
private String segmentname;
@OneToMany(mappedBy = "segments")
private Collection<Entopic> entopicCollection;
@JoinColumn(name = "sectorId", referencedColumnName = "SECTORID")
@ManyToOne
private Sectorsnew sectors;

A desired JPQL string representation of this would be:

Select e FROM Enitems e WHERE e.topics.topicid = :topicid 
AND e.source.sourceid = :sourceid; AND e.topics.segments.segmentid = :segmentid  

Search page uses JSf Primefaces:

<p:panelGrid columns="2">

                    <p:outputLabel value="Sectors: "/>
                    <p:selectOneMenu value="#{sectorBean.sectorid}"
                                     filter="true">
                        <f:selectItem itemValue="0" itemLabel="NULL"/>
                        <f:selectItems value="#{sectorBean.secList}"
                                       var="sect"
                                       itemLabel="#{sect.sectorname}"
                                       itemValue="#{sect.sectorid}"/>
                        <f:ajax listener="#{segmentsBean.segFromSec}" render="segs"/>
                    </p:selectOneMenu>

                    <p:outputLabel value="Segments: "/>
                    <p:panel id="segs">

                        <p:selectOneMenu value="#{queryBean.segmentid}"
                                         rendered="#{not empty segmentsBean.menuNormList}">

                            <f:selectItems value="#{segmentsBean.menuNormList}"
                                           var="segs"
                                           itemLabel="#{segs.segmentname}"
                                           itemValue="#{segs.segmentid}"/>
                        </p:selectOneMenu>

                    </p:panel>

                    <p:outputLabel value="Topics: "/>
                    <p:selectOneMenu value="#{queryBean.topicid}" filter="true">
                        <f:selectItem itemValue="" itemLabel="NULL"/>
                        <f:selectItems value="#{clienTopicBean.publicTopMenu}"
                                       var="pubs"
                                       itemLabel="#{pubs.topicname}"
                                       itemValue="#{pubs.topicid}"/>


                    </p:selectOneMenu>

                    <p:outputLabel value="Type: "/>
                    <p:selectOneMenu value="#{typeBean.typeid}" filter="true">
                        <f:selectItem itemValue="" itemLabel="NULL"/>
                        <f:selectItems value="#{typeBean.menuList}"
                                       var="type"
                                       itemLabel="#{type.typename}"
                                       itemValue="#{type.typeid}"/>

                        <f:ajax listener="#{sourceBean.sourceFromType}" render="src"/>
                    </p:selectOneMenu>

                    <p:outputLabel value="Sources: "/>
                    <p:panel id="src">

                        <p:selectOneMenu value="#{queryBean.sourceid}"
                                         rendered="#{not empty sourceBean.sourceListNorm}">

                            <f:selectItems value="#{sourceBean.sourceListNorm}"
                                           var="srcs"
                                           itemLabel="#{srcs.sourcename}"
                                           itemValue="#{srcs.sourceid}"/>
                        </p:selectOneMenu>

                    </p:panel>
                </p:panelGrid>

This is what I'm trying using CAPI:

public List<Enitem> superQ(Integer topicid, Integer sourceid,
        Integer segmentid) {
    em.clear();

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery cq = cb.createQuery(Enitem.class);
    Root<Enitem> rt = cq.from(Enitem.class);

    cq.select(rt);
    cq.distinct(true);

    List<Predicate> criteria = new ArrayList<Predicate>();
    Predicate whereClause = cb.conjunction();
    // if () {
    Path<Entopic> topJoin = rt.get("topics");


    if (topicid != 0) {

        ParameterExpression<Integer> p
                = cb.parameter(Integer.class, "topicid");
        whereClause = cb.and(whereClause, cb.equal(topJoin.get("topicid"), p));
    }
    if (segmentid != 0) {

        ParameterExpression<Integer> p
                = cb.parameter(Integer.class, "segmentid");
        whereClause = cb.and(whereClause, cb.equal(topJoin.get("segments").get("segmentid"), p));
    }
    //}

    if (sourceid != 0) {
        ParameterExpression<Integer> p
                = cb.parameter(Integer.class, "sourceid");
        whereClause = cb.and(whereClause, cb.equal(rt.get("source").get("sourceid"), p));
    }

//    if(whereClause.getExpressions().isEmpty()) {
//        throw new RuntimeException("no criteria");
//    }
    cq.where(whereClause);

    TypedQuery q = em.createQuery(cq);
    if (topicid != 0) {
        q.setParameter("topicid", topicid);
    }
    if (segmentid != 0) {
        q.setParameter("segmentid", segmentid);
    }

    if (sourceid != 0) {
        q.setParameter("sourceid", sourceid);
    }

    return q.getResultList();

}

It is important to state if(entityName != 0) rather than if(entityName != null), which I was previously doing and this caused the application to require all parameters to be populated by the user. This is probably because integer value of null is actually the number zero?

The generated SQL:

SELECT DISTINCT t1.itemid, t1.daterec, t1.ENTEREDBY, t1.itemdetails, t1.itemname, 
t1.sourceid, t1.topicid FROM enitem t1 LEFT OUTER JOIN entopic t0 ON (t0.topicid 
= t1.topicid) LEFT OUTER JOIN ensource t2 ON (t2.sourceid = t1.sourceid) WHERE 
(((t0.topicid = ?) AND (t2.sourceid = ?)) AND (t0.segmentid = ?))

The application is behaving dynamically in that the user only needs to enter any single value in the search page and a list is returned corresponding to that value. The problem I'm now having is that the same results are being returned if I do a second query even though I already cleared the EntityManager at the start of the method. SO the application only works if it's restarted. Do I need to refersh the entities?

Using From and join both give you objects that can be cast to the root interface allowing joins to be chained.
Try something like:

public List<Enitem> superQ(Integer topicid, Integer sourceid,
      Integer segmentid) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery cq = cb.createQuery(Enitem.class);
    Root<Enitem> rt = cq.from(Enitem.class);

    cq.select(rt);
    cq.distinct(true);

    List<Predicate> criteria = new ArrayList<Predicate>();
    Predicate whereClause = cb.conjunction();
    if ((topicid != null)||(segmentid != null)){
      Path<Entopic> topJoin =rt.get("topics");
      if(topicid != null) {
          ParameterExpression<Integer> p =
                  cb.parameter(Integer.class, "topicid");
          whereClause = cb.and(whereClause, cb.equal(topJoin.get("topicid"), p));
      }
      if(segmentid != null) {
        ParameterExpression<Integer> p =
                cb.parameter(Integer.class, "segmentid");
        whereClause = cb.and(whereClause, cb.equal(topJoin.get("segments").get("segmentid"), p));
      }
    }

    if(sourceid != null) {
      ParameterExpression<Integer> p =
               cb.parameter(Integer.class, "sourceid");
      whereClause = cb.and(whereClause, cb.equal(rt.get("source").get("sourceid"), p));
    }


    if(whereClause.getExpressions().isEmpty()) {
        throw new RuntimeException("no criteria");
    }
    cq.where(whereClause);
}

This will produce something without any joining unless parameters that require a join are specified, and will use inner joins as the provided JPQL would.

So it turns out the logic of the JPA was correct but the problem is in the web framework which means i have to adapt my JPA accordingly. JSF doesn't accept 'null' as a definition of an integer in the search page and only accepts a numerical value or just "". Changing the if statement from :

if(entity != null);

to

if(entity != 0);

resulted in the application behaving as expected. This solves the problem in this question. Thanks to Chris and Tiny for their help

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM