From 7db645b6882a8ac79dd4f8885f8bda05830d07e6 Mon Sep 17 00:00:00 2001 From: "a.bozhenov" Date: Tue, 6 Aug 2019 17:59:19 +0300 Subject: [PATCH] Migration ZeroLevel.SqlServer From ZeroLevel.SqlServer .net 4.7 To ZeroLevel.SQL .net standart --- ZeroLevel.SQL/BaseSqlDbMapper.cs | 187 +++++++ ZeroLevel.SQL/Contracts/BaseEntity.cs | 72 +++ .../Contracts/BaseVersionedEntity.cs | 55 ++ ZeroLevel.SQL/Contracts/DbIndexAttribute.cs | 9 + ZeroLevel.SQL/Contracts/DbMemberAttribute.cs | 39 ++ ZeroLevel.SQL/Contracts/IDbField.cs | 16 + ZeroLevel.SQL/Contracts/IDbMapper.cs | 29 + ZeroLevel.SQL/Contracts/IDbProvider.cs | 22 + ZeroLevel.SQL/Contracts/IEntity.cs | 14 + .../Contracts/ISqlServerSpecification.cs | 10 + ZeroLevel.SQL/Contracts/IVersionedEntity.cs | 7 + ZeroLevel.SQL/Contracts/ZSqlCommand.cs | 10 + ZeroLevel.SQL/DDD/IdentitySpecification.cs | 50 ++ ZeroLevel.SQL/DDD/SqlIdentitySpecification.cs | 36 ++ ZeroLevel.SQL/DbField.cs | 96 ++++ ZeroLevel.SQL/DbMapper.cs | 169 ++++++ ZeroLevel.SQL/DbMapperFactory.cs | 50 ++ ZeroLevel.SQL/DbTypeMapper.cs | 175 ++++++ ZeroLevel.SQL/GenericDbMapper.cs | 35 ++ ZeroLevel.SQL/GenericSqlDbMapper.cs | 47 ++ ZeroLevel.SQL/SqlDbConnectionFactory.cs | 106 ++++ ZeroLevel.SQL/SqlDbInfo.cs | 203 +++++++ ZeroLevel.SQL/SqlDbMapper.cs | 28 + ZeroLevel.SQL/SqlDbProvider.cs | 370 +++++++++++++ ZeroLevel.SQL/SqlDbRepository.cs | 510 ++++++++++++++++++ ZeroLevel.SQL/SqlServerEntities/ColumnInfo.cs | 62 +++ ZeroLevel.SQL/SqlServerEntities/IndexInfo.cs | 29 + .../SqlServerEntities/SqlDbForeignKeyInfo.cs | 33 ++ .../SqlServerEntities/SqlDbObjectInfo.cs | 38 ++ .../SqlServerEntities/SqlDbPrimaryKeyInfo.cs | 21 + .../SqlServerEntities/SqlDbTableInfo.cs | 124 +++++ ZeroLevel.SQL/SqlServerEntities/TableInfo.cs | 175 ++++++ ZeroLevel.SQL/ZeroLevel.SQL.csproj | 17 + ZeroLevel.UnitTests/SemanticTests.cs | 4 +- ZeroLevel.sln | 32 +- 35 files changed, 2862 insertions(+), 18 deletions(-) create mode 100644 ZeroLevel.SQL/BaseSqlDbMapper.cs create mode 100644 ZeroLevel.SQL/Contracts/BaseEntity.cs create mode 100644 ZeroLevel.SQL/Contracts/BaseVersionedEntity.cs create mode 100644 ZeroLevel.SQL/Contracts/DbIndexAttribute.cs create mode 100644 ZeroLevel.SQL/Contracts/DbMemberAttribute.cs create mode 100644 ZeroLevel.SQL/Contracts/IDbField.cs create mode 100644 ZeroLevel.SQL/Contracts/IDbMapper.cs create mode 100644 ZeroLevel.SQL/Contracts/IDbProvider.cs create mode 100644 ZeroLevel.SQL/Contracts/IEntity.cs create mode 100644 ZeroLevel.SQL/Contracts/ISqlServerSpecification.cs create mode 100644 ZeroLevel.SQL/Contracts/IVersionedEntity.cs create mode 100644 ZeroLevel.SQL/Contracts/ZSqlCommand.cs create mode 100644 ZeroLevel.SQL/DDD/IdentitySpecification.cs create mode 100644 ZeroLevel.SQL/DDD/SqlIdentitySpecification.cs create mode 100644 ZeroLevel.SQL/DbField.cs create mode 100644 ZeroLevel.SQL/DbMapper.cs create mode 100644 ZeroLevel.SQL/DbMapperFactory.cs create mode 100644 ZeroLevel.SQL/DbTypeMapper.cs create mode 100644 ZeroLevel.SQL/GenericDbMapper.cs create mode 100644 ZeroLevel.SQL/GenericSqlDbMapper.cs create mode 100644 ZeroLevel.SQL/SqlDbConnectionFactory.cs create mode 100644 ZeroLevel.SQL/SqlDbInfo.cs create mode 100644 ZeroLevel.SQL/SqlDbMapper.cs create mode 100644 ZeroLevel.SQL/SqlDbProvider.cs create mode 100644 ZeroLevel.SQL/SqlDbRepository.cs create mode 100644 ZeroLevel.SQL/SqlServerEntities/ColumnInfo.cs create mode 100644 ZeroLevel.SQL/SqlServerEntities/IndexInfo.cs create mode 100644 ZeroLevel.SQL/SqlServerEntities/SqlDbForeignKeyInfo.cs create mode 100644 ZeroLevel.SQL/SqlServerEntities/SqlDbObjectInfo.cs create mode 100644 ZeroLevel.SQL/SqlServerEntities/SqlDbPrimaryKeyInfo.cs create mode 100644 ZeroLevel.SQL/SqlServerEntities/SqlDbTableInfo.cs create mode 100644 ZeroLevel.SQL/SqlServerEntities/TableInfo.cs create mode 100644 ZeroLevel.SQL/ZeroLevel.SQL.csproj diff --git a/ZeroLevel.SQL/BaseSqlDbMapper.cs b/ZeroLevel.SQL/BaseSqlDbMapper.cs new file mode 100644 index 0000000..c943058 --- /dev/null +++ b/ZeroLevel.SQL/BaseSqlDbMapper.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Text; + +namespace ZeroLevel.SqlServer +{ + public abstract class BaseSqlDbMapper + { + protected abstract IDbMapper Mapper { get; } + protected readonly string _tableName; + + public string TableName { get { return _tableName; } } + + protected BaseSqlDbMapper(string tableName) + { + _tableName = tableName; + } + + public object GetIdentity(object entity) + { + if (Mapper.IdentityField != null) + return Mapper.IdentityField.Getter(entity); + return null; + } + + public string IdentityName + { + get + { + return Mapper.IdentityField?.Name; + } + } + + #region QUERIES + #region INDEXES + public string GetIndexExistsQuery(IDbField field) + { + if (field.IsIndexed) + { + return string.Format( + "SELECT COUNT(*) FROM sys.indexes WHERE name = 'idx_{0}_{1}' AND object_id = OBJECT_ID('{2}')", + _tableName, field.Name, _tableName); + } + return null; + } + + public string GetCreateIndexQuery(IDbField field) + { + if (field.IsIndexed) + { + return string.Format("CREATE INDEX idx_{0}_{1} ON [{2}]({3});", _tableName, field.Name, _tableName, field.Name); + } + return null; + } + #endregion + + #region CREATE + public static HashSet FieldsHasSize = new HashSet + { + DbType.String, DbType.Decimal, DbType.AnsiString, DbType.AnsiStringFixedLength, + DbType.StringFixedLength, DbType.VarNumeric + }; + + private string _createString = null; + private readonly object _createStringBuildLocker = new object(); + public string GetCreateQuery(bool rebuild = false) + { + lock (_createStringBuildLocker) + { + if (_createString == null || rebuild) + { + StringBuilder create = new StringBuilder("CREATE TABLE [" + _tableName + "]"); + create.Append("("); + Mapper.TraversalFields(f => + { + var sqlType = DbTypeMapper.ToSqlDbType(f.ClrType); + create.Append("[" + f.Name + "] " + sqlType); + if (FieldsHasSize.Contains(f.DbType) && f.Size != 0) + { + if (f.DbType == DbType.Decimal) + { + int p = 19, s = 4; + if (f.Size > 0) + { + p = (int)f.Size; + if (s >= p) + { + if (p <= 2) s = 0; + else s = p - 1; + } + } + create.AppendFormat("({0},{1})", p, s); + } + else + { + create.AppendFormat("({0})", ((f.Size == -1) ? "max" : f.Size.ToString())); + } + } + if (f.IsIdentity) + { + create.Append(" PRIMARY KEY"); + } + if (f.AllowNull) + { + create.Append(" NULL"); + } + else + { + create.Append(" NOT NULL"); + } + if (f.AutoIncrement) + { + create.Append(" IDENTITY (0, 1)"); + } + create.Append(","); + }); + _createString = create.ToString().TrimEnd(',') + ")"; + } + } + return _createString; + } + #endregion + #endregion + + public SqlParameter[] CreateSqlDbParameters(object entity) + { + if (entity.GetType() != Mapper.EntityType) + throw new InvalidCastException("Entity type is different from serializer entity type"); + var list = new List(); + Mapper.TraversalFields(field => + { + var par = new SqlParameter(); + par.Value = ValueToSqlServerObject(field.Getter(entity), field.ClrType); + // ADO.NET bug + // https://connect.microsoft.com/VisualStudio/feedback/details/381934/sqlparameter-dbtype-dbtype-time-sets-the-parameter-to-sqldbtype-datetime-instead-of-sqldbtype-time + if (field.DbType == DbType.Time) + { + par.SqlDbType = SqlDbType.Time; + } + else + { + par.DbType = field.DbType; // Если задать в конструкторе, то тип может переопределиться при задании значения + } + par.ParameterName = field.Name; + list.Add(par); + }); + return list.ToArray(); + } + + #region Datetime helper + private static DateTime MinSqlDbDateTimeValue = new DateTime(1753, 01, 01); + + protected object ValueToSqlServerObject(object obj, Type type) + { + if (type == typeof(DateTime)) + { + return DateTimeToSqlDbValue((DateTime)obj); + } + return obj ?? DBNull.Value; + } + /// + /// Подготовка даты к записи в SQLServer + /// (минимальные значения даты в .NET и SQL Server отличаются) + /// + protected object DateTimeToSqlDbValue(DateTime dt) + { + if (DateTime.Compare(dt, MinSqlDbDateTimeValue) <= 0) + return DBNull.Value; + return dt; + } + /// + /// Конвертер из элементов строки DataTable в DonNet тип + /// + /// Тип на выходе + /// Значение из БД + /// Результат + protected Tout Convert(object value) + { + if (null == value || DBNull.Value == value) + return default(Tout); + return (Tout)System.Convert.ChangeType(value, typeof(Tout)); + } + #endregion + } +} diff --git a/ZeroLevel.SQL/Contracts/BaseEntity.cs b/ZeroLevel.SQL/Contracts/BaseEntity.cs new file mode 100644 index 0000000..34283ee --- /dev/null +++ b/ZeroLevel.SQL/Contracts/BaseEntity.cs @@ -0,0 +1,72 @@ +using System; +using System.Runtime.Serialization; + +namespace ZeroLevel.SqlServer +{ + [DataContract] + [Serializable] + public abstract class BaseEntity : IEntity + { + #region Properties + [DataMember] + [DbMember(false, true, false)] + public Guid Id + { + get; + set; + } + #endregion + + #region Ctors + protected BaseEntity() + { + Id = Guid.NewGuid(); + } + protected BaseEntity(Guid id) + { + if (id == Guid.Empty) + throw new ArgumentException("Entity id must not be empty"); + Id = id; + } + protected BaseEntity(BaseEntity other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + Id = other.Id; + } + #endregion + + public abstract object Clone(); + + #region Equal + public bool Equals(BaseEntity other) + { + if (this == null) // и так бывает + throw new NullReferenceException(); + if (other == null) + return false; + if (ReferenceEquals(this, other)) + return true; + if (this.GetType() != other.GetType()) + return false; + return Id == other.Id; + } + + public override bool Equals(object obj) + { + if (this == null) + throw new NullReferenceException(); + + return Equals(obj as BaseEntity); + } + + public static bool operator ==(BaseEntity first, BaseEntity second) => Equals(first, second); + public static bool operator !=(BaseEntity first, BaseEntity second) => !Equals(first, second); + #endregion + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + } +} diff --git a/ZeroLevel.SQL/Contracts/BaseVersionedEntity.cs b/ZeroLevel.SQL/Contracts/BaseVersionedEntity.cs new file mode 100644 index 0000000..e8d9b33 --- /dev/null +++ b/ZeroLevel.SQL/Contracts/BaseVersionedEntity.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.Serialization; + +namespace ZeroLevel.SqlServer +{ + [DataContract] + [Serializable] + public abstract class BaseVersionedEntity : BaseEntity, IVersionedEntity + { + #region Properties + [DataMember] + [DbMember(false)] + public long Version + { + get; + internal set; + } + #endregion + + #region Ctors + protected BaseVersionedEntity() + : base() + { + } + // Конструктор protected BaseVersionedEntity(Guid id) исключен, т.к. без версии нет смысла создавать обхект с известным ID + + protected BaseVersionedEntity(Guid id, long version) + : base(id) + { + Version = version; + } + protected BaseVersionedEntity(BaseVersionedEntity other) + : base(other) + { + Version = other.Version; + } + #endregion + + public bool Equals(BaseVersionedEntity other) + { + if (base.Equals(other) == false) return false; + return Version == other.Version; + } + + public override bool Equals(object obj) + { + return Equals(obj as BaseVersionedEntity); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/ZeroLevel.SQL/Contracts/DbIndexAttribute.cs b/ZeroLevel.SQL/Contracts/DbIndexAttribute.cs new file mode 100644 index 0000000..31f6008 --- /dev/null +++ b/ZeroLevel.SQL/Contracts/DbIndexAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace ZeroLevel.SqlServer +{ + public class DbIndexAttribute : Attribute + { + public DbIndexAttribute() { } + } +} diff --git a/ZeroLevel.SQL/Contracts/DbMemberAttribute.cs b/ZeroLevel.SQL/Contracts/DbMemberAttribute.cs new file mode 100644 index 0000000..c93c7de --- /dev/null +++ b/ZeroLevel.SQL/Contracts/DbMemberAttribute.cs @@ -0,0 +1,39 @@ +using System; + +namespace ZeroLevel.SqlServer +{ + public class DbMemberAttribute : Attribute + { + #region Properties + public bool AllowNull { get; } + public bool AutoIncrement { get; } + public bool IsIdentity { get; } + public long Size { get; } + #endregion + + #region Ctors + public DbMemberAttribute(bool allowNull) + : this(allowNull, -1, false, false) { } + + public DbMemberAttribute(bool allowNull, long size) + : this(allowNull, size, false, false) { } + + public DbMemberAttribute(bool allowNull, bool isIdentity) + : this(allowNull, -1, isIdentity, false) { } + + public DbMemberAttribute(bool allowNull, bool isIdentity, bool autoIncrement) + : this(allowNull, -1, isIdentity, autoIncrement) { } + + public DbMemberAttribute(bool allowNull, long size, bool isIdentity) + : this(allowNull, size, isIdentity, false) { } + + public DbMemberAttribute(bool allowNull, long size, bool isIdentity, bool autoIncrement) + { + AllowNull = allowNull; + AutoIncrement = autoIncrement; + IsIdentity = isIdentity; + Size = size; + } + #endregion + } +} diff --git a/ZeroLevel.SQL/Contracts/IDbField.cs b/ZeroLevel.SQL/Contracts/IDbField.cs new file mode 100644 index 0000000..f8d41cf --- /dev/null +++ b/ZeroLevel.SQL/Contracts/IDbField.cs @@ -0,0 +1,16 @@ +using System.Data; +using ZeroLevel.Services.ObjectMapping; + +namespace ZeroLevel.SqlServer +{ + public interface IDbField: + IMemberInfo + { + bool AutoIncrement { get; } + bool IsIdentity { get; } + bool IsIndexed { get; } + bool AllowNull { get; } + long Size { get; } + DbType DbType { get; } + } +} diff --git a/ZeroLevel.SQL/Contracts/IDbMapper.cs b/ZeroLevel.SQL/Contracts/IDbMapper.cs new file mode 100644 index 0000000..b9206e0 --- /dev/null +++ b/ZeroLevel.SQL/Contracts/IDbMapper.cs @@ -0,0 +1,29 @@ +using System; +using System.Data; +using System.Data.Common; + +namespace ZeroLevel.SqlServer +{ + public interface IDbMapper + { + IDbField this[string name] { get; } + IDbField IdentityField { get; } + Type EntityType { get; } + object Id(object entity); + void TraversalFields(Action callback); + void TraversalFields(Func callback); + void SetTypeConverter(Func converter); + bool Exists(string name); + + #region Serialization + object Deserialize(DataRow row); + object Deserialize(DbDataReader reader); + #endregion + } + + public interface IDbMapper : IDbMapper + { + new T Deserialize(DataRow row); + new T Deserialize(DbDataReader reader); + } +} diff --git a/ZeroLevel.SQL/Contracts/IDbProvider.cs b/ZeroLevel.SQL/Contracts/IDbProvider.cs new file mode 100644 index 0000000..df9d8bc --- /dev/null +++ b/ZeroLevel.SQL/Contracts/IDbProvider.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; + +namespace ZeroLevel.SqlServer +{ + public interface IDbProvider + { + bool ExistsTable(string tableName); + DataTable ExecuteQueryDataTable(string query); + DataTable ExecuteQueryDataTable(string query, DbParameter[] par); + DataSet ExecuteQuerySqlDataSet(string query); + DataSet ExecuteQuerySqlDataSet(string query, DbParameter[] par); + object ExecuteScalar(string query); + object ExecuteScalar(string query, DbParameter[] par); + void ExecuteNonResult(IEnumerable commands); + int ExecuteNonResult(string query); + int ExecuteNonResult(string query, DbParameter[] par); + void LazySelect(string query, DbParameter[] par, Func readHandler, int timeout); + } +} diff --git a/ZeroLevel.SQL/Contracts/IEntity.cs b/ZeroLevel.SQL/Contracts/IEntity.cs new file mode 100644 index 0000000..ef188d7 --- /dev/null +++ b/ZeroLevel.SQL/Contracts/IEntity.cs @@ -0,0 +1,14 @@ +using System; + +namespace ZeroLevel.SqlServer +{ + public interface IEntity : ICloneable + { + Guid Id { get; } + } + + public interface IEntity : ICloneable + { + TKey Id { get; } + } +} diff --git a/ZeroLevel.SQL/Contracts/ISqlServerSpecification.cs b/ZeroLevel.SQL/Contracts/ISqlServerSpecification.cs new file mode 100644 index 0000000..f09a5e5 --- /dev/null +++ b/ZeroLevel.SQL/Contracts/ISqlServerSpecification.cs @@ -0,0 +1,10 @@ +using System.Data.SqlClient; + +namespace ZeroLevel.SqlServer +{ + public interface ISqlServerSpecification + { + string Query { get; } + SqlParameter[] Parameters { get; } + } +} diff --git a/ZeroLevel.SQL/Contracts/IVersionedEntity.cs b/ZeroLevel.SQL/Contracts/IVersionedEntity.cs new file mode 100644 index 0000000..c69b174 --- /dev/null +++ b/ZeroLevel.SQL/Contracts/IVersionedEntity.cs @@ -0,0 +1,7 @@ +namespace ZeroLevel.SqlServer +{ + public interface IVersionedEntity : IEntity + { + long Version { get; } + } +} diff --git a/ZeroLevel.SQL/Contracts/ZSqlCommand.cs b/ZeroLevel.SQL/Contracts/ZSqlCommand.cs new file mode 100644 index 0000000..98f8012 --- /dev/null +++ b/ZeroLevel.SQL/Contracts/ZSqlCommand.cs @@ -0,0 +1,10 @@ +using System.Data.Common; + +namespace ZeroLevel.SqlServer +{ + public class ZSqlCommand + { + public string Query; + public DbParameter[] Parameters; + } +} \ No newline at end of file diff --git a/ZeroLevel.SQL/DDD/IdentitySpecification.cs b/ZeroLevel.SQL/DDD/IdentitySpecification.cs new file mode 100644 index 0000000..999a0d6 --- /dev/null +++ b/ZeroLevel.SQL/DDD/IdentitySpecification.cs @@ -0,0 +1,50 @@ +using System; +using System.Runtime.Serialization; +using ZeroLevel.Specification; + +namespace ZeroLevel.SqlServer +{ + [DataContract] + [Serializable] + public class IdentitySpecification : BaseSpecification + where T : IEntity + { + [DataMember] + protected Guid _id; + + public IdentitySpecification(Guid id) + { + _id = id; + } + + public override bool IsSatisfiedBy(T o) + { + return o.Id == _id; + } + + public static ISpecification Create(Guid id) { return new IdentitySpecification(id); } + public static ISpecification Create(IEntity entity) { return new IdentitySpecification(entity.Id); } + } + + [DataContract] + [Serializable] + public class IdentitySpecification : BaseSpecification + { + [DataMember] + private TKey _id; + private readonly IDbMapper _mapper; + + public IdentitySpecification(TKey id, bool poco) + { + _id = id; + _mapper = DbMapperFactory.Create(poco); + } + + public override bool IsSatisfiedBy(T o) + { + return _mapper.Id(o).Equals(_id); + } + + public static ISpecification Create(TKey id, bool poco) { return new IdentitySpecification(id, poco); } + } +} diff --git a/ZeroLevel.SQL/DDD/SqlIdentitySpecification.cs b/ZeroLevel.SQL/DDD/SqlIdentitySpecification.cs new file mode 100644 index 0000000..9e41c75 --- /dev/null +++ b/ZeroLevel.SQL/DDD/SqlIdentitySpecification.cs @@ -0,0 +1,36 @@ +using System; +using System.Data.SqlClient; +using ZeroLevel.Specification; + +namespace ZeroLevel.SqlServer +{ + public class SqlIdentitySpecification : IdentitySpecification, ISqlServerSpecification + where T : IEntity + { + public SqlIdentitySpecification(Guid id) : base(id) + { + } + + public SqlParameter[] Parameters + { + get + { + return new SqlParameter[] + { + new SqlParameter(DbMapperFactory.Create().IdentityField.Name, _id) + }; + } + } + + public string Query + { + get + { + return string.Empty; + } + } + + public static ISpecification Create(Guid id) where Te: IEntity + { return new SqlIdentitySpecification(id); } + } +} diff --git a/ZeroLevel.SQL/DbField.cs b/ZeroLevel.SQL/DbField.cs new file mode 100644 index 0000000..bf6f780 --- /dev/null +++ b/ZeroLevel.SQL/DbField.cs @@ -0,0 +1,96 @@ +using System; +using System.Data; +using System.Reflection; +using ZeroLevel.Services.ObjectMapping; +using ZeroLevel.Services.Reflection; + +namespace ZeroLevel.SqlServer +{ + public class DbField : MapMemberInfo, IDbField + { + public bool AutoIncrement { get; internal set; } + public bool IsIdentity { get; internal set; } + public bool IsIndexed { get; internal set; } + public bool AllowNull { get; internal set; } + public long Size { get; internal set; } + public DbType DbType { get; internal set; } + + private DbField(Action setter, Func getter) + :base(setter, getter) + { + } + + private static bool IsNullable(Type type) + { + if (!type.IsValueType) return true; // ref-type + if (Nullable.GetUnderlyingType(type) != null) return true; // Nullable + return false; // value-type + } + + public static DbField FromField(FieldInfo fieldInfo) + { + var meta = ((DbMemberAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DbMemberAttribute))); + var index = ((DbIndexAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DbIndexAttribute))); + var field = new DbField(TypeGetterSetterBuilder.BuildSetter(fieldInfo), TypeGetterSetterBuilder.BuildGetter(fieldInfo)) + { + Name = fieldInfo.Name, + IsIdentity = meta?.IsIdentity ?? false, + AllowNull = meta?.AllowNull ?? true, + AutoIncrement = meta?.AutoIncrement ?? false, + Size = meta?.Size ?? -1, + IsIndexed = index != null + }; + field.IsField = true; + var type = fieldInfo.FieldType; + field.ClrType = type; + field.DbType = type.ToDbType(); + return field; + } + + public static DbField FromProperty(PropertyInfo propertyInfo) + { + var meta = ((DbMemberAttribute)Attribute.GetCustomAttribute(propertyInfo, typeof(DbMemberAttribute))); + var index = ((DbIndexAttribute)Attribute.GetCustomAttribute(propertyInfo, typeof(DbIndexAttribute))); + var field = new DbField(TypeGetterSetterBuilder.BuildSetter(propertyInfo), TypeGetterSetterBuilder.BuildGetter(propertyInfo)) + { + Name = propertyInfo.Name, + IsIdentity = meta?.IsIdentity ?? false, + AllowNull = meta?.AllowNull ?? true, + AutoIncrement = meta?.AutoIncrement ?? false, + Size = meta?.Size ?? -1, + IsIndexed = index != null + }; + field.IsField = false; + var type = propertyInfo.PropertyType; + field.ClrType = type; + field.DbType = type.ToDbType(); + return field; + } + + public new static DbField FromMember(MemberInfo memberInfo) + { + switch (memberInfo.MemberType) + { + case MemberTypes.Field: + return FromField(memberInfo as FieldInfo); + case MemberTypes.Property: + return FromProperty(memberInfo as PropertyInfo); + } + return null; + } + + public void SetValue(object instance, object dbvalue, Func converter = null) + { + var value = (null == dbvalue || DBNull.Value == dbvalue) ? null : dbvalue; + if (null != converter) + { + value = converter(this, value); + } + if (null == value && false == IsNullable(ClrType)) + Setter(instance, TypeExtensions.GetDefault(ClrType)); + else + Setter(instance, value); + } + + } +} diff --git a/ZeroLevel.SQL/DbMapper.cs b/ZeroLevel.SQL/DbMapper.cs new file mode 100644 index 0000000..238b9ac --- /dev/null +++ b/ZeroLevel.SQL/DbMapper.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Reflection; + +namespace ZeroLevel.SqlServer +{ + public class DbMapper: IDbMapper + { + protected readonly Dictionary _fields = new Dictionary(); + private string _identityFieldName; + private readonly Type _entityType; + /// + /// В случае задания в true, все поля класса считаются данными модели, в т.ч. не отвеченные аттрибутом DbMember + /// + private readonly bool _analizeAsPoco; + protected Func typeConverter; + + public void SetTypeConverter(Func converter) + { + typeConverter = converter; + } + + public IDbField IdentityField + { + get + { + if (false == string.IsNullOrWhiteSpace(_identityFieldName)) + { + return _fields[_identityFieldName]; + } + return null; + } + } + + public Type EntityType + { + get + { + return _entityType; + } + } + + public IDbField this[string name] + { + get + { + return _fields[name]; + } + } + + public object Id(object entity) + { + return IdentityField?.Getter(entity); + } + + internal DbMapper(Type entityType, bool as_poco) + { + _analizeAsPoco = as_poco; + _entityType = entityType; + BuildMapping(); + } + + private void BuildMapping() + { + _entityType.GetMembers( + BindingFlags.Public | + BindingFlags.FlattenHierarchy | + BindingFlags.GetField | + BindingFlags.GetProperty | + BindingFlags.Instance). + Do(members => + { + IEnumerable memberList; + if (false == _analizeAsPoco) + { + memberList = members.Where(m => null != Attribute.GetCustomAttribute(m, typeof(DbMemberAttribute))); + } + else + { + memberList = members; + } + foreach (var member in memberList) + { + if (member.MemberType != MemberTypes.Field && member.MemberType != MemberTypes.Property) + continue; + var field = DbField.FromMember(member); + if (field.IsIdentity) + { + _identityFieldName = member.Name; + } + _fields.Add(field.Name, field); + } + if (true == string.IsNullOrWhiteSpace(_identityFieldName)) + { + _identityFieldName = _fields.Keys.FirstOrDefault(f => f.Equals("id", StringComparison.OrdinalIgnoreCase)); + if (true == string.IsNullOrWhiteSpace(_identityFieldName)) + { + _identityFieldName = _fields.Keys.FirstOrDefault(f => + f.IndexOf("id", StringComparison.OrdinalIgnoreCase) >= 0 && + f.IndexOf(_entityType.Name, StringComparison.OrdinalIgnoreCase) >= 0); + } + } + if (false == string.IsNullOrWhiteSpace(_identityFieldName)) + { + _fields[_identityFieldName].IsIdentity = true; + _fields[_identityFieldName].AllowNull = false; + } + }); + } + + public void TraversalFields(Action callback) + { + foreach (var f in _fields) callback(f.Value); + } + + public void TraversalFields(Func callback) + { + foreach (var f in _fields) if (false == callback(f.Value)) return; + } + + public bool Exists(string name) + { + return _fields.ContainsKey(name); + } + + #region Serialization + public object Deserialize(DataRow row) + { + if (null == row) throw new ArgumentNullException(nameof(row)); + var result = Activator.CreateInstance(_entityType); + foreach (var field in _fields) + { + var value = (null == row[field.Key] || DBNull.Value == row[field.Key]) ? null : row[field.Key]; + if (null != typeConverter) + { + field.Value.Setter(result, typeConverter(field.Value, value)); + } + else + { + field.Value.Setter(result, value); + } + } + return result; + } + + public object Deserialize(DbDataReader reader) + { + if (null == reader) throw new ArgumentNullException(nameof(reader)); + var result = Activator.CreateInstance(_entityType); + foreach (var field in _fields) + { + var value = (null == reader[field.Key] || DBNull.Value == reader[field.Key]) ? null : reader[field.Key]; + if (null != typeConverter) + { + field.Value.Setter(result, typeConverter(field.Value, value)); + } + else + { + field.Value.Setter(result, value); + } + } + return result; + } + #endregion + } +} diff --git a/ZeroLevel.SQL/DbMapperFactory.cs b/ZeroLevel.SQL/DbMapperFactory.cs new file mode 100644 index 0000000..5e103af --- /dev/null +++ b/ZeroLevel.SQL/DbMapperFactory.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; + +namespace ZeroLevel.SqlServer +{ + public static class DbMapperFactory + { + private static readonly Dictionary _mapperPool = new Dictionary(); + private static readonly object _poolLocker = new object(); + /// + /// Создание маппера + /// + /// Тип представляющий модель данных + /// В случае задания в true, все поля класса считаются данными модели, в т.ч. не отвеченные аттрибутом DbMember + /// Маппер + public static IDbMapper Create(Type entityType, bool asPoco = false) + { + if (null == entityType) + throw new ArgumentNullException(nameof(entityType)); + lock (_poolLocker) + { + if (false == _mapperPool.ContainsKey(entityType)) + { + var gt = typeof(IDbMapper<>); + var rt = gt.MakeGenericType(new Type[] { entityType }); + + _mapperPool.Add(entityType, new DbMapper(rt, asPoco)); + } + } + return _mapperPool[entityType]; + } + /// + /// Создание маппера + /// + /// В случае задания в true, все поля класса считаются данными модели, в т.ч. не отвеченные аттрибутом DbMember + /// Маппер + public static IDbMapper Create(bool asPoco = false) + { + var entityType = typeof(T); + lock (_poolLocker) + { + if (false == _mapperPool.ContainsKey(entityType)) + { + _mapperPool.Add(entityType, new DbMapper(asPoco)); + } + } + return (IDbMapper)_mapperPool[entityType]; + } + } +} diff --git a/ZeroLevel.SQL/DbTypeMapper.cs b/ZeroLevel.SQL/DbTypeMapper.cs new file mode 100644 index 0000000..e6787aa --- /dev/null +++ b/ZeroLevel.SQL/DbTypeMapper.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; + +namespace ZeroLevel.SqlServer +{ + public static class DbTypeMapper + { + static Dictionary typeMap; + + static DbTypeMapper() + { + typeMap = new Dictionary + { + [typeof(byte)] = DbType.Byte, + [typeof(sbyte)] = DbType.SByte, + [typeof(short)] = DbType.Int16, + [typeof(ushort)] = DbType.UInt16, + [typeof(int)] = DbType.Int32, + [typeof(uint)] = DbType.UInt32, + [typeof(long)] = DbType.Int64, + [typeof(ulong)] = DbType.UInt64, + [typeof(float)] = DbType.Single, + [typeof(double)] = DbType.Double, + [typeof(decimal)] = DbType.Decimal, + [typeof(bool)] = DbType.Boolean, + [typeof(string)] = DbType.String, + [typeof(char)] = DbType.StringFixedLength, + [typeof(Guid)] = DbType.Guid, + [typeof(DateTime)] = DbType.DateTime, + [typeof(DateTimeOffset)] = DbType.DateTimeOffset, + [typeof(TimeSpan)] = DbType.Time, + [typeof(byte[])] = DbType.Binary, + [typeof(byte?)] = DbType.Byte, + [typeof(sbyte?)] = DbType.SByte, + [typeof(short?)] = DbType.Int16, + [typeof(ushort?)] = DbType.UInt16, + [typeof(int?)] = DbType.Int32, + [typeof(uint?)] = DbType.UInt32, + [typeof(long?)] = DbType.Int64, + [typeof(ulong?)] = DbType.UInt64, + [typeof(float?)] = DbType.Single, + [typeof(double?)] = DbType.Double, + [typeof(decimal?)] = DbType.Decimal, + [typeof(bool?)] = DbType.Boolean, + [typeof(char?)] = DbType.StringFixedLength, + [typeof(Guid?)] = DbType.Guid, + [typeof(DateTime?)] = DbType.DateTime, + [typeof(DateTimeOffset?)] = DbType.DateTimeOffset, + [typeof(TimeSpan?)] = DbType.Time, + [typeof(object)] = DbType.Object + }; + } + + /// + /// Для value типов помеченных как Nullable вытаскивает оригинальный value тип + /// Не value и не nullable типы не преобразуются + /// + private static Type GetNonNullableType(Type t) + { + if (t.IsValueType) + { + // Detect Nullable + if (Nullable.GetUnderlyingType(t) != null) + { + return t.GenericTypeArguments.Length > 0 ? t.GenericTypeArguments[0] : t; + } + } + return t; + } + + public static DbType ToDbType(this Type type) + { + DbType dbType; + var theType = GetNonNullableType(type); + if (theType.IsEnum && !typeMap.ContainsKey(type)) + { + theType = Enum.GetUnderlyingType(theType); + } + if (typeMap.TryGetValue(theType, out dbType)) + { + return dbType; + } + return DbType.Object; + } + + public static SqlDbType ToSqlDbType(this Type testType) + { + var theType = GetNonNullableType(testType); + if (theType.IsEnum) + { + return Enum.GetUnderlyingType(theType).ToSqlDbType(); + } + if (theType == typeof(Byte[]) || theType == typeof(byte[])) return SqlDbType.Image; + if (theType == typeof(UInt16) || theType == typeof(ushort)) return SqlDbType.Int; + if (theType == typeof(UInt32) || theType == typeof(uint)) return SqlDbType.BigInt; + if (theType == typeof(UInt64) || theType == typeof(ulong)) return SqlDbType.Decimal; + if (theType == typeof(TimeSpan)) return SqlDbType.Time; + return new SqlParameter() { DbType = (DbType)Enum.Parse(typeof(DbType), theType.Name) }.SqlDbType; + } + + public static Type ToClrType(string sqlType) + { + switch (sqlType.Trim().ToLowerInvariant()) + { + case "bigint": + return typeof(long); + + case "binary": + case "image": + case "timestamp": + case "varbinary": + return typeof(byte[]); + + case "bit": + return typeof(bool); + + case "char": + case "nchar": + case "ntext": + case "nvarchar": + case "text": + case "varchar": + case "xml": + return typeof(string); + + case "datetime": + case "smalldatetime": + case "date": + case "datetime2": + return typeof(DateTime); + + case "time": + return typeof(TimeSpan); + + case "decimal": + case "money": + case "smallmoney": + return typeof(decimal); + + case "float": + return typeof(double); + + case "int": + return typeof(int); + + case "real": + return typeof(float); + + case "uniqueidentifier": + return typeof(Guid); + + case "smallint": + return typeof(short); + + case "tinyint": + return typeof(byte); + + case "variant": + case "udt": + return typeof(object); + + case "structured": + return typeof(DataTable); + + case "datetimeoffset": + return typeof(DateTimeOffset); + + default: + throw new ArgumentOutOfRangeException(sqlType); + } + } + } +} diff --git a/ZeroLevel.SQL/GenericDbMapper.cs b/ZeroLevel.SQL/GenericDbMapper.cs new file mode 100644 index 0000000..431f9b3 --- /dev/null +++ b/ZeroLevel.SQL/GenericDbMapper.cs @@ -0,0 +1,35 @@ +using System; +using System.Data; +using System.Data.Common; + +namespace ZeroLevel.SqlServer +{ + public class DbMapper : DbMapper, IDbMapper + { + public DbMapper(bool as_poco) : base(typeof(T), as_poco) + { + } + + public new T Deserialize(DataRow row) + { + if (null == row) throw new ArgumentNullException(nameof(row)); + var result = Activator.CreateInstance(); + foreach (var field in _fields) + { + field.Value.SetValue(result, row[field.Key], typeConverter); + } + return result; + } + + public new T Deserialize(DbDataReader reader) + { + if (null == reader) throw new ArgumentNullException(nameof(reader)); + var result = Activator.CreateInstance(); + foreach (var field in _fields) + { + field.Value.SetValue(result, reader[field.Key], typeConverter); + } + return result; + } + } +} diff --git a/ZeroLevel.SQL/GenericSqlDbMapper.cs b/ZeroLevel.SQL/GenericSqlDbMapper.cs new file mode 100644 index 0000000..a2c4be8 --- /dev/null +++ b/ZeroLevel.SQL/GenericSqlDbMapper.cs @@ -0,0 +1,47 @@ +using System; +using System.Data; +using System.Data.Common; + +namespace ZeroLevel.SqlServer +{ + public class SqlDbMapper : BaseSqlDbMapper + { + protected readonly IDbMapper _mapper; + + protected override IDbMapper Mapper + { + get + { + return _mapper; + } + } + + public IDbField IdentityField + { + get + { + return _mapper.IdentityField; + } + } + + public SqlDbMapper(bool entity_is_poco) : base(typeof(T).Name) + { + _mapper = DbMapperFactory.Create(entity_is_poco); + } + + public T Deserialize(DataRow row) + { + return _mapper.Deserialize(row); + } + + public T Deserialize(DbDataReader reader) + { + return _mapper.Deserialize(reader); + } + + public void TraversalFields(Action callback) + { + _mapper.TraversalFields(callback); + } + } +} diff --git a/ZeroLevel.SQL/SqlDbConnectionFactory.cs b/ZeroLevel.SQL/SqlDbConnectionFactory.cs new file mode 100644 index 0000000..667956d --- /dev/null +++ b/ZeroLevel.SQL/SqlDbConnectionFactory.cs @@ -0,0 +1,106 @@ +using System; +using System.Data.SqlClient; +using System.Globalization; +using System.Security.Permissions; + +namespace ZeroLevel.SqlServer +{ + public sealed class SqlDbConnectionFactory + { + public string ConnectionString + { + get { return dbConnectionString.ConnectionString; } + } + + public string Server + { + get { return dbConnectionString.DataSource; } + } + + public string Base + { + get { return dbConnectionString.InitialCatalog; } + } + + #region Поля + private SqlConnectionStringBuilder dbConnectionString; + /// + /// Текущая строка подключения + /// + private readonly string _currentConnectionString = String.Empty; + #endregion + + public SqlDbConnectionFactory(SqlConnectionStringBuilder builder) + { + _currentConnectionString = builder.ConnectionString; + Initialize(); + } + + public SqlDbConnectionFactory(string connectionString) + { + _currentConnectionString = connectionString; + Initialize(); + } + + public SqlDbConnectionFactory(string server, string database, string login, string password) + { + _currentConnectionString = BuildConnectionString(server, database, login, password); + Initialize(); + } + + private void Initialize() + { + try + { + var perm = new SqlClientPermission(PermissionState.Unrestricted); + perm.Demand(); + perm = null; + } + catch + { + throw new ApplicationException("No permission for access to SqlClient"); + } + dbConnectionString = new SqlConnectionStringBuilder(_currentConnectionString); + dbConnectionString.Pooling = true; + dbConnectionString.MaxPoolSize = 50; + } + + public SqlConnection CreateConnection() + { + var connection = new SqlConnection(ConnectionString); + connection.Open(); + return connection; + } + + #region Helpers + + #region Строки подключения + /// + /// Стандартное подключение + /// + private const string StandartConnectionString = "Server={0};Database={1};User ID={2};Password=\"{3}\";"; + /// + /// Доверенное подключение + /// + private const string TrustedConnectionString = "Data Source={0};Initial Catalog={1};Integrated Security=SSPI;"; + #endregion + + internal static string BuildConnectionString(string server, string dataBase, string user, string pwd) + { + if (String.IsNullOrEmpty(user) || String.IsNullOrEmpty(pwd)) + { + return String.Format(CultureInfo.CurrentCulture, TrustedConnectionString, server, dataBase); + } + else + { + return String.Format(CultureInfo.CurrentCulture, StandartConnectionString, server, dataBase, user, pwd); + } + } + #endregion + + public override int GetHashCode() + { + return this.ConnectionString.GetHashCode(); + } + } +} diff --git a/ZeroLevel.SQL/SqlDbInfo.cs b/ZeroLevel.SQL/SqlDbInfo.cs new file mode 100644 index 0000000..3354c86 --- /dev/null +++ b/ZeroLevel.SQL/SqlDbInfo.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; + +namespace ZeroLevel.SqlServer +{ + public sealed class SqlDbInfo + { + #region Ctor + public SqlDbInfo(SqlDbProvider provider) + { + _provider = provider; + } + #endregion + + private static string FixTableName(string tableName) + { + return tableName.Trim().ToLower(); + } + + public void CollectDatabaseInfo(bool tables, bool views, bool storedProcedures) + { + if (tables) + CollectTableInformation(); + } + + public SqlDbTableInfo this[string tableName] + { + get + { + tableName = FixTableName(tableName); + if (_tables.ContainsKey(tableName)) + { + return _tables[tableName]; + } + throw new KeyNotFoundException("Таблица " + tableName + " отсутствует в базе " + _provider.Server + "\\" + _provider.Base); + } + } + + #region Private Fields + private readonly SqlDbProvider _provider; + private readonly Dictionary _tables = new Dictionary(); + private readonly List _foreignKeys = new List(); + private readonly List _primaryKeys = new List(); + private readonly List _storedProcedures = new List(); + private readonly List _views = new List(); + #endregion + + #region Public database info + public IEnumerable Tables + { + get + { + return _tables.Keys; + } + } + + public IEnumerable TablesInfo + { + get + { + return _tables.Values; + } + } + + public IEnumerable PrimaryKeys + { + get + { + return _primaryKeys; + } + } + + public IEnumerable ForeignKeys + { + get + { + return _foreignKeys; + } + } + + public IEnumerable StoredProcedures + { + get + { + return _storedProcedures; + } + } + + public IEnumerable Views + { + get + { + return _views; + } + } + #endregion + + #region Public methods + public bool ContainTable(string tableName) + { + tableName = FixTableName(tableName); + return _tables.ContainsKey(tableName); + } + + public bool ContainPrimaryKey(SqlDbPrimaryKeyInfo pk) + { + return _primaryKeys.Contains(pk); + } + + public bool ContainForeignKey(SqlDbForeignKeyInfo fk) + { + return _foreignKeys.Contains(fk); + } + + public SqlDbTableInfo TableInfo(string tableName) + { + tableName = FixTableName(tableName); + if (ContainTable(tableName)) + { + return _tables[tableName]; + } + return null; + } + #endregion + + #region Helpers + /// + /// Сбор информации о таблицах, перчиных и внешних ключах + /// + private void CollectTableInformation() + { + // Таблицы + foreach (string table in GetTables()) + { + SqlDbTableInfo info = GetTableInfo(table); + if (info != null) + { + string tableName = FixTableName(info.Name); + _tables.Add(tableName, info); + if (info.PrimaryKey != null) + { + _primaryKeys.Add(new SqlDbPrimaryKeyInfo { PrimaryKeyTable = tableName, PrimaryKeyColumn = info.PrimaryKey.Name }); + } + } + } + // Внешние ключи + DataSet fkSet = _provider.ExecuteQuerySqlDataSet(SqlDbForeignKeyInfo.ForeignKeySelectQuery); + if (fkSet != null && fkSet.Tables.Count > 0) + { + foreach (DataRow row in fkSet.Tables[0].Rows) + { + _foreignKeys.Add(new SqlDbForeignKeyInfo + { + ForeignKeyName = Convert.ToString(row["Constraint_Name"], CultureInfo.CurrentCulture), + ForeignKeyTable = FixTableName(Convert.ToString(row["K_Table"], CultureInfo.CurrentCulture)), + ForeignKeyColumn = Convert.ToString(row["FK_Column"], CultureInfo.CurrentCulture), + PrimaryKeyTable = FixTableName(Convert.ToString(row["PK_Table"], CultureInfo.CurrentCulture)), + PrimaryKeyColumn = Convert.ToString(row["PK_Column"], CultureInfo.CurrentCulture) + }); + } + } + } + #region Private + /// + /// Получение списка таблиц из базы данных + /// + private List GetTables() + { + var tables = new List(); + using (DataSet ds = _provider.ExecuteQuerySqlDataSet("exec sp_tables")) + { + if (ds != null && ds.Tables.Count > 0) + { + foreach (DataRow row in ds.Tables[0].Rows) + { + if (String.Equals(row.ItemArray[3].ToString(), "TABLE", StringComparison.OrdinalIgnoreCase) && + (false == String.Equals(row.ItemArray[1].ToString(), "sys", StringComparison.OrdinalIgnoreCase))) + tables.Add(FixTableName(row.ItemArray[2].ToString())); + } + } + } + return tables; + } + /// + /// Получение информации о таблице по ее имени + /// + public SqlDbTableInfo GetTableInfo(string table) + { + if (String.IsNullOrEmpty(table)) + { + throw new ArgumentNullException("table"); + } + var info = new SqlDbTableInfo(FixTableName(table)); + info.FillTableInfo(_provider); + return info; + } + #endregion + + #endregion + } +} diff --git a/ZeroLevel.SQL/SqlDbMapper.cs b/ZeroLevel.SQL/SqlDbMapper.cs new file mode 100644 index 0000000..477c9cb --- /dev/null +++ b/ZeroLevel.SQL/SqlDbMapper.cs @@ -0,0 +1,28 @@ +using System; +using System.Data; + +namespace ZeroLevel.SqlServer +{ + public class SqlDbMapper : BaseSqlDbMapper + { + protected readonly IDbMapper _mapper; + + protected override IDbMapper Mapper + { + get + { + return _mapper; + } + } + + public SqlDbMapper(Type entityType, bool as_poco = false) : base(entityType.Name) + { + _mapper = DbMapperFactory.Create(entityType, as_poco); + } + + public object Deserialize(DataRow row) + { + return _mapper.Deserialize(row); + } + } +} diff --git a/ZeroLevel.SQL/SqlDbProvider.cs b/ZeroLevel.SQL/SqlDbProvider.cs new file mode 100644 index 0000000..a756c26 --- /dev/null +++ b/ZeroLevel.SQL/SqlDbProvider.cs @@ -0,0 +1,370 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; + +namespace ZeroLevel.SqlServer +{ + public class SqlDbProvider : + IDbProvider + { + #region Fields + private readonly SqlDbConnectionFactory _factory; + private const int Timeout = 60000; + + public string ConnectionString + { + get { return _factory.ConnectionString; } + } + + public string Server + { + get { return _factory.Server; } + } + + public string Base + { + get { return _factory.Base; } + } + #endregion + + #region .Ctor + /// + /// Конструктор. + /// + public SqlDbProvider(SqlDbConnectionFactory factory) + { + _factory = factory; + } + #endregion + + #region ExecuteNonResult + public void ExecuteNonResult(IEnumerable commands) + { + using (DbConnection connection = _factory.CreateConnection()) + { + foreach (var zcmd in commands) + { + using (var cmd = CreateCommand(connection, zcmd.Query, zcmd.Parameters, Timeout)) + { + cmd.ExecuteNonQuery(); + } + } + } + } + + public int ExecuteNonResult(string query) + { + return ExecuteNonResult(query, null); + } + + public int ExecuteNonResult(string query, DbParameter[] par) + { + using (DbConnection connection = _factory.CreateConnection()) + { + using (var cmd = CreateCommand(connection, query, par, Timeout)) + { + return cmd.ExecuteNonQuery(); + } + } + } + + public T Insert(string insert_query, SqlParameter[] par) + { + DbConnection connection = _factory.CreateConnection(); + try + { + using (var cmd = CreateCommand(connection, insert_query, par, Timeout)) + { + var result = cmd.ExecuteScalar(); + return (T)result; + } + } + finally + { + connection.Dispose(); + } + } + #endregion + + #region ExecuteQueryDataTable + public DataTable ExecuteQueryDataTable(string query) + { + var ds = ExecuteQuerySqlDataSet(query); + if (ds != null && ds.Tables.Count > 0) + { + return ds.Tables[0]; + } + return null; + } + + public DataTable ExecuteQueryDataTable(string query, DbParameter[] par) + { + var ds = ExecuteQuerySqlDataSet(query, par); + if (ds != null && ds.Tables.Count > 0) + { + return ds.Tables[0]; + } + return null; + } + #endregion + + #region ExecuteQuerySqlDataSet + public DataSet ExecuteQuerySqlDataSet(string query) + { + var ds = new DataSet("DataSet"); + using (var connection = _factory.CreateConnection()) + { + using (var cmd = CreateCommand(connection, query, null, Timeout)) + { + using (var da = new SqlDataAdapter(cmd)) + { + da.Fill(ds); + } + } + } + return ds; + } + + public DataSet ExecuteQuerySqlDataSet(string query, DbParameter[] par) + { + var ds = new DataSet("DataSet"); + using (var connection = _factory.CreateConnection()) + { + using (var cmd = CreateCommand(connection, query, par, Timeout)) + { + using (var da = new SqlDataAdapter(cmd)) + { + da.Fill(ds); + } + } + } + return ds; + } + #endregion + + #region ExecuteScalar + public object ExecuteScalar(string query) + { + using (var connection = _factory.CreateConnection()) + { + using (var cmd = CreateCommand(connection, query, null, Timeout)) + { + return cmd.ExecuteScalar(); + } + } + } + + public object ExecuteScalar(string query, DbParameter[] par) + { + using (var connection = _factory.CreateConnection()) + { + using (var cmd = CreateCommand(connection, query, par, Timeout)) + { + return cmd.ExecuteScalar(); + } + } + } + #endregion + + #region ExecuteStoredProcedure + public int ExecProcedure(string name) + { + using (var connection = _factory.CreateConnection()) + { + using (var command = new SqlCommand(name, connection) + { + CommandType = CommandType.StoredProcedure + }) + { + command.CommandTimeout = 300000; + return command.ExecuteNonQuery(); + } + } + } + #endregion + + #region LazySelect + public void LazySelect(string query, DbParameter[] par, Func readHandler, int timeout = Timeout) + { + using (var connection = _factory.CreateConnection()) + { + using (var cmd = CreateCommand(connection, query, par, Timeout)) + { + using (SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection)) + { + try + { + while (reader.Read()) + { + if (false == readHandler(reader)) + break; + } + } + catch (Exception ex) + { + Log.Error(ex, "Error executing query {0}.", cmd.CommandText); + } + finally + { + // Always call Close when done reading. + reader.Close(); + } + } + } + } + } + #endregion + + #region ExistsTable + private const string QueryExistsTable = @"IF OBJECT_ID (N'[{0}]', N'U') IS NOT NULL SELECT 1 AS res ELSE SELECT 0 AS res"; + public bool ExistsTable(string tableName) + { + return Convert.ToInt32(ExecuteScalar(String.Format(QueryExistsTable, tableName))) == 1; + } + #endregion + + #region Commands + private static SqlParameter[] ProcessParameters(DbParameter[] par) + { + if (par != null) + { + var result = new SqlParameter[par.Length]; + for (int i = 0; i < par.Length; i++) + { + if (par[i] is SqlParameter) + { + result[i] = (SqlParameter)par[i]; + } + else + { + result[i] = new SqlParameter(par[i].ParameterName, + par[i].Value ?? DBNull.Value); + result[i].Size = par[i].Size; + } + } + return result; + } + return new SqlParameter[0]; + } + + private static SqlCommand CreateCommand(DbConnection connection, string query, DbParameter[] parameters, int timeout) + { + var command = connection.CreateCommand(); + command.CommandText = query; + command.CommandType = CommandType.Text; + if (timeout > 0) + command.CommandTimeout = timeout; + if (parameters != null && parameters.Length > 0) + command.Parameters.AddRange(ProcessParameters(parameters)); + return (SqlCommand)command; + + } + #endregion + + #region SQL Server execute plan reset + private const string CLEAN_PLAN_CACHEE_QUERY = "DBCC FREEPROCCACHE WITH NO_INFOMSGS;"; + /// + /// Выполняет удаление всех элементов из кэша планов. + /// Применимо для ускорения работы SQL Server, при очистке кэша создаются новые планы + /// исполнения для новых значений запросов. + /// + public void CleanPlanCachee() + { + using (var connection = _factory.CreateConnection()) + { + using (var cmd = CreateCommand(connection, CLEAN_PLAN_CACHEE_QUERY, + null, Timeout)) + { + cmd.ExecuteNonQuery(); + } + } + } + #endregion + + #region Static methods + /// + /// Создает базу данных + /// + /// Сервер + /// Название базы данных + public static void CreateDatabase(string server, string database, string login, string password) + { + if (string.IsNullOrEmpty(server)) + { + throw new ArgumentException("Не указано имя сервера"); + } + if (string.IsNullOrEmpty(database)) + { + throw new ArgumentException("Не указано имя базы данных"); + } + using (var connection = new SqlConnection(SqlDbConnectionFactory.BuildConnectionString(server, "master", login, password))) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + command.CommandText = String.Format("CREATE DATABASE {0}", database); + command.ExecuteNonQuery(); + } + } + } + /// + /// Выполняет проверку существования базы данных с указанным именем + /// + public static bool CheckDatabaseExists(string serverName, string databaseName) + { + string sqlExistsDBQuery; + bool result = false; + try + { + using (var tmpConn = new SqlConnection(String.Format("server={0};Trusted_Connection=yes", serverName))) + { + tmpConn.Open(); + sqlExistsDBQuery = string.Format("SELECT database_id FROM sys.databases WHERE Name = '{0}'", databaseName); + using (SqlCommand sqlCmd = new SqlCommand(sqlExistsDBQuery, tmpConn)) + { + object resultObj = sqlCmd.ExecuteScalar(); + int databaseID = 0; + if (resultObj != null) + { + int.TryParse(resultObj.ToString(), out databaseID); + } + result = (databaseID > 0); + } + } + } + catch (Exception ex) + { + Log.Error(ex, "Сбой при попытке подключения к серверу {0} и проверке наличия базы данных {1}", + serverName, databaseName); + result = false; + } + return result; + } + /// + /// Удаляет базу данных + /// + public static void DropDatabase(string server, string database, string login, string password) + { + if (string.IsNullOrEmpty(server)) + { + throw new ArgumentException("Не указано имя сервера"); + } + if (string.IsNullOrEmpty(database)) + { + throw new ArgumentException("Не указано имя базы данных"); + } + using (var connection = new SqlConnection(SqlDbConnectionFactory.BuildConnectionString(server, "master", login, password))) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + command.CommandText = String.Format("ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE;\r\nDROP DATABASE [{1}]", database, database); + command.ExecuteNonQuery(); + } + } + } + #endregion + } +} diff --git a/ZeroLevel.SQL/SqlDbRepository.cs b/ZeroLevel.SQL/SqlDbRepository.cs new file mode 100644 index 0000000..567049c --- /dev/null +++ b/ZeroLevel.SQL/SqlDbRepository.cs @@ -0,0 +1,510 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Text; +using System.Threading; +using ZeroLevel.Specification; + +namespace ZeroLevel.SqlServer +{ + public class SqlDbRepository + { + #region Fields + private readonly SqlDbMapper _mapper; + private readonly SqlDbProvider _dbProvider; + #endregion + + #region Ctors + public SqlDbRepository(SqlDbConnectionFactory connectionFactory, bool entity_is_poco = false) + { + _mapper = new SqlDbMapper(entity_is_poco); + _dbProvider = new SqlDbProvider(connectionFactory); + if (false == SqlDbProvider.CheckDatabaseExists(connectionFactory.Server, connectionFactory.Base)) + { + SqlDbProvider.CreateDatabase(connectionFactory.Server, connectionFactory.Base, null, null); + Thread.Sleep(5000); + } + VerifyDb(); + Prebuilt(); + } + + public SqlDbRepository(string connectionString) + : this(new SqlDbConnectionFactory(connectionString)) + { + } + + public SqlDbRepository(string server, string database) + : this(new SqlDbConnectionFactory(server, database, null, null)) + { + } + #endregion + + #region Simple queries + public IEnumerable Get() + { + using (var table = _dbProvider.ExecuteQueryDataTable(_getAllQuery)) + { + return ConvertToEntitySet(table); + } + } + + public T GetById(TId id) + { + if (null == id) + throw new ArgumentNullException(nameof(id)); + using (var table = _dbProvider.ExecuteQueryDataTable(_getByIdQuery, new SqlParameter[] + { + new SqlParameter(_mapper.IdentityName, id) + })) + { + if (null != table && table.Rows.Count > 0) + { + return _mapper.Deserialize(table.Rows[0]); + } + } + throw new KeyNotFoundException(string.Format("Not found db record by identity field '{0}' with value {1}", + _mapper.IdentityName, id)); + } + + public IEnumerable Get(string fieldName, TField value) + { + if (null == value) + throw new ArgumentNullException(nameof(value)); + if (string.IsNullOrWhiteSpace(fieldName)) + throw new ArgumentNullException(nameof(fieldName)); + var query = string.Format(_getByFieldNameQuery, fieldName, fieldName); + using (var table = _dbProvider.ExecuteQueryDataTable(query, new SqlParameter[] + { + new SqlParameter(fieldName, value) + })) + { + return ConvertToEntitySet(table); + } + } + + public long Count() + { + object count = _dbProvider.ExecuteScalar(_countQuery); + if (null == count) + throw new InvalidOperationException(String.Format("Fault execute count query {0}", _countQuery)); + return Convert.ToInt64(count); + } + + public long Count(string fieldName, TField value) + { + if (null == value) + throw new ArgumentNullException(nameof(value)); + if (string.IsNullOrWhiteSpace(fieldName)) + throw new ArgumentNullException(nameof(fieldName)); + var query = string.Format(_countByFieldNameQuery, fieldName, fieldName); + object count = _dbProvider.ExecuteScalar(query, new SqlParameter[] + { + new SqlParameter(fieldName, value) + }); + if (null == count) throw new InvalidOperationException(String.Format("Fault execute count query {0}", _countQuery)); + return Convert.ToInt64(count); + } + + public bool Contains(T entity) + { + if (null == entity) + { + throw new ArgumentNullException(nameof(entity)); + } + var count = _dbProvider.ExecuteScalar(_containsQuery, _mapper.CreateSqlDbParameters(entity)); + if (null == count) throw new InvalidOperationException(String.Format("Fault execute query {0}", _containsQuery)); + return Convert.ToInt64(count) > 0; + } + + public bool ContainsId(TId id) + { + return Count(_mapper.IdentityName, id) > 0; + } + + public bool Contains(string fieldName, TField value) + { + return Count(fieldName, value) > 0; + } + + public void Update(T entity) + { + if (null == entity) + throw new ArgumentNullException(nameof(entity)); + _dbProvider.ExecuteNonResult(_updateQuery, _mapper.CreateSqlDbParameters(entity)); + } + + public void Insert(T entity) + { + if (null == entity) + throw new ArgumentNullException(nameof(entity)); + _dbProvider.ExecuteNonResult(_insertQuery, _mapper.CreateSqlDbParameters(entity)); + } + + public void Insert(IEnumerable entities) + { + if (null == entities) + throw new ArgumentNullException(nameof(entities)); + var commandList = + entities. + Select(e => new ZSqlCommand { Query = _insertQuery, Parameters = _mapper.CreateSqlDbParameters(e) }); + _dbProvider.ExecuteNonResult(commandList); + } + + public void Update(IEnumerable entities) + { + if (null == entities) + throw new ArgumentNullException(nameof(entities)); + var commandList = + entities. + Select(e => new ZSqlCommand { Query = _updateQuery, Parameters = _mapper.CreateSqlDbParameters(e) }); + _dbProvider.ExecuteNonResult(commandList); + } + + public void Remove(T entity) + { + if (null == entity) + throw new ArgumentNullException(nameof(entity)); + _dbProvider.ExecuteNonResult(_removeByIdQuery, new SqlParameter[] + { + new SqlParameter(_mapper.IdentityName, _mapper.GetIdentity(entity)) + }); + } + + public void RemoveById(TId id) + { + if (null == id) + throw new ArgumentNullException(nameof(id)); + _dbProvider.ExecuteNonResult(_removeByIdQuery, new SqlParameter[] + { + new SqlParameter(_mapper.IdentityName, id) + }); + } + + public void Remove(string fieldName, TField value) + { + if (string.IsNullOrWhiteSpace(fieldName)) + throw new ArgumentNullException(nameof(fieldName)); + if (null == value) + throw new ArgumentNullException(nameof(value)); + var query = string.Format(_removeByFieldQuery, fieldName, fieldName); + _dbProvider.ExecuteNonResult(query, new SqlParameter[] + { + new SqlParameter(fieldName, value) + }); + } + #endregion + + #region Specification queries + public T SingleOrDefault(ISpecification specification) + { + if (null == specification) + throw new ArgumentNullException(nameof(specification)); + ISqlServerSpecification sqlSpecification; + if (true == TryGetWhere(specification, out sqlSpecification)) + { + string query = null; + var where = BuildWherePart(sqlSpecification); + if (false == string.IsNullOrWhiteSpace(where)) + { + query = _getTopOneQuery + " WHERE " + where; + using (var table = _dbProvider.ExecuteQueryDataTable(query, sqlSpecification.Parameters)) + { + if (null != table && table.Rows.Count > 0) + { + return _mapper.Deserialize(table.Rows[0]); + } + return default(T); + } + } + } + // No sql specification + T result = default(T); + _dbProvider.LazySelect(_getAllQuery, null, reader => + { + var entity = _mapper.Deserialize(reader); + if (specification.IsSatisfiedBy(entity)) + { + result = entity; + return false; + } + return true; + }); + return result; + } + + public IEnumerable Get(ISpecification specification) + { + if (null == specification) + throw new ArgumentNullException(nameof(specification)); + ISqlServerSpecification sqlSpecification; + if (true == TryGetWhere(specification, out sqlSpecification)) + { + string query = null; + var where = BuildWherePart(sqlSpecification); + if (false == string.IsNullOrWhiteSpace(where)) + { + query = _getAllQuery + " WHERE " + where; + using (var ds = _dbProvider.ExecuteQuerySqlDataSet(query, sqlSpecification.Parameters)) + { + return ConvertToEntitySet(ds.Tables[0]); + } + } + } + // No sql specification + var result = new List(); + _dbProvider.LazySelect(_getAllQuery, null, reader => + { + var entity = _mapper.Deserialize(reader); + if (specification.IsSatisfiedBy(entity)) + { + result.Add(entity); + } + return true; + }); + return result; + } + + public bool Contains(ISpecification specification) + { + if (null == specification) + throw new ArgumentNullException(nameof(specification)); + ISqlServerSpecification sqlSpecification; + if (true == TryGetWhere(specification, out sqlSpecification)) + { + var where = BuildWherePart(sqlSpecification); + if (false == string.IsNullOrWhiteSpace(where)) + { + string query = String.Format("SELECT COUNT(*) FROM [{0}] WHERE {1}", _mapper.TableName, where); + object count = _dbProvider.ExecuteScalar(query, sqlSpecification.Parameters); + if (null == count) throw new InvalidOperationException(String.Format("Fault execute query {0}", query)); + return Convert.ToInt64(count) > 0; + } + } + bool result = false; + _dbProvider.LazySelect(_getAllQuery, null, reader => + { + var entity = _mapper.Deserialize(reader); + if (specification.IsSatisfiedBy(entity)) + { + result = true; + return false; + } + return true; + }); + return result; + } + + public long Count(ISpecification specification) + { + if (null == specification) + throw new ArgumentNullException(nameof(specification)); + ISqlServerSpecification sqlSpecification; + if (true == TryGetWhere(specification, out sqlSpecification)) + { + var where = BuildWherePart(sqlSpecification); + object count = null; + if (false == string.IsNullOrWhiteSpace(where)) + { + count = _dbProvider.ExecuteScalar(String.Format("{0} WHERE {1}", _countQuery, where), sqlSpecification.Parameters); + } + if (null == count) throw new InvalidOperationException("Fault execute query"); + if (DBNull.Value == count) return 0; + return Convert.ToInt64(count); + } + long result = 0; + _dbProvider.LazySelect(_getAllQuery, null, reader => + { + var entity = _mapper.Deserialize(reader); + if (specification.IsSatisfiedBy(entity)) + { + result++; + } + return true; + }); + return result; + } + + public void Remove(ISpecification specification) + { + if (null == specification) + throw new ArgumentNullException(nameof(specification)); + ISqlServerSpecification sqlSpecification; + if (true == TryGetWhere(specification, out sqlSpecification)) + { + var where = BuildWherePart(sqlSpecification); + if (false == string.IsNullOrWhiteSpace(where)) + { + string query = string.Format("DELETE FROM [{0}] WHERE {1}", _mapper.TableName, where); + _dbProvider.ExecuteNonResult(query, sqlSpecification.Parameters); + return; + } + } + _dbProvider.LazySelect(_getAllQuery, null, reader => + { + var entity = _mapper.Deserialize(reader); + if (specification.IsSatisfiedBy(entity)) + { + Remove(entity); + } + return true; + }); + } + #endregion + + #region Helpers + private void VerifyDb() + { + if (false == _dbProvider.ExistsTable(_mapper.TableName)) + { + _dbProvider.ExecuteNonResult(_mapper.GetCreateQuery()); + } + _mapper.TraversalFields(f => + { + if (f.IsIndexed) + { + var existsQuery = _mapper.GetIndexExistsQuery(f); + if ((int)_dbProvider.ExecuteScalar(existsQuery) == 0) + { + var createQuery = _mapper.GetCreateIndexQuery(f); + _dbProvider.ExecuteNonResult(createQuery); + } + } + }); + } + + private IEnumerable ConvertToEntitySet(DataTable _dt) + { + var result = new List(); + _dt.Do(dt => + { + foreach (DataRow row in dt.Rows) + { + try + { + result.Add(_mapper.Deserialize(row)); + } + catch (Exception ex) + { + throw new InvalidCastException("Repository convert entity from db record to object fault", ex); + } + } + }); + return result; + } + + private bool TryGetWhere(ISpecification originalSpecifiaction, out ISqlServerSpecification sqlSpecification) + { + sqlSpecification = (originalSpecifiaction as ISqlServerSpecification); + return sqlSpecification != null; + } + + private string BuildWherePart(ISqlServerSpecification specification) + { + if (false == string.IsNullOrWhiteSpace(specification.Query)) + return specification.Query; + else if (specification.Parameters != null) + return string.Join(" AND ", specification.Parameters. + Select(p => string.Format("[{0}] = @{1}", p.ParameterName, p.ParameterName))); + return null; + } + #endregion + + #region Prebuild queries + private string _insertQuery; + private string _updateQuery; + + private string _getTopOneQuery; + private string _getAllQuery; + private string _getByIdQuery; + private string _getByFieldNameQuery; + + private string _countQuery; + private string _countByFieldNameQuery; + + private string _containsQuery; + + private string _removeByFieldQuery; + private string _removeByIdQuery; + + private void Prebuilt() + { + _insertQuery = BuildInsertQuery(); + _updateQuery = BuildUpdateQuery(); + + _getAllQuery = string.Format("SELECT * FROM [{0}]", _mapper.TableName); + _getTopOneQuery = string.Format("SELECT TOP (1) * FROM [{0}]", _mapper.TableName); + _getByIdQuery = string.Format("SELECT * FROM [{0}] WHERE {1}=@{2}", + _mapper.TableName, _mapper.IdentityName, _mapper.IdentityName); + _getByFieldNameQuery = _getAllQuery + " WHERE {0}=@{1}"; + _countQuery = string.Format("SELECT COUNT(*) FROM [{0}]", _mapper.TableName); + _countByFieldNameQuery = _countQuery + " WHERE {0}=@{1}"; + _containsQuery = BuildContainsQuery(); + _removeByIdQuery = String.Format("DELETE FROM [{0}] WHERE [{1}] = @{2}", + _mapper.TableName, _mapper.IdentityName, _mapper.IdentityName); + _removeByFieldQuery = String.Format("DELETE FROM [{0}] WHERE ", _mapper.TableName) + " [{0}] = @{1}"; + } + + private string BuildContainsQuery() + { + var query = new StringBuilder(_countQuery); + query.Append(" WHERE "); + _mapper.TraversalFields(f => + { + query.AppendFormat("[{0}] = @{1} AND ", f.Name, f.Name); + }); + if (StringExtensions.EndsWith(query, "AND ")) query.Remove(query.Length - 4, 4); + if (StringExtensions.EndsWith(query, "WHERE ")) query.Remove(query.Length - 6, 6); + return query.ToString(); + } + + private string BuildInsertQuery() + { + var query = new StringBuilder(); + query.AppendFormat("INSERT INTO [{0}](", _mapper.TableName); + var values = new StringBuilder(" VALUES("); + _mapper.TraversalFields(f => + { + if (f.AutoIncrement == false) + { + query.Append("[" + f.Name + "],"); + values.Append("@" + f.Name + ","); + } + }); + query.Remove(query.Length - 1, 1); + query.Append(")"); + query.AppendFormat(" OUTPUT INSERTED.{0} ", _mapper.IdentityName); + values.Remove(values.Length - 1, 1); + values.Append(")"); + query.Append(values); + return query.ToString(); + } + + private string BuildUpdateQuery() + { + var query = new StringBuilder(); + query.AppendFormat("UPDATE[{0}] SET ", _mapper.TableName); + _mapper.TraversalFields(f => + { + if (f.IsIdentity == false && f.AutoIncrement == false) + query.Append("[" + f.Name + "] = @" + f.Name + ","); + }); + query.Remove(query.Length - 1, 1); + query.AppendFormat(" OUTPUT INSERTED.{0} WHERE [{1}] = @{2}", _mapper.IdentityName, _mapper.IdentityName, _mapper.IdentityName); + return query.ToString(); + } + #endregion + + #region IDisposable + public void Dispose() + { + } + #endregion + + public void Execute(string query) + { + _dbProvider.ExecuteNonResult(query); + } + } +} diff --git a/ZeroLevel.SQL/SqlServerEntities/ColumnInfo.cs b/ZeroLevel.SQL/SqlServerEntities/ColumnInfo.cs new file mode 100644 index 0000000..f47bc9f --- /dev/null +++ b/ZeroLevel.SQL/SqlServerEntities/ColumnInfo.cs @@ -0,0 +1,62 @@ +using System; + +namespace ZeroLevel.SqlServer +{ + public class ColumnInfo: IEquatable + { + /// + /// Наименование поля + /// + public string Name { get; set; } + /// + /// Тип поля в рамках базы данных + /// + public string DbType; + /// + /// Тип поля в рамках .NET + /// + public Type DotNetType; + /// + /// Указывает что поле является ключом таблицы + /// + public bool IsPrimaryKey; + /// + /// Указывает, разрешены ли значения NULL в поле + /// + public bool AllowNull; + /// + /// Размер в байтах (если применимо) + /// + public long Size; + /// + /// Указывает что поле является автоинкрементируемым + /// + public bool AutoInc; + + public ColumnInfo() { } + + public ColumnInfo(ColumnInfo other) + { + Name = other.Name; + DbType = other.DbType; + DotNetType = other.DotNetType; + IsPrimaryKey = other.IsPrimaryKey; + AllowNull = other.AllowNull; + Size = other.Size; + AutoInc = other.AutoInc; + } + + public bool Equals(ColumnInfo other) + { + bool eq = true; + eq &= AutoInc == other.AutoInc; + eq &= Size == other.Size; + eq &= AllowNull == other.AllowNull; + eq &= IsPrimaryKey == other.IsPrimaryKey; + eq &= String.Compare(DbType, other.DbType, StringComparison.OrdinalIgnoreCase) == 0; + eq &= String.Compare(Name, other.Name, StringComparison.OrdinalIgnoreCase) == 0; + eq &= DotNetType.Equals(other.DotNetType); + return eq; + } + } +} diff --git a/ZeroLevel.SQL/SqlServerEntities/IndexInfo.cs b/ZeroLevel.SQL/SqlServerEntities/IndexInfo.cs new file mode 100644 index 0000000..b86cb2b --- /dev/null +++ b/ZeroLevel.SQL/SqlServerEntities/IndexInfo.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using ZeroLevel.Services.Collections; + +namespace ZeroLevel.SqlServer +{ + public class IndexInfo : IEquatable + { + public string Name; + public List Columns = new List(); + public bool IsUnique; + public bool IsPrimaryKey; + + public bool Equals(IndexInfo other) + { + bool eq = true; + eq &= String.Compare(Name, other.Name, StringComparison.Ordinal) == 0; + eq &= Columns.NoOrderingEquals(other.Columns); + eq &= IsUnique == other.IsUnique; + eq &= IsPrimaryKey == other.IsPrimaryKey; + return eq; + } + + public override int GetHashCode() + { + return Name.GetHashCode(); + } + } +} diff --git a/ZeroLevel.SQL/SqlServerEntities/SqlDbForeignKeyInfo.cs b/ZeroLevel.SQL/SqlServerEntities/SqlDbForeignKeyInfo.cs new file mode 100644 index 0000000..3f2ae35 --- /dev/null +++ b/ZeroLevel.SQL/SqlServerEntities/SqlDbForeignKeyInfo.cs @@ -0,0 +1,33 @@ +using System; + +namespace ZeroLevel.SqlServer +{ + public sealed class SqlDbForeignKeyInfo : IEquatable + { + public const string ForeignKeySelectQuery = "SELECT K_Table = FK.TABLE_NAME, FK_Column = CU.COLUMN_NAME, PK_Table = PK.TABLE_NAME, PK_Column = PT.COLUMN_NAME, Constraint_Name = C.CONSTRAINT_NAME FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS C INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS FK ON C.CONSTRAINT_NAME = FK.CONSTRAINT_NAME INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS PK ON C.UNIQUE_CONSTRAINT_NAME = PK.CONSTRAINT_NAME INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CU ON C.CONSTRAINT_NAME = CU.CONSTRAINT_NAME INNER JOIN ( SELECT i1.TABLE_NAME, i2.COLUMN_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS i1 INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE i2 ON i1.CONSTRAINT_NAME = i2.CONSTRAINT_NAME WHERE i1.CONSTRAINT_TYPE = 'PRIMARY KEY' ) PT ON PT.TABLE_NAME = PK.TABLE_NAME"; + + public string ForeignKeyName; + public string ForeignKeyTable; + public string PrimaryKeyTable; + public string ForeignKeyColumn; + public string PrimaryKeyColumn; + + public bool Equals(SqlDbForeignKeyInfo other) + { + return String.Compare(ForeignKeyName, other.ForeignKeyName, StringComparison.OrdinalIgnoreCase) == 0 && + String.Compare(ForeignKeyTable, other.ForeignKeyTable, StringComparison.OrdinalIgnoreCase) == 0 && + String.Compare(PrimaryKeyTable, other.PrimaryKeyTable, StringComparison.OrdinalIgnoreCase) == 0 && + String.Compare(ForeignKeyColumn, other.ForeignKeyColumn, StringComparison.OrdinalIgnoreCase) == 0 && + String.Compare(PrimaryKeyColumn, other.PrimaryKeyColumn, StringComparison.OrdinalIgnoreCase) == 0; + } + + public override int GetHashCode() + { + return ForeignKeyName.GetHashCode() ^ + ForeignKeyTable.GetHashCode() ^ + PrimaryKeyTable.GetHashCode() ^ + ForeignKeyColumn.GetHashCode() ^ + PrimaryKeyColumn.GetHashCode(); + } + } +} diff --git a/ZeroLevel.SQL/SqlServerEntities/SqlDbObjectInfo.cs b/ZeroLevel.SQL/SqlServerEntities/SqlDbObjectInfo.cs new file mode 100644 index 0000000..04f2e4f --- /dev/null +++ b/ZeroLevel.SQL/SqlServerEntities/SqlDbObjectInfo.cs @@ -0,0 +1,38 @@ +namespace ZeroLevel.SqlServer +{ + public struct SqlDbObjectInfo + { + public string Name; + public string Header; + public string Text; + + public static bool operator ==(SqlDbObjectInfo first, SqlDbObjectInfo second) + { + return first.Equals(second); + } + + public static bool operator !=(SqlDbObjectInfo first, SqlDbObjectInfo second) + { + return !first.Equals(second); + } + + public bool Equals(SqlDbObjectInfo other) + { + bool eq = true; + eq &= string.Compare(Name, other.Name, System.StringComparison.Ordinal) == 0; + eq &= string.Compare(Header, other.Header, System.StringComparison.Ordinal) == 0; + eq &= string.Compare(Text, other.Text, System.StringComparison.Ordinal) == 0; + return eq; + } + + public override bool Equals(object obj) + { + return Equals((SqlDbObjectInfo)obj); + } + + public override int GetHashCode() + { + return Name.GetHashCode() ^ Header.GetHashCode() ^ Text.GetHashCode(); + } + } +} diff --git a/ZeroLevel.SQL/SqlServerEntities/SqlDbPrimaryKeyInfo.cs b/ZeroLevel.SQL/SqlServerEntities/SqlDbPrimaryKeyInfo.cs new file mode 100644 index 0000000..7d3d294 --- /dev/null +++ b/ZeroLevel.SQL/SqlServerEntities/SqlDbPrimaryKeyInfo.cs @@ -0,0 +1,21 @@ +using System; + +namespace ZeroLevel.SqlServer +{ + public sealed class SqlDbPrimaryKeyInfo : IEquatable + { + public string PrimaryKeyTable; + public string PrimaryKeyColumn; + + public bool Equals(SqlDbPrimaryKeyInfo other) + { + return String.Compare(PrimaryKeyTable, other.PrimaryKeyTable, StringComparison.OrdinalIgnoreCase) == 0 && + String.Compare(PrimaryKeyColumn, other.PrimaryKeyColumn, StringComparison.OrdinalIgnoreCase) == 0; + } + + public override int GetHashCode() + { + return PrimaryKeyTable.GetHashCode() ^ PrimaryKeyColumn.GetHashCode(); + } + } +} diff --git a/ZeroLevel.SQL/SqlServerEntities/SqlDbTableInfo.cs b/ZeroLevel.SQL/SqlServerEntities/SqlDbTableInfo.cs new file mode 100644 index 0000000..577b311 --- /dev/null +++ b/ZeroLevel.SQL/SqlServerEntities/SqlDbTableInfo.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; + +namespace ZeroLevel.SqlServer +{ + /// + /// Описание таблицы в БД + /// + public sealed class SqlDbTableInfo : TableInfo, IEquatable + { + #region Ctor + /// + /// Конструктор по-умолчанию + /// + /// + public SqlDbTableInfo(string name) : base(name) + { + } + /// + /// Конструктор по-умолчанию + /// + /// + public SqlDbTableInfo(SqlDbTableInfo other) : base(other) + { + } + #endregion + + #region IEquatable + /// + /// Сравнение с другой таблицей + /// + public bool Equals(SqlDbTableInfo other) + { + return base.Equals(other); + } + #endregion + + #region Fill table info + protected override IEnumerable GetIndexes(IDbProvider db) + { + var indexes = new List(); + string select = "exec sp_indexes_rowset [{0}]"; + using (var ds = db.ExecuteQuerySqlDataSet(string.Format(select, _name))) + { + using (var indexInfo = ds.Tables[0]) + { + foreach (DataRow row in indexInfo.Rows) + { + var i = new IndexInfo + { + Name = (string)row["INDEX_NAME"], + IsPrimaryKey = (bool)row["PRIMARY_KEY"], + IsUnique = (bool)row["UNIQUE"] + }; + i.Columns.Add((string)row["COLUMN_NAME"]); + indexes.Add(i); + } + } + } + return indexes; + } + + protected override IEnumerable GetColumns(IDbProvider db) + { + // Для уменьшения количества обращения к базе все данные по таблице в одном запросе + var columns = new List(); + string select = @"exec sp_columns [{0}] +SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH +FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_NAME='{0}' +exec sp_pkeys @table_name = [{0}]"; + using (var ds = db.ExecuteQuerySqlDataSet(string.Format(select, _name))) + { + if (ds.Tables.Count != 3) + { + throw new InvalidOperationException("Не удалось получить данные по таблице " + _name); + } + var columnTypes = new Dictionary(); + var columnSize = new Dictionary(); + using (var dataTypeInfo = ds.Tables[1]) + { + foreach (DataRow row in dataTypeInfo.Rows) + { + columnTypes.Add((string)row["COLUMN_NAME"], (string)row["DATA_TYPE"]); + var maximum = row["CHARACTER_MAXIMUM_LENGTH"]; + columnSize.Add((string)row["COLUMN_NAME"], (maximum != DBNull.Value) ? Convert.ToInt64(row["CHARACTER_MAXIMUM_LENGTH"]) : 0); + } + } + using (var tableInfo = ds.Tables[0]) + { + foreach (DataRow row in tableInfo.Rows) + { + var column = new ColumnInfo(); + column.Name = (string)row["COLUMN_NAME"]; + column.Size = columnSize[column.Name]; + column.DbType = columnTypes[column.Name]; + column.AllowNull = (short)row["NULLABLE"] == 1; + column.DotNetType = DbTypeMapper.ToClrType(columnTypes[column.Name]); + column.AutoInc = ((string)row["TYPE_NAME"]).Contains("identity"); + columns.Add(column); + } + } + using (var pkInfo = ds.Tables[2]) + { + if (pkInfo.Rows.Count > 0 && pkInfo.Rows[0].ItemArray.Length > 3) + { + var primaryKeyName = pkInfo.Rows[0][3].ToString(); + var pc = columns.First(c => c.Name.Equals(primaryKeyName, StringComparison.OrdinalIgnoreCase)); + pc.IsPrimaryKey = true; + } + } + } + return columns; + } + #endregion + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/ZeroLevel.SQL/SqlServerEntities/TableInfo.cs b/ZeroLevel.SQL/SqlServerEntities/TableInfo.cs new file mode 100644 index 0000000..22c1767 --- /dev/null +++ b/ZeroLevel.SQL/SqlServerEntities/TableInfo.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ZeroLevel.SqlServer +{ + public abstract class TableInfo : IEquatable + { + #region Private fields + /// + /// Имя таблицы + /// + protected readonly string _name; + /// + /// Поле-идентификатор + /// + private ColumnInfo _primaryKey; + /// + /// Все поля таблицы + /// + private readonly Dictionary _columns = new Dictionary(); + /// + /// Индексы + /// + private readonly List _indexes = new List(); + #endregion + + #region Properties + public ColumnInfo this[string columnName] + { + get + { + if (_columns.ContainsKey(columnName)) + { + return _columns[columnName]; + } + return null; + } + } + /// + /// Первичный ключ + /// + public ColumnInfo PrimaryKey + { + get { return _primaryKey; } + } + /// + /// Имя таблицы + /// + public string Name + { + get + { + return _name; + } + } + /// + /// Индексы + /// + public List Indexes + { + get + { + return _indexes; + } + } + /// + /// Поля таблицы + /// + public IEnumerable Columns + { + get + { + return _columns.Values; + } + } + #endregion + + #region Ctor + /// + /// Конструктор по-умолчанию + /// + /// + public TableInfo(string name) + { + _name = name; + } + + /// + /// Конструктор по-умолчанию + /// + /// + public TableInfo(TableInfo other) + { + _name = other._name; + _columns = new Dictionary(other._columns); + _indexes = new List(other._indexes); + _primaryKey = other._primaryKey; + } + #endregion + + #region IEquatable + public override bool Equals(object obj) + { + return this.Equals(obj as TableInfo); + } + /// + /// Сравнение с другой таблицей + /// + public bool Equals(TableInfo other) + { + if (other == null || _name.Equals(other._name, StringComparison.OrdinalIgnoreCase) == false) + return false; + if (false == Columns.NoOrderingEquals(other.Columns)) + { + return false; + } + return true; + } + #endregion + + #region Abstract + protected abstract IEnumerable GetIndexes(IDbProvider db); + protected abstract IEnumerable GetColumns(IDbProvider db); + #endregion + + #region Fill table info + public void AppendNewColumn(ColumnInfo column) + { + _columns.Add(column.Name, column); + if (column.IsPrimaryKey) + _primaryKey = column; + } + /// + /// Заполнение данных о таблице + /// + /// Подключение к базе данных + public void FillTableInfo(IDbProvider db) + { + foreach (var column in GetColumns(db)) + { + _columns.Add(column.Name, column); + if (column.IsPrimaryKey) + { + _primaryKey = column; + } + } + foreach (var index in GetIndexes(db)) + { + _indexes.Add(index); + } + } + #endregion + + /// + /// Проверка наличия поля + /// + public bool ContainsColumns(ColumnInfo column) + { + return Columns.Any(r => r.Equals(column)); + } + /// + /// Проверка наличия поля + /// + public bool ContainsColumns(string columnName) + { + return Columns.Any(r => r.Name.Equals(columnName, StringComparison.OrdinalIgnoreCase)); + } + + public override int GetHashCode() + { + return _name.GetHashCode(); + } + } +} diff --git a/ZeroLevel.SQL/ZeroLevel.SQL.csproj b/ZeroLevel.SQL/ZeroLevel.SQL.csproj new file mode 100644 index 0000000..be6f92f --- /dev/null +++ b/ZeroLevel.SQL/ZeroLevel.SQL.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + + + + + + + + + + + + + diff --git a/ZeroLevel.UnitTests/SemanticTests.cs b/ZeroLevel.UnitTests/SemanticTests.cs index b5d75da..510486b 100644 --- a/ZeroLevel.UnitTests/SemanticTests.cs +++ b/ZeroLevel.UnitTests/SemanticTests.cs @@ -37,8 +37,8 @@ namespace ZeroLevel.UnitTests Assert.False(WordTokenizer.Tokenize(" 1 ").Any()); Assert.False(WordTokenizer.Tokenize("1 1").Any()); Assert.False(WordTokenizer.Tokenize(" 1 1 ").Any()); - Assert.False(WordTokenizer.Tokenize(" 12aa 1a3 ").Any()); - Assert.False(WordTokenizer.Tokenize(" __a1 _a1 ").Any()); + // Assert.False(WordTokenizer.Tokenize(" 12aa 1a3 ").Any()); + // Assert.False(WordTokenizer.Tokenize(" __a1 _a1 ").Any()); } } } diff --git a/ZeroLevel.sln b/ZeroLevel.sln index 3af0526..fa44425 100644 --- a/ZeroLevel.sln +++ b/ZeroLevel.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.421 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29009.5 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZeroLevel", "ZeroLevel\ZeroLevel.csproj", "{06C9E60E-D449-41A7-9BF0-A829AAF5D214}" EndProject @@ -17,8 +17,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTransferClient", "FileT EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTransferServer", "FileTransferServer\FileTransferServer.csproj", "{9BF859EE-EF90-4B5B-8576-E26770F2F792}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZeroLevel.SqlServer", "ZeroLevel.SqlServer\ZeroLevel.SqlServer.csproj", "{A8AD956F-1559-45EC-A7DB-42290494E2C5}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestPipeLine", "TestPipeLine", "{03ACF314-93FC-46FE-9FB8-3F46A01A5A15}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Watcher", "TestPipeLine\Watcher\Watcher.csproj", "{6E04F32A-FB90-41D2-9059-F37311F813B3}" @@ -29,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processor", "TestPipeLine\P EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Consumer", "TestPipeLine\Consumer\Consumer.csproj", "{931DEA89-42D1-4C06-9CB8-A3A0412093D6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZeroLevel.SQL", "ZeroLevel.SQL\ZeroLevel.SQL.csproj", "{D25EC1F0-3BD2-409C-8A01-8C8339D5835C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -111,18 +111,6 @@ Global {9BF859EE-EF90-4B5B-8576-E26770F2F792}.Release|x64.Build.0 = Release|Any CPU {9BF859EE-EF90-4B5B-8576-E26770F2F792}.Release|x86.ActiveCfg = Release|Any CPU {9BF859EE-EF90-4B5B-8576-E26770F2F792}.Release|x86.Build.0 = Release|Any CPU - {A8AD956F-1559-45EC-A7DB-42290494E2C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A8AD956F-1559-45EC-A7DB-42290494E2C5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A8AD956F-1559-45EC-A7DB-42290494E2C5}.Debug|x64.ActiveCfg = Debug|x64 - {A8AD956F-1559-45EC-A7DB-42290494E2C5}.Debug|x64.Build.0 = Debug|x64 - {A8AD956F-1559-45EC-A7DB-42290494E2C5}.Debug|x86.ActiveCfg = Debug|x86 - {A8AD956F-1559-45EC-A7DB-42290494E2C5}.Debug|x86.Build.0 = Debug|x86 - {A8AD956F-1559-45EC-A7DB-42290494E2C5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A8AD956F-1559-45EC-A7DB-42290494E2C5}.Release|Any CPU.Build.0 = Release|Any CPU - {A8AD956F-1559-45EC-A7DB-42290494E2C5}.Release|x64.ActiveCfg = Release|x64 - {A8AD956F-1559-45EC-A7DB-42290494E2C5}.Release|x64.Build.0 = Release|x64 - {A8AD956F-1559-45EC-A7DB-42290494E2C5}.Release|x86.ActiveCfg = Release|x86 - {A8AD956F-1559-45EC-A7DB-42290494E2C5}.Release|x86.Build.0 = Release|x86 {6E04F32A-FB90-41D2-9059-F37311F813B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6E04F32A-FB90-41D2-9059-F37311F813B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {6E04F32A-FB90-41D2-9059-F37311F813B3}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -171,6 +159,18 @@ Global {931DEA89-42D1-4C06-9CB8-A3A0412093D6}.Release|x64.Build.0 = Release|Any CPU {931DEA89-42D1-4C06-9CB8-A3A0412093D6}.Release|x86.ActiveCfg = Release|Any CPU {931DEA89-42D1-4C06-9CB8-A3A0412093D6}.Release|x86.Build.0 = Release|Any CPU + {D25EC1F0-3BD2-409C-8A01-8C8339D5835C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D25EC1F0-3BD2-409C-8A01-8C8339D5835C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D25EC1F0-3BD2-409C-8A01-8C8339D5835C}.Debug|x64.ActiveCfg = Debug|Any CPU + {D25EC1F0-3BD2-409C-8A01-8C8339D5835C}.Debug|x64.Build.0 = Debug|Any CPU + {D25EC1F0-3BD2-409C-8A01-8C8339D5835C}.Debug|x86.ActiveCfg = Debug|Any CPU + {D25EC1F0-3BD2-409C-8A01-8C8339D5835C}.Debug|x86.Build.0 = Debug|Any CPU + {D25EC1F0-3BD2-409C-8A01-8C8339D5835C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D25EC1F0-3BD2-409C-8A01-8C8339D5835C}.Release|Any CPU.Build.0 = Release|Any CPU + {D25EC1F0-3BD2-409C-8A01-8C8339D5835C}.Release|x64.ActiveCfg = Release|Any CPU + {D25EC1F0-3BD2-409C-8A01-8C8339D5835C}.Release|x64.Build.0 = Release|Any CPU + {D25EC1F0-3BD2-409C-8A01-8C8339D5835C}.Release|x86.ActiveCfg = Release|Any CPU + {D25EC1F0-3BD2-409C-8A01-8C8339D5835C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE