Skip to content

Commit

Permalink
Merge pull request #76 from henkmollema/multiple-key-properties
Browse files Browse the repository at this point in the history
Implement composite primary keys
  • Loading branch information
henkmollema authored Oct 19, 2018
2 parents a7789f4 + f7dd255 commit e3a7439
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 43 deletions.
19 changes: 7 additions & 12 deletions src/Dommel/DommelMapper.DefaultKeyPropertyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,27 @@ public static partial class DommelMapper
public class DefaultKeyPropertyResolver : IKeyPropertyResolver
{
/// <summary>
/// Finds the key property by looking for a property with the [Key] attribute or with the name 'Id'.
/// Finds the key properties by looking for properties with the [Key] attribute.
/// </summary>
public virtual PropertyInfo ResolveKeyProperty(Type type) => ResolveKeyProperty(type, out _);
public PropertyInfo[] ResolveKeyProperties(Type type) => ResolveKeyProperties(type, out _);

/// <summary>
/// Finds the key property by looking for a property with the [Key] attribute or with the name 'Id'.
/// Finds the key properties by looking for properties with the [Key] attribute.
/// </summary>
public PropertyInfo ResolveKeyProperty(Type type, out bool isIdentity)
public PropertyInfo[] ResolveKeyProperties(Type type, out bool isIdentity)
{
var keyProps = Resolvers
.Properties(type)
.Where(p => p.GetCustomAttribute<KeyAttribute>() != null || p.Name.Equals("Id", StringComparison.OrdinalIgnoreCase))
.Where(p => string.Equals(p.Name, "Id", StringComparison.OrdinalIgnoreCase) || p.GetCustomAttribute<KeyAttribute>() != null)
.ToArray();

if (keyProps.Length == 0)
{
throw new InvalidOperationException($"Could not find the key property for type '{type.FullName}'.");
}

if (keyProps.Length > 1)
{
throw new InvalidOperationException($"Multiple key properties were found for type '{type.FullName}'.");
throw new InvalidOperationException($"Could not find the key properties for type '{type.FullName}'.");
}

isIdentity = true;
return keyProps[0];
return keyProps;
}
}
}
Expand Down
101 changes: 101 additions & 0 deletions src/Dommel/DommelMapper.Get.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dapper;

Expand All @@ -10,6 +12,7 @@ namespace Dommel
public static partial class DommelMapper
{
private static readonly ConcurrentDictionary<Type, string> _getQueryCache = new ConcurrentDictionary<Type, string>();
private static readonly ConcurrentDictionary<Type, string> _getByIdsQueryCache = new ConcurrentDictionary<Type, string>();
private static readonly ConcurrentDictionary<Type, string> _getAllQueryCache = new ConcurrentDictionary<Type, string>();

/// <summary>
Expand Down Expand Up @@ -60,6 +63,104 @@ private static string BuildGetById(Type type, object id, out DynamicParameters p
return sql;
}

/// <summary>
/// Retrieves the entity of type <typeparamref name="TEntity"/> with the specified id.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="connection">The connection to the database. This can either be open or closed.</param>
/// <param name="ids">The id of the entity in the database.</param>
/// <returns>The entity with the corresponding id.</returns>
public static TEntity Get<TEntity>(this IDbConnection connection, params object[] ids) where TEntity : class
=> Get<TEntity>(connection, ids, transaction: null);

/// <summary>
/// Retrieves the entity of type <typeparamref name="TEntity"/> with the specified id.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="connection">The connection to the database. This can either be open or closed.</param>
/// <param name="ids">The id of the entity in the database.</param>
/// <param name="transaction">Optional transaction for the command.</param>
/// <returns>The entity with the corresponding id.</returns>
public static TEntity Get<TEntity>(this IDbConnection connection, object[] ids, IDbTransaction transaction = null) where TEntity : class
{
if (ids.Length == 1)
{
return Get<TEntity>(connection, ids[0], transaction);
}

var sql = BuildGetByIds(typeof(TEntity), ids, out var parameters);
LogQuery<TEntity>(sql);
return connection.QueryFirstOrDefault<TEntity>(sql, parameters);
}

/// <summary>
/// Retrieves the entity of type <typeparamref name="TEntity"/> with the specified id.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="connection">The connection to the database. This can either be open or closed.</param>
/// <param name="ids">The id of the entity in the database.</param>
/// <returns>The entity with the corresponding id.</returns>
public static Task<TEntity> GetAsync<TEntity>(this IDbConnection connection, params object[] ids) where TEntity : class
=> GetAsync<TEntity>(connection, ids, transaction: null);

/// <summary>
/// Retrieves the entity of type <typeparamref name="TEntity"/> with the specified id.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="connection">The connection to the database. This can either be open or closed.</param>
/// <param name="ids">The id of the entity in the database.</param>
/// <param name="transaction">Optional transaction for the command.</param>
/// <returns>The entity with the corresponding id.</returns>
public static Task<TEntity> GetAsync<TEntity>(this IDbConnection connection, object[] ids, IDbTransaction transaction = null) where TEntity : class
{
if (ids.Length == 1)
{
return GetAsync<TEntity>(connection, ids[0], transaction);
}

var sql = BuildGetByIds(typeof(TEntity), ids, out var parameters);
LogQuery<TEntity>(sql);
return connection.QueryFirstOrDefaultAsync<TEntity>(sql, parameters);
}

private static string BuildGetByIds(Type type, object[] ids, out DynamicParameters parameters)
{
if (!_getByIdsQueryCache.TryGetValue(type, out var sql))
{
var tableName = Resolvers.Table(type);
var keyProperties = Resolvers.KeyProperties(type);
var keyColumnsNames = keyProperties.Select(Resolvers.Column).ToArray();
if (keyColumnsNames.Length != ids.Length)
{
throw new InvalidOperationException($"Number of key columns ({keyColumnsNames.Length}) of type {type.Name} does not match with the number of specified IDs ({ids.Length}).");
}

var sb = new StringBuilder("select * from ").Append(tableName).Append(" where");
var i = 0;
foreach (var keyColumnName in keyColumnsNames)
{
if (i != 0)
{
sb.Append(" and");
}

sb.Append(" ").Append(keyColumnName).Append(" = @Id").Append(i);
i++;
}

sql = sb.ToString();
_getByIdsQueryCache.TryAdd(type, sql);
}

parameters = new DynamicParameters();
for (var i = 0; i < ids.Length; i++)
{
parameters.Add("Id" + i, ids[i]);
}

return sql;
}

/// <summary>
/// Retrieves all the entities of type <typeparamref name="TEntity"/>.
/// </summary>
Expand Down
18 changes: 9 additions & 9 deletions src/Dommel/DommelMapper.IKeyPropertyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ public static partial class DommelMapper
public interface IKeyPropertyResolver
{
/// <summary>
/// Resolves the key property for the specified type.
/// Resolves the key properties for the specified type.
/// </summary>
/// <param name="type">The type to resolve the key property for.</param>
/// <returns>A <see cref="PropertyInfo"/> instance of the key property of <paramref name="type"/>.</returns>
PropertyInfo ResolveKeyProperty(Type type);
/// <param name="type">The type to resolve the key properties for.</param>
/// <returns>A collection of <see cref="PropertyInfo"/> instances of the key properties of <paramref name="type"/>.</returns>
PropertyInfo[] ResolveKeyProperties(Type type);

/// <summary>
/// Resolves the key property for the specified type.
/// Resolves the key properties for the specified type.
/// </summary>
/// <param name="type">The type to resolve the key property for.</param>
/// <param name="isIdentity">Indicates whether the key property is an identity property.</param>
/// <returns>A <see cref="PropertyInfo"/> instance of the key property of <paramref name="type"/>.</returns>
PropertyInfo ResolveKeyProperty(Type type, out bool isIdentity);
/// <param name="type">The type to resolve the key properties for.</param>
/// <param name="isIdentity">Indicates whether the key properties are identity properties.</param>
/// <returns>A collection of <see cref="PropertyInfo"/> instances of the key properties of <paramref name="type"/>.</returns>
PropertyInfo[] ResolveKeyProperties(Type type, out bool isIdentity);
}
}
}
57 changes: 43 additions & 14 deletions src/Dommel/DommelMapper.Resolvers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static class Resolvers
{
private static readonly ConcurrentDictionary<Type, string> _typeTableNameCache = new ConcurrentDictionary<Type, string>();
private static readonly ConcurrentDictionary<string, string> _columnNameCache = new ConcurrentDictionary<string, string>();
private static readonly ConcurrentDictionary<Type, KeyPropertyInfo> _typeKeyPropertyCache = new ConcurrentDictionary<Type, KeyPropertyInfo>();
private static readonly ConcurrentDictionary<Type, KeyPropertyInfo> _typeKeyPropertiesCache = new ConcurrentDictionary<Type, KeyPropertyInfo>();
private static readonly ConcurrentDictionary<Type, PropertyInfo[]> _typePropertiesCache = new ConcurrentDictionary<Type, PropertyInfo[]>();
private static readonly ConcurrentDictionary<string, ForeignKeyInfo> _typeForeignKeyPropertyCache = new ConcurrentDictionary<string, ForeignKeyInfo>();

Expand All @@ -66,21 +66,50 @@ public static class Resolvers
/// Gets the key property for the specified type, using the configured <see cref="IKeyPropertyResolver"/>.
/// </summary>
/// <param name="type">The <see cref="Type"/> to get the key property for.</param>
/// <param name="isIdentity">A value indicating whether the key is an identity.</param>
/// <param name="isIdentity">A value indicating whether the key represents an identity.</param>
/// <returns>The key property for <paramref name="type"/>.</returns>
public static PropertyInfo KeyProperty(Type type, out bool isIdentity)
{
if (!_typeKeyPropertyCache.TryGetValue(type, out var keyProperty))
if (!_typeKeyPropertiesCache.TryGetValue(type, out var keyPropertyInfo))
{
var propertyInfo = _keyPropertyResolver.ResolveKeyProperty(type, out isIdentity);
keyProperty = new KeyPropertyInfo(propertyInfo, isIdentity);
_typeKeyPropertyCache.TryAdd(type, keyProperty);
var propertyInfos = _keyPropertyResolver.ResolveKeyProperties(type, out isIdentity);
keyPropertyInfo = new KeyPropertyInfo(propertyInfos, isIdentity);
_typeKeyPropertiesCache.TryAdd(type, keyPropertyInfo);
}

isIdentity = keyProperty.IsIdentity;
isIdentity = keyPropertyInfo.IsIdentity;

LogReceived?.Invoke($"Resolved property '{keyProperty.PropertyInfo}' (Identity: {isIdentity}) as key property for '{type.Name}'");
return keyProperty.PropertyInfo;
var propertyInfo = keyPropertyInfo.PropertyInfos[0];
LogReceived?.Invoke($"Resolved property '{propertyInfo}' (Identity: {isIdentity}) as key property for '{type.Name}'");
return propertyInfo;
}

/// <summary>
/// Gets the key properties for the specified type, using the configured <see cref="IKeyPropertyResolver"/>.
/// </summary>
/// <param name="type">The <see cref="Type"/> to get the key properties for.</param>
/// <returns>The key properties for <paramref name="type"/>.</returns>
public static PropertyInfo[] KeyProperties(Type type) => KeyProperties(type, out _);

/// <summary>
/// Gets the key properties for the specified type, using the configured <see cref="IKeyPropertyResolver"/>.
/// </summary>
/// <param name="type">The <see cref="Type"/> to get the key properties for.</param>
/// <param name="isIdentity">A value indicating whether the keys represent an identity.</param>
/// <returns>The key properties for <paramref name="type"/>.</returns>
public static PropertyInfo[] KeyProperties(Type type, out bool isIdentity)
{
if (!_typeKeyPropertiesCache.TryGetValue(type, out var keyPropertyInfo))
{
var propertyInfos = _keyPropertyResolver.ResolveKeyProperties(type, out isIdentity);
keyPropertyInfo = new KeyPropertyInfo(propertyInfos, isIdentity);
_typeKeyPropertiesCache.TryAdd(type, keyPropertyInfo);
}

isIdentity = keyPropertyInfo.IsIdentity;

LogReceived?.Invoke($"Resolved property '{string.Join<PropertyInfo>(", ", keyPropertyInfo.PropertyInfos)}' (Identity: {isIdentity}) as key property for '{type.Name}'");
return keyPropertyInfo.PropertyInfos;
}

/// <summary>
Expand Down Expand Up @@ -172,20 +201,20 @@ public static string Column(PropertyInfo propertyInfo)
return columnName;
}

private class KeyPropertyInfo
private struct KeyPropertyInfo
{
public KeyPropertyInfo(PropertyInfo propertyInfo, bool isIdentity)
public KeyPropertyInfo(PropertyInfo[] propertyInfos, bool isIdentity)
{
PropertyInfo = propertyInfo;
PropertyInfos = propertyInfos;
IsIdentity = isIdentity;
}

public PropertyInfo PropertyInfo { get; }
public PropertyInfo[] PropertyInfos { get; }

public bool IsIdentity { get; }
}

private class ForeignKeyInfo
private struct ForeignKeyInfo
{
public ForeignKeyInfo(PropertyInfo propertyInfo, ForeignKeyRelation relation)
{
Expand Down
17 changes: 9 additions & 8 deletions test/Dommel.Tests/DefaultKeyPropertyResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ namespace Dommel.Tests
{
public class DefaultKeyPropertyResolverTests
{
private static DommelMapper.DefaultKeyPropertyResolver Resolver = new DommelMapper.DefaultKeyPropertyResolver();
private static DommelMapper.IKeyPropertyResolver Resolver = new DommelMapper.DefaultKeyPropertyResolver();

[Fact]
public void MapsIdProperty()
{
var prop = Resolver.ResolveKeyProperty(typeof(Foo));
var prop = Resolver.ResolveKeyProperties(typeof(Foo))[0];
Assert.Equal(typeof(Foo).GetProperty("Id"), prop);
}

Expand All @@ -35,22 +35,23 @@ public void MapsIdPropertyGenericInheritance()
[Fact]
public void MapsWithAttribute()
{
var prop = Resolver.ResolveKeyProperty(typeof(Bar));
var prop = Resolver.ResolveKeyProperties(typeof(Bar))[0];
Assert.Equal(typeof(Bar).GetProperty("BarId"), prop);
}

[Fact]
public void NoKeyProperties_ThrowsException()
{
var ex = Assert.Throws<InvalidOperationException>(() => Resolver.ResolveKeyProperty(typeof(Nope)));
Assert.Equal($"Could not find the key property for type '{typeof(Nope).FullName}'.", ex.Message);
var ex = Assert.Throws<InvalidOperationException>(() => Resolver.ResolveKeyProperties(typeof(Nope))[0]);
Assert.Equal($"Could not find the key properties for type '{typeof(Nope).FullName}'.", ex.Message);
}

[Fact]
public void MultipleKeyProperties_ThrowsException()
public void MapsMultipleKeyProperties()
{
var ex = Assert.Throws<InvalidOperationException>(() => Resolver.ResolveKeyProperty(typeof(FooBar)));
Assert.Equal($"Multiple key properties were found for type '{typeof(FooBar).FullName}'.", ex.Message);
var keyProperties = Resolver.ResolveKeyProperties(typeof(FooBar));
Assert.Equal(typeof(FooBar).GetProperty("Id"), keyProperties[0]);
Assert.Equal(typeof(FooBar).GetProperty("BarId"), keyProperties[1]);
}


Expand Down
33 changes: 33 additions & 0 deletions test/Dommel.Tests/LoggingTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data;
using Dapper;
using Moq;
Expand Down Expand Up @@ -29,6 +30,27 @@ public void GetLogsSql()
Assert.Single(logs);
Assert.Equal("Get<Foo>: select * from Foos where Id = @Id", logs[0]);
}

[Fact]
public void GetByIdsLogsSql()
{
// Arrange
var logs = new List<string>();
var mock = new Mock<IDbConnection>();
mock.SetupDapper(x => x.QueryFirstOrDefault<Foo>(It.IsAny<string>(), It.IsAny<object>(), null, null, null))
.Returns(new Foo());

// Initialize resolver caches so these messages are not logged
mock.Object.Get<Bar>("key1", "key2", "key3");
DommelMapper.LogReceived = s => logs.Add(s);

// Act
mock.Object.Get<Bar>("key1", "key2", "key3");

// Assert
Assert.Single(logs);
Assert.Equal("Get<Bar>: select * from Bars where Id = @Id0 and KeyColumn2 = @Id1 and KeyColumn3 = @Id2", logs[0]);
}

[Fact]
public void GetTwiceLogsSqlTwice()
Expand Down Expand Up @@ -74,4 +96,15 @@ public class Foo
{
public int Id { get; set; }
}

public class Bar
{
public string Id { get; set; }

[Key]
public string KeyColumn2 {get;set;}

[Key]
public string KeyColumn3 {get;set;}
}
}

0 comments on commit e3a7439

Please sign in to comment.