PostgreSQL · RetrieveMultiple

Accounts and contacts in one query

Reading across tables, in three scenarios: a link entity (a join with its own alias, columns and criteria); a LEFT join that keeps rows without a match; and the short form, a direct join written in the column set (account_id._lk.account.name,account_number).

Set up for this page

The scenarios build on the sample application: SampleContext.cs (the dbContext every sample is given); SampleOutput.cs (Step and Show, which print); SampleContext.Application.cs (dbContext.Service, the application's ARX service); SampleCatalog.cs (the tables' ids, keys, lookups and display names).

Before scenario 1 the sample runs this, so the service and the rows the scenarios use are there:

Set up
var account = new ArxEntity("account");
account["name"] = "Linked Logistics";
account["account_number"] = "ACC-R602";
account["status"] = new ArxSelectionSetValue("AccountStatus", "ACTIVE");
var accountId = await service.CreateAsync(account);
var contacts = new List<ArxEntity>();
foreach (var (first, withAccount) in new[] { ("Lena", true), ("Lars", true), ("Lone", false) })
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = "Link-R602";
    contact["account_id"] = withAccount ? new ArxEntityReference("account", accountId) : null;
    contacts.Add(contact);
}
await service.CreateMultipleAsync(contacts);

1 A link entity: each contact with its account

Contacts with their account's name and number: an inner join; the columns come back as acc.name.

Already in scope: service (ArxPgDbService)

C#
var query = Contacts();
var account = query.AddLink("account", "account_id", "id", ArxJoinOperator.Inner);
account.EntityAlias = "acc";
account.ColumnSet.AddColumns("name", "account_number");
account.LinkCriteria.AddCondition("status", ArxConditionOperator.Equal, "ACTIVE");

var rows = await service.RetrieveMultipleAsync(query);

foreach (var row in rows.Entities)
{
    Console.WriteLine($"{row["first_name"]} works at {row["acc.name"]} ({row["acc.account_number"]})");
}
Used above

Defined once in this sample.

private static ArxQueryExpression Contacts()
{
    var query = new ArxQueryExpression("contact");
    query.ColumnSet.AddColumn("first_name");
    query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, "Link-R602");
    query.AddOrder("first_name", ArxOrderType.Ascending);
    return query;
}

Check it in the database

SQL

Run it in any SQL tool connected to the database, after the code has run.

SELECT c.first_name, a.name, a.account_number
FROM contact c
JOIN account a ON a.id = c.account_id AND a.status = 'ACTIVE'
WHERE c.last_name = 'Link-R602'
ORDER BY c.first_name;

Expect: 2 rows, Lars and Lena at Linked Logistics (ACC-R602).

2 A LEFT join keeps the contact without an account

A LEFT join: Lone has no account and is kept, with a null account name.

Already in scope: service (ArxPgDbService)

C#
var query = Contacts();
var account = query.AddLink("account", "account_id", "id", ArxJoinOperator.Left);
account.EntityAlias = "acc";
account.ColumnSet.AddColumn("name");

var rows = await service.RetrieveMultipleAsync(query);
Console.WriteLine(string.Join(", ", rows.Entities.Select(x => $"{x["first_name"]}: {x["acc.name"] ?? "(no account)"}")));
Used above

Defined once in this sample.

private static ArxQueryExpression Contacts()
{
    var query = new ArxQueryExpression("contact");
    query.ColumnSet.AddColumn("first_name");
    query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, "Link-R602");
    query.AddOrder("first_name", ArxOrderType.Ascending);
    return query;
}

Check it in the database

SQL

Run it in any SQL tool connected to the database, after the code has run.

SELECT c.first_name, a.name
FROM contact c
LEFT JOIN account a ON a.id = c.account_id
WHERE c.last_name = 'Link-R602'
ORDER BY c.first_name;

Expect: 3 rows, Lars and Lena with Linked Logistics, Lone with NULL.

3 A direct join written in the column set

The short form: account_id._lk.account.name,account_number in the column set joins the account on its declared id and reads its columns as account_id.name and account_id.account_number.

Already in scope: service (ArxPgDbService)

C#
var query = Contacts();
query.ColumnSet.AddColumn("account_id._lk.account.name,account_number");

var rows = await service.RetrieveMultipleAsync(query);
Console.WriteLine(string.Join(", ", rows.Entities.Select(x => $"{x["first_name"]}: {x["account_id.account_number"] ?? "-"}")));
Used above

Defined once in this sample.

private static ArxQueryExpression Contacts()
{
    var query = new ArxQueryExpression("contact");
    query.ColumnSet.AddColumn("first_name");
    query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, "Link-R602");
    query.AddOrder("first_name", ArxOrderType.Ascending);
    return query;
}

Check it in the database

SQL

Run it in any SQL tool connected to the database, after the code has run.

SELECT c.first_name, a.account_number
FROM contact c
LEFT JOIN account a ON a.id = c.account_id
WHERE c.last_name = 'Link-R602'
ORDER BY c.first_name;

Expect: 3 rows, Lars ACC-R602, Lena ACC-R602, Lone NULL (a direct join is a LEFT join).

Run the complete sample

The code above comes from one program, built in a fresh project and run against the database. To run it yourself, here is every file and step.

The program, its files and how to run it

Run it locally

  1. .NET SDK: .NET SDK 10.0.100 or later 10.0.x (target net10.0).
  2. Create the project:
    Terminal
    dotnet new console -n ArxSample -f net10.0
    cd ArxSample
  3. Install the packages:
    Terminal
    dotnet add package ARX.SDK.PostgreSql --version 1.0.0-rc.1
  4. Database: a PostgreSQL 16 (tested with 16.15) database with the sample schema.
  5. Configuration: set ARX_SAMPLES_POSTGRESQL to its connection string. Nothing is hard-coded.
  6. Source code: add the files of the sample application this sample uses (SampleContext.cs, SampleOutput.cs, SampleContext.Application.cs, SampleCatalog.cs), then replace Program.cs and add the other file below, with the file names shown.
  7. Run it:
    Terminal
    dotnet run
  8. Expected output: below.

The files

Program.cs

Runs the sample. Generated for this page; it reads the connection string from the environment.

using ArxSamples.PostgreSql;
using ArxSamples.PostgreSql.Operations.RetrieveMultiple.RealCases;

// The connection string comes from the environment, never from the code (see "Configure the connection").
var connectionString = Environment.GetEnvironmentVariable("ARX_SAMPLES_POSTGRESQL")
    ?? throw new InvalidOperationException("Set the ARX_SAMPLES_POSTGRESQL environment variable to your PostgreSQL connection string.");

await new RealCase2_AccountWithItsContacts().RunAsync(new SampleContext(connectionString));
RealCase2_AccountWithItsContacts.cs

The sample, as it is in the repository.

using ARX.SDK.Query;
using ARX.SDK.Services;
using static ArxSamples.PostgreSql.SampleOutput;

namespace ArxSamples.PostgreSql.Operations.RetrieveMultiple.RealCases;

/// <summary>
/// Reading across tables, in three scenarios: a link entity (a join with its own alias, columns and criteria); a LEFT
/// join that keeps rows without a match; and the short form, a direct join written in the column set
/// (<c>account_id._lk.account.name,account_number</c>).
/// </summary>
public sealed class RealCase2_AccountWithItsContacts : ISample
{
    public string Name => "retrieve-multiple-real-2-account-with-its-contacts";

    public string Title => "Accounts and contacts in one query";

    public async Task RunAsync(SampleContext dbContext)
    {
        var account = new ArxEntity("account");
        account["name"] = "Linked Logistics";
        account["account_number"] = "ACC-R602";
        account["status"] = new ArxSelectionSetValue("AccountStatus", "ACTIVE");
        var accountId = await dbContext.Service.CreateAsync(account);

        var contacts = new List<ArxEntity>();

        foreach (var (first, withAccount) in new[] { ("Lena", true), ("Lars", true), ("Lone", false) })
        {
            var contact = new ArxEntity("contact");
            contact["first_name"] = first;
            contact["last_name"] = "Link-R602";
            contact["account_id"] = withAccount ? new ArxEntityReference("account", accountId) : null;
            contacts.Add(contact);
        }

        await dbContext.Service.CreateMultipleAsync(contacts);

        await Scenario1_ALinkEntityAsync(dbContext);

        await Scenario2_ALeftJoinKeepsEveryContactAsync(dbContext);

        await Scenario3_ADirectJoinInTheColumnSetAsync(dbContext);
    }

    private static ArxQueryExpression Contacts()
    {
        var query = new ArxQueryExpression("contact");
        query.ColumnSet.AddColumn("first_name");
        query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, "Link-R602");
        query.AddOrder("first_name", ArxOrderType.Ascending);
        return query;
    }

    /// <summary>1. Contacts with their account's name and number: an inner join; the columns come back as <c>acc.name</c>.</summary>
    private static async Task Scenario1_ALinkEntityAsync(SampleContext dbContext)
    {
        Step("1. A link entity: each contact with its account");

        var query = Contacts();
        var account = query.AddLink("account", "account_id", "id", ArxJoinOperator.Inner);
        account.EntityAlias = "acc";
        account.ColumnSet.AddColumns("name", "account_number");
        account.LinkCriteria.AddCondition("status", ArxConditionOperator.Equal, "ACTIVE");

        var rows = await dbContext.Service.RetrieveMultipleAsync(query);

        foreach (var row in rows.Entities)
        {
            Show($"{row["first_name"]} works at {row["acc.name"]} ({row["acc.account_number"]})");
        }

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT c.first_name, a.name, a.account_number
         * FROM contact c
         * JOIN account a ON a.id = c.account_id AND a.status = 'ACTIVE'
         * WHERE c.last_name = 'Link-R602'
         * ORDER BY c.first_name;
         * -- Expect: 2 rows, Lars and Lena at Linked Logistics (ACC-R602).
         * ===========================================================
         */
    }

    /// <summary>2. A LEFT join: Lone has no account and is kept, with a null account name.</summary>
    private static async Task Scenario2_ALeftJoinKeepsEveryContactAsync(SampleContext dbContext)
    {
        Step("2. A LEFT join keeps the contact without an account");

        var query = Contacts();
        var account = query.AddLink("account", "account_id", "id", ArxJoinOperator.Left);
        account.EntityAlias = "acc";
        account.ColumnSet.AddColumn("name");

        var rows = await dbContext.Service.RetrieveMultipleAsync(query);
        Show(string.Join(", ", rows.Entities.Select(x => $"{x["first_name"]}: {x["acc.name"] ?? "(no account)"}")));

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT c.first_name, a.name
         * FROM contact c
         * LEFT JOIN account a ON a.id = c.account_id
         * WHERE c.last_name = 'Link-R602'
         * ORDER BY c.first_name;
         * -- Expect: 3 rows, Lars and Lena with Linked Logistics, Lone with NULL.
         * ===========================================================
         */
    }

    /// <summary>
    /// 3. The short form: <c>account_id._lk.account.name,account_number</c> in the column set joins the account on its
    /// declared id and reads its columns as <c>account_id.name</c> and <c>account_id.account_number</c>.
    /// </summary>
    private static async Task Scenario3_ADirectJoinInTheColumnSetAsync(SampleContext dbContext)
    {
        Step("3. A direct join written in the column set");

        var query = Contacts();
        query.ColumnSet.AddColumn("account_id._lk.account.name,account_number");

        var rows = await dbContext.Service.RetrieveMultipleAsync(query);
        Show(string.Join(", ", rows.Entities.Select(x => $"{x["first_name"]}: {x["account_id.account_number"] ?? "-"}")));

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT c.first_name, a.account_number
         * FROM contact c
         * LEFT JOIN account a ON a.id = c.account_id
         * WHERE c.last_name = 'Link-R602'
         * ORDER BY c.first_name;
         * -- Expect: 3 rows, Lars ACC-R602, Lena ACC-R602, Lone NULL (a direct join is a LEFT join).
         * ===========================================================
         */
    }
}

Expected output

What dotnet run printed when the sample was verified, on a freshly created sample schema. Ids differ when the tables already hold rows.

Output
  > 1. A link entity: each contact with its account
      Lars works at Linked Logistics (ACC-R602)
      Lena works at Linked Logistics (ACC-R602)
  > 2. A LEFT join keeps the contact without an account
      Lars: Linked Logistics, Lena: Linked Logistics, Lone: (no account)
  > 3. A direct join written in the column set
      Lars: ACC-R602, Lena: ACC-R602, Lone: -