280 lines
12 KiB
C#
280 lines
12 KiB
C#
using Npgsql;
|
||
using Npgsql.NameTranslation;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Data;
|
||
using System.Linq;
|
||
using System.Text;
|
||
|
||
namespace Kit.Helpers.SqlHelperAbstract.Postgres
|
||
{
|
||
public class SqlHelper : ISqlHelper
|
||
{
|
||
private SqlHelper() { }
|
||
|
||
public INpgsqlNameTranslator Translator { get; } = new NpgsqlSnakeCaseNameTranslator();
|
||
|
||
public static SqlHelper Instance { get; } = new SqlHelper();
|
||
|
||
private IEnumerable<KeyValuePair<string, object>> UpdateParamNames(IEnumerable<KeyValuePair<string, object>> @params)
|
||
{
|
||
var result = new List<KeyValuePair<string, object>>();
|
||
if (@params.IsNullOrEmpty()) return result;
|
||
|
||
@params.ForEach(_param =>
|
||
{
|
||
// Имя параметра транслируется в snake_case как есть (без добавления "_"):
|
||
// должно совпадать с именем параметра хранимой функции (p_doc_code и т.п.),
|
||
// т.к. вызов строится именованной нотацией: select * from fn(p_doc_code => :p_doc_code).
|
||
string key = Translator.TranslateMemberName(_param.Key.Remove(" ").Trim('@', '_'));
|
||
result.Add(new KeyValuePair<string, object>(key, _param.Value));
|
||
});
|
||
return result;
|
||
}
|
||
|
||
private string GenerateParamsForQuery(IEnumerable<KeyValuePair<string, object>> @params)
|
||
{
|
||
var sb = new StringBuilder();
|
||
if (@params.IsNullOrEmpty() == false)
|
||
{
|
||
for (int i = 0; i < @params.Count(); i++)
|
||
{
|
||
string key = @params.ElementAt(i).Key;
|
||
sb.Append(key).Append(" => :").Append(key);
|
||
|
||
if (i < @params.Count() - 1)
|
||
{
|
||
sb.Append(", ");
|
||
}
|
||
}
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
|
||
private string TranslateName(string name, bool withStrictSyntax)
|
||
{
|
||
if (withStrictSyntax)
|
||
{
|
||
return name;
|
||
}
|
||
|
||
return name.Remove("[").Remove("]").Split('.').Select(x => Translator.TranslateMemberName(x)).Join(".");
|
||
}
|
||
|
||
// Композиты, уже зарегистрированные в глобальном маппере (Npgsql 10:
|
||
// повторный MapComposite той же пары тип/имя бросает исключение).
|
||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<(Type Type, string Name), bool> MappedComposites
|
||
= new System.Collections.Concurrent.ConcurrentDictionary<(Type, string), bool>();
|
||
|
||
private NpgsqlCommand AppendTypes(NpgsqlCommand command, RequestParams requestParams)
|
||
{
|
||
requestParams.CustomTypes?.ForEach(x =>
|
||
{
|
||
string name = TranslateName(x.Name, requestParams.WithStrictSyntax);
|
||
if (MappedComposites.TryAdd((x.Type, name), true))
|
||
{
|
||
NpgsqlConnection.GlobalTypeMapper.MapComposite(x.Type, name, Translator);
|
||
}
|
||
});
|
||
return command;
|
||
}
|
||
|
||
private NpgsqlCommand CreateCommand(RequestParams requestParams)
|
||
{
|
||
string sql = TranslateName(requestParams.CommandText, requestParams.WithStrictSyntax);
|
||
|
||
bool hasParameters = requestParams.Parameters != null && requestParams.Parameters.Count() > 0;
|
||
|
||
var parameters = UpdateParamNames(requestParams.Parameters);
|
||
|
||
|
||
if (requestParams.ConnectionString.Value.IsNullOrEmpty()) throw new ArgumentNullException("connectionString");
|
||
if (sql.IsNullOrEmpty()) throw new ArgumentNullException("sql");
|
||
|
||
var connection = new NpgsqlConnection(requestParams.ConnectionString.Value);
|
||
|
||
NpgsqlCommand command = connection.CreateCommand();
|
||
command.CommandTimeout = connection.ConnectionTimeout;
|
||
if (requestParams.IsStoredProcedure)
|
||
{
|
||
sql = $"select * from {sql}({GenerateParamsForQuery(parameters)});";
|
||
}
|
||
|
||
command.CommandType = System.Data.CommandType.Text;
|
||
command.CommandText = sql;
|
||
|
||
if (parameters != null && parameters.Any())
|
||
{
|
||
foreach (var keyValuePair in parameters)
|
||
{
|
||
string key = keyValuePair.Key.Remove("@").Remove(" ").ToLower();
|
||
if (keyValuePair.Value is Guid)
|
||
{
|
||
command.Parameters.AddWithValue(key.ToLower(), keyValuePair.Value ?? Guid.Empty);
|
||
}
|
||
else
|
||
{
|
||
command.Parameters.AddWithValue(key.ToLower(), keyValuePair.Value ?? DBNull.Value);
|
||
}
|
||
}
|
||
}
|
||
|
||
return command;
|
||
}
|
||
|
||
public void ExecuteNonQuery(RequestParams requestParams)
|
||
{
|
||
NpgsqlCommand command = CreateCommand(requestParams);
|
||
using (command.Connection)
|
||
{
|
||
command.Connection.Open();
|
||
AppendTypes(command, requestParams);
|
||
command.ExecuteNonQuery();
|
||
command.Connection.Close();
|
||
}
|
||
}
|
||
|
||
public object ExecuteScalar(RequestParams requestParams)
|
||
{
|
||
object returnScalar;
|
||
|
||
NpgsqlCommand command = CreateCommand(requestParams);
|
||
using (command.Connection)
|
||
{
|
||
command.Connection.Open();
|
||
AppendTypes(command, requestParams);
|
||
returnScalar = command.ExecuteScalar();
|
||
command.Connection.Close();
|
||
}
|
||
|
||
return returnScalar;
|
||
}
|
||
|
||
public TResult ExecuteScalar<TResult>(RequestParams requestParams)
|
||
{
|
||
object returnScalar;
|
||
|
||
NpgsqlCommand command = CreateCommand(requestParams);
|
||
using (command.Connection)
|
||
{
|
||
command.Connection.Open();
|
||
AppendTypes(command, requestParams);
|
||
returnScalar = command.ExecuteScalar();
|
||
command.Connection.Close();
|
||
}
|
||
return (TResult)returnScalar;
|
||
}
|
||
|
||
private IEnumerable<TEntity> ExecuteSelectMany<TEntity>(SelectType extraSelectType, RequestParamsSelect<TEntity> requestParams)
|
||
{
|
||
if (requestParams.Converters == null || requestParams.Converters.Count() < 1) throw new ArgumentNullException("converters");
|
||
|
||
NpgsqlCommand command = CreateCommand(requestParams);
|
||
command.CommandTimeout = command.Connection.ConnectionTimeout;
|
||
|
||
IList<TEntity> selectResult = extraSelectType.CreateList<TEntity>();
|
||
|
||
using (command.Connection)
|
||
{
|
||
command.Connection.Open();
|
||
AppendTypes(command, requestParams);
|
||
using (NpgsqlTransaction tran = command.Connection.BeginTransaction())
|
||
{
|
||
using (IDataReader readerFirst = command.ExecuteReader())
|
||
{
|
||
var dataTypeName = readerFirst.GetDataTypeName(0); // Для первого столбца
|
||
if (dataTypeName == "refcursor")
|
||
{
|
||
ProcessForCursors(readerFirst, command, requestParams.Converters, selectResult, extraSelectType);
|
||
}
|
||
else
|
||
{
|
||
ProcessForSimple(readerFirst, requestParams.Converters, selectResult, extraSelectType);
|
||
}
|
||
|
||
}
|
||
tran.Commit();
|
||
}
|
||
}
|
||
return selectResult;
|
||
}
|
||
|
||
private void ProcessForSimple<TEntity>(IDataReader reader, IEnumerable<Action<IDataRecordSpecified, IList<TEntity>>> converters, IList<TEntity> selectResult, SelectType extraSelectType = SelectType.Default)
|
||
{
|
||
IDataRecordSpecified recordSpecified = new DataRecordSpecified(reader, Translator);
|
||
foreach (var converter in converters)
|
||
{
|
||
while (reader.Read())
|
||
{
|
||
converter(recordSpecified, selectResult);
|
||
}
|
||
reader.NextResult();
|
||
}
|
||
recordSpecified.ProcessExtraSelect(selectResult, extraSelectType);
|
||
}
|
||
|
||
private void CheckCursorCount(int cursorCount, int converterCount, SelectType extraSelectType)
|
||
{
|
||
switch (extraSelectType)
|
||
{
|
||
case SelectType.Page:
|
||
case SelectType.Offer:
|
||
Check.IsTrue(cursorCount >= converterCount + 1, "Количество конвертеров больше количества возвращаемых курсоров");
|
||
break;
|
||
case SelectType.Default:
|
||
default:
|
||
Check.IsTrue(cursorCount >= converterCount, "Количество конвертеров больше количества возвращаемых курсоров");
|
||
break;
|
||
}
|
||
}
|
||
|
||
private void ProcessForCursors<TEntity>(IDataReader readerFirst, NpgsqlCommand command, IEnumerable<Action<IDataRecordSpecified, IList<TEntity>>> converters, IList<TEntity> selectResult, SelectType extraSelectType = SelectType.Default)
|
||
{
|
||
var cursors = new List<string>();
|
||
while (readerFirst.Read())
|
||
{
|
||
cursors.Add(readerFirst[0].ToString());
|
||
}
|
||
readerFirst.Close();
|
||
|
||
CheckCursorCount(cursors.Count, converters.Count(), extraSelectType);
|
||
|
||
for (int i = 0; i < converters.Count(); i++)
|
||
{
|
||
command.CommandText = $"fetch all in \"{cursors[i]}\"";
|
||
command.CommandType = CommandType.Text;
|
||
|
||
using (NpgsqlDataReader reader = command.ExecuteReader())
|
||
{
|
||
IDataRecordSpecified recordSpecified = new DataRecordSpecified(reader, Translator);
|
||
while (reader.Read())
|
||
{
|
||
converters.ElementAt(i)(recordSpecified, selectResult);
|
||
}
|
||
}
|
||
}
|
||
if (extraSelectType != SelectType.Default)
|
||
{
|
||
// считывание данных постраничного вывода
|
||
command.CommandText = $"fetch all in \"{cursors.Last()}\"";
|
||
command.CommandType = CommandType.Text;
|
||
|
||
using (NpgsqlDataReader reader = command.ExecuteReader())
|
||
{
|
||
IDataRecordSpecified recordSpecified = new DataRecordSpecified(reader, Translator);
|
||
recordSpecified.ProcessExtraSelect(selectResult, extraSelectType);
|
||
}
|
||
}
|
||
}
|
||
|
||
public IEnumerable<TEntity> ExecuteSelectMany<TEntity>(RequestParamsSelect<TEntity> requestParams) => ExecuteSelectMany(SelectType.Default, requestParams);
|
||
public IEnumerableWithPage<TEntity> ExecuteSelectManyWithPage<TEntity>(RequestParamsSelect<TEntity> requestParams) => (ListWithPage<TEntity>)ExecuteSelectMany(SelectType.Page, requestParams);
|
||
public IEnumerableWithOffer<TEntity> ExecuteSelectManyWithOffer<TEntity>(RequestParamsSelect<TEntity> requestParams) => (ListWithOffer<TEntity>)ExecuteSelectMany(SelectType.Offer, requestParams);
|
||
}
|
||
|
||
public static class SqlHelperExtensions
|
||
{
|
||
public static TValue Get<TValue>(this IDataRecordSpecified record, string fieldName) => record.Get<TValue>(fieldName);
|
||
public static TValue GetN<TValue>(this IDataRecordSpecified record, string fieldName) => record.GetN<TValue>(fieldName);
|
||
}
|
||
} |