Мне часто приходилось загружать несколько элементов в определенную запись в базе данных. Например: на веб-странице отображаются элементы для включения в один отчет, все из которых являются записями в базе данных (отчет - это запись в таблице отчета, элементы - это записи в таблице элементов). Пользователь выбирает элементы для включения в один отчет через веб-приложение, и, допустим, он выбирает 3 элемента и отправляет их. Процесс добавит эти 3 элемента в этот отчет, добавив записи в таблицу с именем ReportItems (ReportId, ItemId).
В настоящее время я бы сделал что-то вроде этого в коде:
public void AddItemsToReport(string connStr, int Id, List<int> itemList)
{
Database db = DatabaseFactory.CreateDatabase(connStr);
string sqlCommand = "AddItemsToReport"
DbCommand dbCommand = db.GetStoredProcCommand(sqlCommand);
string items = "";
foreach (int i in itemList)
items += string.Format("{0}~", i);
if (items.Length > 0)
items = items.Substring(0, items.Length - 1);
// Add parameters
db.AddInParameter(dbCommand, "ReportId", DbType.Int32, Id);
db.AddInParameter(dbCommand, "Items", DbType.String, perms);
db.ExecuteNonQuery(dbCommand);
}
и это в хранимой процедуре:
INSERT INTO ReportItem (ReportId,ItemId)
SELECT @ReportId,
Id
FROM fn_GetIntTableFromList(@Items,'~')
Где функция возвращает таблицу целых чисел из одного столбца.
У меня такой вопрос: есть ли лучший способ справиться с чем-то вроде этого? Обратите внимание: я не спрашиваю о нормализации базы данных или о чем-то подобном, мой вопрос касается конкретно кода.





Вы либо делаете то, что у вас уже есть, передаете строку с разделителями, а затем анализируете значение таблицы, либо другой выбор - передача XML-кода и примерно то же самое:
У меня еще не было возможности взглянуть на SQL 2008, чтобы узнать, добавили ли они какие-либо новые функции для обработки таких вещей.
Логику объединения строк, вероятно, можно упростить:
string items =
string.Join("~", itemList.Select(item=>item.ToString()).ToArray());
Это сэкономит вам некоторую конкатенацию строк, что в .Net дорого.
Я не думаю, что что-то не так с тем, как вы сохраняете предметы. Вы ограничиваете поездки в БД, и это хорошо. Если ваша структура данных была более сложной, чем список целых чисел, я бы предложил XML.
Примечание: В комментариях меня спросили, спасет ли это нас от конкатенации строк (это действительно так). Я считаю, что это отличный вопрос, и хотел бы продолжить его.
Если вы откроете открытую строку. Присоединитесь к Отражатель, вы увидите, что Microsoft использует пару небезопасных (в смысле этого слова .Net) техник, включая использование указателя char и структуры под названием UnSafeCharBuffer. То, что они делают, когда вы действительно сводите это к минимуму, - это использование указателей для перехода по пустой строке и создания соединения. Помните, что основная причина того, что конкатенация строк настолько дорога в .Net, заключается в том, что новый строковый объект помещается в кучу для каждой конкатенации, потому что строка неизменяема. Эти операции с памятью дороги. String.Join (..) по сути выделяет память один раз, а затем работает с ней с помощью указателя. Очень быстро.
Да, это избавит вас от конкатенации. String.Join использует UnsafeCharBuffer для выполнения concat (посмотрите на него в Reflector). Что касается LINQ, это просто перечисление по списку, вызывающее ToString (), что неизбежно, и создание массива. Создание массива может быть дорогостоящим.
Я не знал, что у вас получится так, спасибо за подсказку!
И обратите внимание, что в .NET 4 вы можете отказаться от .ToArray().
Одна потенциальная проблема с вашей техникой заключается в том, что она не обрабатывает очень большие списки - вы можете превысить максимальную длину строки для своей базы данных. Я использую вспомогательный метод, который объединяет целочисленные значения в перечисление строк, каждая из которых меньше указанного максимума (следующая реализация также необязательно проверяет и удаляет повторяющиеся идентификаторы):
public static IEnumerable<string> ConcatenateValues(IEnumerable<int> values, string separator, int maxLength, bool skipDuplicates)
{
IDictionary<int, string> valueDictionary = null;
StringBuilder sb = new StringBuilder();
if (skipDuplicates)
{
valueDictionary = new Dictionary<int, string>();
}
foreach (int value in values)
{
if (skipDuplicates)
{
if (valueDictionary.ContainsKey(value)) continue;
valueDictionary.Add(value, "");
}
string s = value.ToString(CultureInfo.InvariantCulture);
if ((sb.Length + separator.Length + s.Length) > maxLength)
{
// Max length reached, yield the result and start again
if (sb.Length > 0) yield return sb.ToString();
sb.Length = 0;
}
if (sb.Length > 0) sb.Append(separator);
sb.Append(s);
}
// Yield whatever's left over
if (sb.Length > 0) yield return sb.ToString();
}
Затем вы используете это примерно так:
using(SqlCommand command = ...)
{
command.Connection = ...;
command.Transaction = ...; // if in a transaction
SqlParameter parameter = command.Parameters.Add("@Items", ...);
foreach(string itemList in ConcatenateValues(values, "~", 8000, false))
{
parameter.Value = itemList;
command.ExecuteNonQuery();
}
}
Хороший момент, я не думал о превышении длины струны. Хотя я сомневаюсь, что у меня будут случаи, которые хотя бы приблизились к этому, хорошо узнать это, спасибо!
См. http://www.sommarskog.se/arrays-in-sql-2005.html для подробного обсуждения этой проблемы и различных подходов, которые вы могли бы использовать.
Запрос одного поля для нескольких значений в хранимой процедуре
http://www.norimek.com/blog/post/2008/04/Query-a-Single-Field-for-Multiple-Values-in-a-Stored-Procedure.aspx
Если вам подходит переход на SQL Server 2008, есть новая функция под названием «Табличные параметры» для решения именно этой проблемы.
Ознакомьтесь с дополнительными сведениями о TVP здесь и здесь или просто спросите у Google «возвращающие табличное значение параметры SQL Server 2008» - вы найдете много информации и примеров.
Настоятельно рекомендуется - если, вы можете перейти на SQL Server 2008 ...
Почему бы не использовать возвращающий табличное значение параметр? https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/table-valued-parameters
Поскольку для этого требуется тип таблицы, определяемый пользователем, который может оставаться в базе данных постоянно, если вы не гарантируете, что вы удаляете этот тип после каждого вызова.
Им довольно сложно управлять, например, если вам нужно внести изменения в скрипт для динамического развертывания.
Вот очень четкое объяснение табличных параметров с sqlteam.com: Табличные параметры со значениями
Это действительно спасет вас от конкатенации? Вы знаете, реализован ли метод с помощью StringBuilder?