I've created a helper class IdTempTable based on the solution proposed by @takrl .
The additional issue I was facing was that our Dapper code resides in a separate layer, so I couldn't use several execute statements.
Usage:
public IList<int> LoadAnimalTypeIdsFromAnimalIds(IList<int> animalIds)
{
var idTempTable = new IdTempTable<int>(animalIds);
string query = string.Concat(idTempTable.Create,
idTempTable.Insert,
@"SELECT a.animalID
FROM dbo.animalTypes [at]",
idTempTable.JoinOn, @"at.animalId
INNER JOIN animals [a] on a.animalTypeId = at.animalTypeId
INNER JOIN edibleAnimals e on e.animalID = a.animalID");
using (var db = new SqlConnection(this.connectionString))
{
return db.Query<int>(query).ToList();
}
}
IdTempTable.cs:
/// <summary>
/// Helper class to filter a SQL query on a set of ID's,
/// using a temporary table instead of a WHERE clause.
/// </summary>
internal class IdTempTable<T>
where T: struct
{
// The limit SQL allows for the number of values in an INSERT statement.
private readonly int _chunkSize = 1000;
// Unique name for this instance, for thread safety.
private string _tableName;
/// <summary>
/// Helper class to filter a SQL query on a set of ID's,
/// using a temporary table instead of a WHERE clause.
/// </summary>
/// <param name="ids">
/// All elements in the collection must be of an integer number type.
/// </param>
internal IdTempTable(IEnumerable<T> ids)
{
Validate(ids);
var distinctIds = ids.Distinct();
Initialize(distinctIds);
}
/// <summary>
/// The SQL statement to create the temp table.
/// </summary>
internal string Create { get; private set; }
/// <summary>
/// The SQL statement to fill the temp table.
/// </summary>
internal string Insert { get; private set; }
/// <summary>
/// The SQL clause to join the temp table with the main table.
/// Complete the clause by adding the foreign key from the main table.
/// </summary>
internal string JoinOn => $" INNER JOIN {_tableName} ON {_tableName}.Id = ";
private void Initialize(IEnumerable<T> ids)
{
_tableName = BuildName();
Create = BuildCreateStatement(ids);
Insert = BuildInsertStatement(ids);
}
private string BuildName()
{
var guid = Guid.NewGuid();
return "#ids_" + guid.ToString("N");
}
private string BuildCreateStatement(IEnumerable<T> ids)
{
string dataType = GetDataType(ids);
return $"CREATE TABLE {_tableName} (Id {dataType} NOT NULL PRIMARY KEY); ";
}
private string BuildInsertStatement(IEnumerable<T> ids)
{
var statement = new StringBuilder();
while (ids.Any())
{
string group = string.Join(") ,(", ids.Take(_chunkSize));
statement.Append($"INSERT INTO {_tableName} VALUES ({group}); ");
ids = ids.Skip(_chunkSize);
}
return statement.ToString();
}
private string GetDataType(IEnumerable<T> ids)
{
string type = !ids.Any() || ids.First() is long || ids.First() is ulong
? "BIGINT"
: "INT";
return type;
}
private void Validate(IEnumerable<T> ids)
{
if (ids == null)
{
throw new ArgumentNullException(nameof(ids));
}
if (!ids.Any())
{
return;
}
if (ids.Any(id => !IsInteger(id)))
{
throw new ArgumentException("One or more values in the collection are not an integer");
}
}
private bool IsInteger(T value)
{
return value is sbyte ||
value is byte ||
value is short ||
value is ushort ||
value is int ||
value is uint ||
value is long ||
value is ulong;
}
}
Some advantages I can see:
You can first join with the temp table for better performance. This limits the number of rows you're joining with the other tables.
Reusable for other integer number types.
You can separate the logic from the actual execution.
Every instance has its unique table name, making it thread safe.
(arguably) better readability.