Skip to content

Commit

Permalink
Revisited Table Identity.
Browse files Browse the repository at this point in the history
  • Loading branch information
NimaAra committed Jan 27, 2018
1 parent dc8ec98 commit d522062
Show file tree
Hide file tree
Showing 12 changed files with 117 additions and 115 deletions.
1 change: 0 additions & 1 deletion Easy.Storage.Common/Attributes/AliasAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ public sealed class AliasAttribute : Attribute
/// <summary>
/// Creates an instance of the <see cref="AliasAttribute"/>.
/// </summary>
/// <param name="name"></param>
public AliasAttribute(string name)
{
Name = Ensure.NotNullOrEmptyOrWhiteSpace(name, "Alias cannot be null or empty or whitespace");
Expand Down
18 changes: 16 additions & 2 deletions Easy.Storage.Common/Attributes/KeyAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,22 @@ namespace Easy.Storage.Common.Attributes
using System;

/// <summary>
/// Used to mark a given property as the key of the model.
/// Used to mark a given property as the key and/or identity of the model.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class KeyAttribute : Attribute { }
public sealed class KeyAttribute : Attribute
{
/// <summary>
/// Creates an instance of the <see cref="KeyAttribute"/>.
/// </summary>
/// <param name="isIdentity">
/// The flag indicating whether the key acts also as the identity.
/// </param>
public KeyAttribute(bool isIdentity = true) => IsIdentity = isIdentity;

/// <summary>
/// Gets the flag indicating whether the key acts also as the identity.
/// </summary>
public bool IsIdentity { get; }
}
}
23 changes: 4 additions & 19 deletions Easy.Storage.Common/DBContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ namespace Easy.Storage.Common
using System.Threading.Tasks;
using Easy.Common;
using Easy.Common.Extensions;
using Easy.Storage.Common.Attributes;
using Easy.Storage.Common.Extensions;
using Easy.Storage.Common.Filter;

Expand Down Expand Up @@ -121,18 +120,11 @@ public async Task<IList<T>> GetWhere(Filter<T> filter, IDbTransaction transactio
/// Inserts the given <paramref name="item"/> to the storage.
/// </summary>
/// <param name="item">The item to be inserted.</param>
/// <param name="modelHasIdentityColumn">
/// The flag indicating whether the model has an identity column specified. If <c>True</c>,
/// The property of the model named <c>ID</c> (or marked with the <see cref="KeyAttribute"/>)
/// will not be inserted as it's value will be generated by the backing database. If this
/// flag is set to <c>False</c>, then every property value not marked with the <see cref="IgnoreAttribute"/>
/// will be stored into the database.
/// </param>
/// <param name="transaction">The transaction</param>
/// <returns>The inserted id of the <paramref name="item"/>.</returns>
public async Task<object> Insert(T item, bool modelHasIdentityColumn = true, IDbTransaction transaction = null)
public async Task<object> Insert(T item, IDbTransaction transaction = null)
{
var insertSql = modelHasIdentityColumn ? Table.InsertIdentity : Table.InsertAll;
var insertSql = Table.HasIdentityColumn ? Table.InsertIdentity : Table.InsertAll;
return (await Connection.QueryAsync<dynamic>(insertSql, item, transaction, buffered: true)
.ConfigureAwait(false)).First().Id;
}
Expand All @@ -141,23 +133,16 @@ public async Task<object> Insert(T item, bool modelHasIdentityColumn = true, IDb
/// Inserts the given <paramref name="items"/> to the storage.
/// </summary>
/// <param name="items">The items to be inserted.</param>
/// <param name="modelHasIdentityColumn">
/// The flag indicating whether the model has an identity column specified. If <c>True</c>,
/// The property of the model named <c>ID</c> (or marked with the <see cref="KeyAttribute"/>)
/// will not be inserted as it's value will be generated by the backing database. If this
/// flag is set to <c>False</c>, then every property value not marked with the <see cref="IgnoreAttribute"/>
/// will be stored into the database.
/// </param>
/// <param name="transaction">The transaction</param>
/// <returns>The number of inserted records.</returns>
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
public Task<int> Insert(IEnumerable<T> items, bool modelHasIdentityColumn = true, IDbTransaction transaction = null)
public Task<int> Insert(IEnumerable<T> items, IDbTransaction transaction = null)
{
Ensure.NotNull(items, nameof(items));

if (!items.Any()) { return _cachedZeroTask; }

var insertSql = modelHasIdentityColumn ? Table.InsertIdentity : Table.InsertAll;
var insertSql = Table.HasIdentityColumn ? Table.InsertIdentity : Table.InsertAll;
return Connection.ExecuteAsync(insertSql, items, transaction);
}

Expand Down
19 changes: 2 additions & 17 deletions Easy.Storage.Common/IDBContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Data;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Easy.Storage.Common.Attributes;
using Easy.Storage.Common.Filter;

/// <summary>
Expand Down Expand Up @@ -66,31 +65,17 @@ public interface IDBContext<T>
/// Inserts the given <paramref name="item"/> to the storage.
/// </summary>
/// <param name="item">The item to be inserted.</param>
/// <param name="modelHasIdentityColumn">
/// The flag indicating whether the model has an identity column specified. If <c>True</c>,
/// The property of the model named <c>ID</c> (or marked with the <see cref="KeyAttribute"/>)
/// will not be inserted as it's value will be generated by the backing database. If this
/// flag is set to <c>False</c>, then every property value not marked with the <see cref="IgnoreAttribute"/>
/// will be stored into the database.
/// </param>
/// <param name="transaction">The transaction</param>
/// <returns>The inserted id of the <paramref name="item"/>.</returns>
Task<dynamic> Insert(T item, bool modelHasIdentityColumn = true, IDbTransaction transaction = null);
Task<dynamic> Insert(T item, IDbTransaction transaction = null);

/// <summary>
/// Inserts the given <paramref name="items"/> to the storage.
/// </summary>
/// <param name="items">The items to be inserted.</param>
/// <param name="modelHasIdentityColumn">
/// The flag indicating whether the model has an identity column specified. If <c>True</c>,
/// The property of the model named <c>ID</c> (or marked with the <see cref="KeyAttribute"/>)
/// will not be inserted as it's value will be generated by the backing database. If this
/// flag is set to <c>False</c>, then every property value not marked with the <see cref="IgnoreAttribute"/>
/// will be stored into the database.
/// </param>
/// <param name="transaction">The transaction</param>
/// <returns>The number of inserted records.</returns>
Task<int> Insert(IEnumerable<T> items, bool modelHasIdentityColumn = true, IDbTransaction transaction = null);
Task<int> Insert(IEnumerable<T> items, IDbTransaction transaction = null);

/// <summary>
/// Inserts every specified columns in the given <paramref name="item"/> as the record.
Expand Down
41 changes: 29 additions & 12 deletions Easy.Storage.Common/Table.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
/// </summary>
public sealed class Table
{
private static readonly Type IdentityType = typeof(KeyAttribute);
private static readonly Type KeyAttributeType = typeof(KeyAttribute);
private static readonly ConcurrentDictionary<TableKey, Table> Cache =
new ConcurrentDictionary<TableKey, Table>();

Expand All @@ -30,8 +30,9 @@ internal static Table MakeOrGet<T>(Dialect dialect, string name)
internal readonly HashSet<string> IgnoredProperties;
internal readonly Dictionary<PropertyInfo, string> PropertyToColumns;
internal readonly Dictionary<string, string> PropertyNamesToColumns;
internal readonly bool HasIdentityColumn;
internal readonly PropertyInfo IdentityColumn;

private Table(TableKey key, string tableName)
{
Dialect = key.Dialect;
Expand All @@ -45,19 +46,24 @@ private Table(TableKey key, string tableName)
Name = tableName.GetAsEscapedSQLName();

var props = key.Type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
IdentityColumn = GetIdentityColumn(ModelType, props);

PropertyToColumns = GetPropertiesToColumnsMappings(props, out HashSet<string> ignoredProperties);
PropertyNamesToColumns = PropertyToColumns.ToDictionary(kv => kv.Key.Name, kv => kv.Value);

IgnoredProperties = ignoredProperties;

Select = Dialect.GetSelectQuery(this);
Delete = Dialect.GetDeleteQuery(this);

GetKeyProperty(props, out IdentityColumn, out HasIdentityColumn);

UpdateAll = Dialect.GetUpdateQuery(this, true);
UpdateIdentity = Dialect.GetUpdateQuery(this, false);
InsertAll = Dialect.GetInsertQuery(this, true);
InsertIdentity = Dialect.GetInsertQuery(this, false);

if (HasIdentityColumn)
{
UpdateIdentity = Dialect.GetUpdateQuery(this, false);
InsertIdentity = Dialect.GetInsertQuery(this, false);
}
}

/// <summary>
Expand Down Expand Up @@ -105,23 +111,34 @@ private Table(TableKey key, string tableName)
/// </summary>
public string Delete { get; }

private static PropertyInfo GetIdentityColumn(Type modelType, PropertyInfo[] props)
private static void GetKeyProperty(
PropertyInfo[] props, out PropertyInfo result, out bool isIdentity)
{
var possibleIdentityColumns = props
.Where(p => p.CustomAttributes.Any(at => at.AttributeType == IdentityType))
.Where(p => p.CustomAttributes.Any(at => at.AttributeType == KeyAttributeType))
.ToArray();

Ensure.That<InvalidOperationException>(possibleIdentityColumns.Length <= 1,
"The model can only have one property specified as the Identity.");

// A marked Identity property has precedence over default Id property
if (possibleIdentityColumns.Length == 1) { return possibleIdentityColumns[0]; }
if (possibleIdentityColumns.Length == 1)
{
result = possibleIdentityColumns[0];
isIdentity = result.GetCustomAttribute<KeyAttribute>().IsIdentity;
return;
}

var defaultIdProp = props.SingleOrDefault(p => p.Name.Equals("Id", StringComparison.Ordinal));
if (defaultIdProp != null)
{
result = defaultIdProp;
isIdentity = true;
return;
}

if (defaultIdProp != null) { return defaultIdProp; }

throw new InvalidOperationException($"The model: '{modelType.Name}' does not have a default 'Id' property specified or any of its members marked as '{IdentityType.FullName}'.");
result = null;
isIdentity = false;
}

private static string GetModelName(Type type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
<NamedTestSelector>
<TestName>Easy.Storage.Tests.Unit.SQLite.SQLiteConnections.SQLiteAttachedConnectionTests.When_querying_attached_files</TestName>
</NamedTestSelector>
<NamedTestSelector>
<TestName>Easy.Storage.Tests.Unit.SQLServer.SQLServerStorageContextTests.Run</TestName>
</NamedTestSelector>
</IgnoredTests>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
Expand Down
14 changes: 12 additions & 2 deletions Easy.Storage.Tests.Unit/Models/ModelWithNoIdOrPrimaryKey.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
namespace Easy.Storage.Tests.Unit.Models
{
internal sealed class ModelWithNoIdOrPrimaryKey
using Easy.Storage.Common.Attributes;

[Alias("Person")]
internal sealed class ModelWithKeyButNoIdentity
{
public string Text { get; set; }
[Key(false)]
public long Id { get; set; }

public string Name { get; set; }
public int Age { get; set; }

[Ignore]
public string Foo { get; set; }
}
}
14 changes: 5 additions & 9 deletions Easy.Storage.Tests.Unit/OverriddenTableTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
namespace Easy.Storage.Tests.Unit
{
using System;
using System.Collections.Generic;
using Easy.Storage.Common;
using Easy.Storage.SQLite;
Expand All @@ -16,10 +15,12 @@ internal sealed class OverrriddenTableTests
public void When_creating_table()
{
var table = Table.MakeOrGet<Person>(GenericSQLDialect.Instance, "Foo");

table.Dialect.ShouldBe(GenericSQLDialect.Instance);
table.Dialect.Type.ShouldBe(DialectType.Generic);
table.ModelType.ShouldBe(typeof(Person));
table.Name.ShouldBe("[Foo]");
table.HasIdentityColumn.ShouldBeTrue();
table.Select.ShouldBe("SELECT\r\n"
+ " [Foo].[Id] AS 'Id',\r\n"
+ " [Foo].[Name] AS 'Name',\r\n"
Expand Down Expand Up @@ -51,14 +52,6 @@ public void When_creating_table()
table.Delete.ShouldBe("DELETE FROM [Foo]\r\nWHERE\r\n 1 = 1;");
}

[Test]
public void When_creating_table_for_model_with_no_id_or_identity_attribute()
{
Should.Throw<InvalidOperationException>(
() => Table.MakeOrGet<ModelWithNoIdOrPrimaryKey>(GenericSQLDialect.Instance, "Foo"))
.Message.ShouldBe("The model: 'ModelWithNoIdOrPrimaryKey' does not have a default 'Id' property specified or any of its members marked as 'Easy.Storage.Common.Attributes.KeyAttribute'.");
}

[Test]
public void When_creating_a_table_for_model_with_a_default_id_property_generic_dialect()
{
Expand All @@ -69,6 +62,7 @@ public void When_creating_a_table_for_model_with_a_default_id_property_generic_d
table.Dialect.Type.ShouldBe(DialectType.Generic);
table.ModelType.ShouldBe(typeof(SampleModel));
table.Name.ShouldBe("[Foo]");
table.HasIdentityColumn.ShouldBeTrue();
table.PropertyNamesToColumns["Text"].ShouldBe("[Text]");
table.PropertyNamesToColumns["Guid"].ShouldBe("[Key]");
Should.Throw<KeyNotFoundException>(() => table.PropertyNamesToColumns["Composite"].ShouldBe("[Text]"))
Expand Down Expand Up @@ -152,6 +146,7 @@ public void When_creating_a_table_for_model_with_a_default_id_property_sqlite_di

table.ShouldNotBeNull();
table.Name.ShouldBe("[Foo]");
table.HasIdentityColumn.ShouldBeTrue();
table.PropertyNamesToColumns["Text"].ShouldBe("[Text]");
table.PropertyNamesToColumns["Guid"].ShouldBe("[Key]");
Should.Throw<KeyNotFoundException>(() => table.PropertyNamesToColumns["Composite"].ShouldBe("[Text]"))
Expand Down Expand Up @@ -236,6 +231,7 @@ public void When_creating_a_table_for_model_with_a_default_id_property_sqlserver

table.ShouldNotBeNull();
table.Name.ShouldBe("[Foo]");
table.HasIdentityColumn.ShouldBeTrue();
table.PropertyNamesToColumns["Text"].ShouldBe("[Text]");
table.PropertyNamesToColumns["Guid"].ShouldBe("[Key]");
Should.Throw<KeyNotFoundException>(() => table.PropertyNamesToColumns["Composite"].ShouldBe("[Text]"))
Expand Down
Loading

0 comments on commit d522062

Please sign in to comment.