1

Looking for assistance/direction in setting up a loop? function to find related records in a table.

The table (tblTransactions) holds information about various transactions we are tracking. I am also using this table to reference a predecessor transaction. Now I am seeking a way to loop through the table to find related records.

The table has the following fields:

  • TransID - primary key

  • Grantor - name field

  • Grantee - name field

  • PTrans - number field that references TransID


Some sample data:

+---------+---------+---------+--------+
| TransID | Grantor | Grantee | PTrans |
+---------+---------+---------+--------+
|       1 | Bob     | Sally   |      0 |
|       2 | Jane    | Emily   |      0 |
|       3 | Sally   | Beth    |      1 |
|       4 | Beth    | Sam     |      3 |
+---------+---------+---------+--------+

Ideally I'd like to be able to start with TransID 4 and show all the transaction data, on separate rows, for the selected transaction (4) and it's predecessors.

Results would be:

+---+-------+-------+
| 4 | Beth  | Sam   |
| 3 | Sally | Beth  |
| 1 | Bob   | Sally |
+---+-------+-------+
braX
  • 11,506
  • 5
  • 20
  • 33
Ember
  • 13
  • 2

1 Answers1

0

Your question concerning querying self-referential data is very similar to this question in which the user has a table of employees, each of which may have a supervisor whose employeee record is also present in the same table, thus forming a hierarchy.

A relatively easy way to solve this would be using a DLookup expression within a loop or within a recursive call until the expression returned Null. For example, here is a recursive variant:

Function TransLookup(lngtrn As Long)
    Dim lngptr
    lngptr = DLookup("ptrans", "tbltransactions", "transid = " & lngtrn)

    If Not IsNull(lngptr) Then
        Debug.Print lngtrn ' Do something with the data
        TransLookup (lngptr)
    End If
End Function

Evaluated with your data this would yield:

?TransLookup(4)
 4 
 3 
 1 

This is of course only printing the transaction ID, but the function could alternatively populate a separate table with the data for each transaction if required.


However, returning the results record-by-record or populating a temporary table seems inelegant if we can construct a single SQL query to return all of the results in one go.

However,since MS Access does not support recursive SQL queries, the difficulty when querying such hierarchical data is not knowing how many levels to code ahead of time.

As such, you could use a VBA function to construct the SQL query itself, and thus always incorporating as many levels as is necessary to return the full dataset.

Indeed, this is the approach I put forward in my answer to the related question linked above - the function provided in that answer could equally be adapted to suit this situation, for example:

Function BuildQuerySQL(lngtrn As Long) As String
    Dim intlvl As Integer
    Dim strsel As String: strsel = selsql(intlvl)
    Dim strfrm As String: strfrm = "tbltransactions as t0 "
    Dim strwhr As String: strwhr = "where t0.transid = " & lngtrn

    While HasRecordsP(strsel & strfrm & strwhr)
        intlvl = intlvl + 1
        BuildQuerySQL = BuildQuerySQL & " union " & strsel & strfrm & strwhr
        strsel = selsql(intlvl)
        If intlvl > 1 Then
            strfrm = "(" & strfrm & ")" & frmsql(intlvl)
        Else
            strfrm = strfrm & frmsql(intlvl)
        End If
    Wend
    BuildQuerySQL = Mid(BuildQuerySQL, 8)
End Function

Function HasRecordsP(strSQL As String) As Boolean
    Dim dbs As DAO.Database
    Set dbs = CurrentDb
    With dbs.OpenRecordset(strSQL)
        HasRecordsP = Not .EOF
        .Close
    End With
    Set dbs = Nothing
End Function

Function selsql(intlvl As Integer) As String
    selsql = "select t" & intlvl & ".* from "
End Function

Function frmsql(intlvl As Integer) As String
    frmsql = " inner join tbltransactions as t" & intlvl & " on t" & intlvl - 1 & ".ptrans = t" & intlvl & ".transid "
End Function

Now, evaluating the BuildQuerySQL function with Transaction ID 4 yields the following SQL UNION query, with each level of nesting unioned with the previous query:

select 
    t0.* 
from 
    tbltransactions as t0 
where 
    t0.transid = 4 

union 

select 
    t1.* 
from 
    tbltransactions as t0 inner join tbltransactions as t1 
    on t0.ptrans = t1.transid 
where 
    t0.transid = 4 

union 

select 
    t2.* 
from
    (
        tbltransactions as t0 inner join tbltransactions as t1 
        on t0.ptrans = t1.transid 
    ) 
    inner join tbltransactions as t2 
    on t1.ptrans = t2.transid 
where 
    t0.transid = 4

Such function may therefore be evaluated to construct a saved query, e.g. for Transaction ID = 4, the following would create a query called TransactionList:

Sub test()
    CurrentDb.CreateQueryDef "TransactionList", BuildQuerySQL(4)
End Sub

Or alternatively, the SQL may be evaluated to open a RecordSet of the results, depending on the requirements of your application.

When evaluated with your sample data, the above SQL query will yield the following results:

+---------+---------+---------+--------+
| TransID | Grantor | Grantee | PTrans |
+---------+---------+---------+--------+
|       1 | Bob     | Sally   |      0 |
|       3 | Sally   | Beth    |      1 |
|       4 | Beth    | Sam     |      3 |
+---------+---------+---------+--------+
Lee Mac
  • 15,615
  • 6
  • 32
  • 80
  • Thank you @lee-mac, I did see that post and didn't think it would apply to my specific situation. This suggestion returns 3 levels, but what if I don't know how many levels it would take to reach the top (first transaction in the sequence)? – Ember Oct 07 '19 at 18:29
  • @Ember Did you read my post in full? The VBA functions provided will handle any number of levels. – Lee Mac Oct 07 '19 at 21:23
  • yes I did read the whole thing, multiple times in fact, but apparently I didn't understand it fully. I added your suggested BuildQuerySQL to a new module and added the SQLquery to a new query. I didn't see how the two related to one another, but when I ran my new query (with your suggested code) it worked, but it only showed 3 levels. Currently in my db I have a 5 level chain. I figured out how to nest the inner joins and now when I run the query I do see my 5 level chain. – Ember Oct 08 '19 at 19:38
  • in re-re-reading everything, I'm thinking that the suggested querySQL was not meant to be added directly into a query but that is what would be added once the BuildQuerySQL function was referenced. I added a command button and on the OnClick added an event procedure to call the docmd.openmodule "TransLookup", "BuildQuerySQL". TransLookup is what I called the module since there were several functions provided. When I click on my command button it just takes me to the module but no errors pop up...thinking I'm still missing a piece. The search continues... – Ember Oct 08 '19 at 20:17
  • ooo, I was right! the querySQL is an output when you run the BuildQuerySQL. I was able to add, and run, the Sub test () and totally see how this works. Now how to get it to ask for, or look for, the starting transid. current code forces it to #4, which I can change in the code and run to view, but how to pass this value to the function. Getting close... – Ember Oct 08 '19 at 20:29
  • Glad to hear that you got there in the end. To prompt the user to supply an appropriate transaction ID, you could use a basic VBA `InputBox`, but this is unprofessional in my opinion; I would instead suggest prompting the user with an MS Access form in which they may either specify a transaction ID in an edit box, or select a valid transaction ID from a list box or combo box. You can then easily reference the content of the form control. – Lee Mac Oct 08 '19 at 21:36
  • 1
    thank you so much for your assistance! The function is working beautifully! – Ember Oct 09 '19 at 17:23