4

For the life of me, I can't figure out how to construct this JPA query.

I need to find TransactionLogs which have not been transmitted under a given SyncSendingConfig, ordered by ID.

Researching it on SO, I figure it should be possible in SQL to do an outer join where the IDs are null for the one side, as in this diagram:

enter image description here

Here's the Entities I have to work with.

@Entity
public class SyncSendingConfig {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private long id;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "sendingConfig")
    private Set<SyncJob> sendJobs = new HashSet<>();
}

@Entity
public class SyncJob {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "sending_config_id")
    private SyncSendingConfig sendingConfig;

    @ManyToMany(cascade = { CascadeType.ALL })
    @JoinTable(
        name = "SyncJob_TransactionLog", 
        joinColumns = { @JoinColumn(name = "sync_job_id") }, 
        inverseJoinColumns = { @JoinColumn(name = "transaction_log_id") }
    )
    private Set<TransactionLog> transmitted = new HashSet<>();
}

@Entity
public class TransactionLog {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private long id;

    @ManyToMany(mappedBy = "transmitted")
    private Set<SyncJob> syncJobs = new HashSet<>();
}

And the DAO I'm trying to write:

public interface SyncSendingConfigDao extends JpaRepository<SyncSendingConfig, Long> {

    // TODO: This is the query I'm trying to get to work
    /** Returns those transactions that were never sent for the given SyncSenderConfig, ordered by ID */
    @Query("SELECT tl FROM SyncJob sj "+
        "JOIN SyncSendingConfig ssc ON sj.sendingConfig = ssc.id AND ssc.id= :sendingConfigId "+
        "RIGHT JOIN TransactionLog tl on tl.syncJobs = sj "+
        "WHERE sj.id is null"
    )
    Stream<TransactionLog> findTransactionsNotSentForSyncSendingConfigId(@Param("sendingConfigId") long sendingConfigId);

    // If this part is relevant, this join shows how I can get only those SyncJobs which are related to the SyncSendingConfig of interest
    @Query("SELECT sj FROM SyncJob sj JOIN SyncSendingConfig ssc ON sj.sendingConfig = ssc.id WHERE ssc.id= :sendingConfigId ")
    @QueryHints(value = @QueryHint(name = org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE, value = "500"))
    Stream<SyncJob> findJobs(@Param("sendingConfigId") long sendingConfigId);

}

The query above on the DAO shows what I'm attempting to do. I'm really unsure of how to translate SQL to JPQL... especially on the join conditions and order.

Update:

Here's the exact SQL query which I'm trying to translate. It matches all the relationships defined by hibernate in the classes above.

select tl.* 
from sync_job sj 
  join sync_sending_config ssc 
    on ssc.id = sj.sending_config_id and ssc.id=2 
  join sync_job_transaction_log sjtl 
    on sjtl.sync_job_id = sj.id 
  RIGHT JOIN transaction_log tl 
    on tl.id = sjtl.transaction_log_id 
  where sjtl.sync_job_id is null

When this query is run directly, it returns the exact results being sought.

If anyone can offer help, I'd greatly appreciate it. I've been running against a wall trying to figure the JPQL syntax out.

Thanks

Update 2

After working with '@S B', it appears that JPQL doesn't support a right join. Short of finding out how to write this in JPQL with a left join (if possible), I went with a native query:

@Query(value = "select tl.* from sync_job sj "+
                "join sync_sending_config ssc on ssc.id = sj.sending_config_id and ssc.id = :sendingConfigId "+
                "join sync_job_transaction_log sjtl on sjtl.sync_job_id = sj.id "+
                "RIGHT JOIN transaction_log tl on tl.id = sjtl.transaction_log_id "+
                "where sjtl.sync_job_id is null", 
                nativeQuery = true)
@QueryHints(value = @QueryHint(name = org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE, value = "500"))
Stream<TransactionLog> findTransactionsNotSentForSyncSendingConfigId(@Param("sendingConfigId") long sendingConfigId);
Cuga
  • 17,668
  • 31
  • 111
  • 166
  • What is the other side of join column ? T1 left join config on config.transmitted = ? – S B Jan 26 '20 at 14:32
  • My thought is TransactionLog TL can be joined with SyncJob "SJ" according to the TL.id and SJ.id in the Join Table "SyncJob_TransactionLog". Then restrict the results where the only SJ considered is where SJ.id = SyncSendingConfig.id where SyncSendConfig = the id parameter. Then restrict the results where SJ.id is null. – Cuga Jan 27 '20 at 11:59
  • Try it out! Let's see the results. Basically, joining between two tables should be atleast between one related column from each side – S B Jan 27 '20 at 12:08
  • I've been trying different hacks at the query to get it working... I'll update the question with the stack trace. But I'm not even sure which order to join the tables, or what the syntax should look like, being new to JPQL. – Cuga Jan 27 '20 at 12:19
  • Just a thought based on the edit, if you can get the list of only those SyncJobs which are related to the SyncSendingConfig of interest, then you can exclude such from the TransactionLog's syncJobs ? Something like - SELECT tl from TransactionLog tl where t1.syncJobs not in (syncJobs selected above) – S B Jan 27 '20 at 12:37
  • Tried a couple variations of that: `SELECT tl from TransactionLog tl where t1.syncJobs not in (SELECT sj FROM SyncJob sj JOIN SyncSendingConfig ssc ON sj.sendingConfig = ssc.id WHERE ssc.id= :sendingConfigId )` and `SELECT tl from TransactionLog tl where t1.transmitted not in (SELECT sj FROM SyncJob sj JOIN SyncSendingConfig ssc ON sj.sendingConfig = ssc.id WHERE ssc.id= :sendingConfigId )` and get invalid path syntax errors. I feel like it must be possible to accomplish via the proper order of left or right joins... – Cuga Jan 27 '20 at 14:11
  • Let's break it step by step and write JPQLs for each step and then merge. 1. Select sj FROM SyncJob sj join sj.sendingConfig ssc where ssc.id = 2. select t1 from TransactionLog t1 join t1.syncJobs sj where sj.id not in (ids selected from 1 above) Some good references here -> https://en.wikibooks.org/wiki/Java_Persistence/JPQL – S B Jan 27 '20 at 14:43

1 Answers1

0

Assuming the below dummy data setup:

  1. Transaction Log IDs: 1, 2, 3, 4
  2. SyncSendingConfig IDs: 1, 2
  3. Sync Jobs:
    • ID 1, SyncSendingConfigID 1
    • ID 2, SyncSendingConfigID 1
    • ID 3, SyncSendingConfigID 2
    • ID 4, SyncSendingConfigID 2
  4. sync_job_transaction_log
    • SyncJobId 1, TransactionLogId 1
    • SyncJobId 1, TransactionLogId 2
    • SyncJobId 2, TransactionLogId 1
    • SyncJobId 2, TransactionLogId 2

TransactionLogs 1 and 2 are transmitted under SyncSendingConfig ID 1 as per the mapping in sync_job_transaction_log table. Therefore, TransactionLogs not transmitted under SyncSendingConfig ID 1 would be 3 and 4. So, in order to find TransactionLogs which have not been transmitted under a given SyncSendingConfig, corresponding JPQL is -

@Query("select t from TransactionLog t where t not in (" +
        "select t1 from TransactionLog t1 join t1.syncJobs tsj where tsj in "
        + "(select sj from SyncJob sj where sj.sendingConfig.id = :sendingConfigId)"
        + ")")

Consider JPQL as SQL applied to Java objects with entities representing tables, their properties representing columns and the has-a relationship as expressing the mapping relationship. Now, when you want to join two tables, just refer to the corresponding entities and so long as the join columns are correctly specified, the SQL query will be correctly formed on the join tables and columns. Example SQL -

select column(s) from table1 <type of> join table2 on table1.column1 = table2.column1 where <filter conditions here>

Corresponding JPQL setup -

Entity1 (corresponds to table1) -> 
    property1 (corresponds to column)
    property2 (corresponds to mapping relationship, has @JoinColumn details)

JPQL for above setup -

select property1 from entity1 <type of> join entity1.property2 where <filter condition here>

Update after discussion in comments - Since a right join in the current setup is not possible, suggest to evaluate JPQL on performance parameters or alternatively to use the working SQL as nativeQuery.

@Query(value = "select tl.* from sync_job sj "+
                "join sync_sending_config ssc on ssc.id = sj.sending_config_id "+
                "join sync_job_transaction_log sjtl on sjtl.sync_job_id = sj.id "+
                "RIGHT JOIN transaction_log tl on tl.id = sjtl.transaction_log_id "+
                "where sjtl.sync_job_id is null and ssc.id = :sendingConfigId", 
                nativeQuery = true)
S B
  • 384
  • 2
  • 8
  • I'm really interested in learning the JPQL syntax for doing this join. – Cuga Jan 28 '20 at 22:48
  • Sure, but did this meet your needs? – S B Jan 29 '20 at 03:12
  • I updated answer with JPQL construction syntax details. – S B Jan 29 '20 at 10:05
  • I updated the post to include the exact SQL query I'm trying to write to go with the data. This query works when I run it directly in sql, matched up with the Entity classes in the post – Cuga Jan 29 '20 at 21:39
  • Your SQL and my JPQL returned the exact same results when I ran it as per the data setup in my answer. My JPQL works without a right join as it is first getting the TransactionLogs transmitted for a given SyncSendingConfig ID and then removing all such transmitted TransactionLogs from the available TransactionLogs. – S B Jan 30 '20 at 08:21
  • I'm concerned about performance impacts of using subselects, especially with 'in' conditions. In the end, I'm going to have to argue for whatever I submit to our repo. I could be wrong, but my impression is joining the tables will use the existing indexes, without duplicative records, in the most efficient manner. – Cuga Jan 31 '20 at 14:31
  • I understand the concern, it's just that with the current setup of entity relationships, right joining TransactionLog on SyncJobTransactionLog will not result in correct conditions being met and using sub-query as part of join does not seem to be an option. If you have enough volume of data to test, you can get the statistics of SQL vis-a-vis the JPQL. Alternatively, you could use the SQL as a nativeQuery – S B Jan 31 '20 at 14:36
  • Thanks, I appreciate that. Apparently JPQL doesn't allow right joins https://www.eclipse.org/forums/index.php/t/368610/. I have it working with a native query, like you suggested. If you update your answer to reflect the native query with a join, I'll award the bounty. `@Query(value = "select tl.* from sync_job sj join sync_sending_config ssc on ssc.id = sj.sending_config_id and ssc.id=:sendingConfigId join sync_job_transaction_log sjtl on sjtl.sync_job_id = sj.id RIGHT JOIN transaction_log tl on tl.id = sjtl.transaction_log_id where sjtl.sync_job_id is null", nativeQuery = true)` – Cuga Jan 31 '20 at 16:45