Как я могу использовать Nhibernate для извлечения данных, когда «WHERE IN ()» имеет тысячи значений? (слишком много параметров в sql) - PullRequest
20 голосов
/ 09 июня 2011

Проблема : Nhibernate анализирует каждое значение в sql "WHERE IN ()" как параметры, и сервер MS SQL не поддерживает достаточное количество параметров (более 2000).

Я использую Nhibernate с Linq для извлечения моих данных с сервера SQL, и мне нужно загрузить множество объектов на основе уже известных идентификаторов.

Мой код выглядит примерно так:

int[] knownIds = GetIDsFromFile();
var loadedEntities = _Repository.GetAll()
                                .Where(x => knownIds.Contains(x.ID))
                                .ToList();

, которые дают sql, как это:

SELECT id, name FROM MyTable 
WHERE id IN (1 /* @p0 */,2 /* @p1 */,3 /* @p2 */,4 /* @p3 */, 5 /* @p4 */)

Если в knownIds слишком много значений, этот код вызовет исключение из-за множества параметров, используемых NHibernate.

Я думаю, что лучшим решением было бы, если бы я мог заставить NHibernate использовать только 1 параметр для всего "WHERE IN ()", но я не знаю, как это сделать:

SELECT id, name FROM MyTable WHERE id IN (1, 2, 3, 4, 5 /* @p0 */)

Я буду рад услышать любые идеи о том, как решить эту проблему - либо путем расширения поставщика LINQ, либо другими способами. Одним из решений является простое выполнение запроса x раз (knownIds.Count / 1000), но я бы предпочел универсальное решение, которое работало бы для всех моих сущностей.

Я пытался расширить возможности поставщика LINQ с помощью поиска в Google и Stackoverflow, однако не могу найти решения и не имею опыта работы с HQL или построителем дерева. Вот несколько сайтов, на которых я был:

UPDATE: Я знаю, что это не очень хорошая практика - иметь так много значений в предложении IN, но я не знаю лучшего решения для того, что я хочу сделать.
Рассмотрим компанию, в которой все клиенты платят за услуги компании один раз в месяц. Компания не обрабатывает платежи сама, но имеет другую компанию для сбора денег. Один раз в месяц компания получает файл с информацией о состоянии этих платежей: были ли они оплачены или нет. Файл содержит только идентификатор конкретного платежа, а не идентификатор клиента. Компания с 3000 ежемесячных клиентов будет делать 3000 LogPayments каждый месяц, где статус должен быть обновлен. Через 1 год будет около 36 000 LogPayments, поэтому просто загрузить их все тоже не кажется хорошим решением.

МОЕ РЕШЕНИЕ: Спасибо за все полезные ответы. В конце концов я решил использовать комбинацию ответов. Для этого конкретного случая я сделал что-то вроде того, что предложил Четвертый, поскольку это значительно повысило бы производительность. Однако я также реализовал общий метод, предложенный Стефаном Стейнеггером, потому что мне нравится, что я могу сделать это, если это то, чего я действительно хочу. Кроме того, я не хочу, чтобы моя программа аварийно завершала работу, поэтому в будущем я также буду использовать этот метод ContainsAlot в качестве защиты.

Ответы [ 7 ]

12 голосов
/ 09 июня 2011

См. этот похожий вопрос: NHibernate Restrictions.In с сотнями значений

Я обычно настраиваю несколько запросов, которые все получают, например, 1000 записей. Просто разбейте массив идентификаторов на несколько частей.

Примерно так:

// only flush the session once. I have a using syntax to disable
// autoflush within a limited scope (without direct access to the
// session from the business logic)
session.Flush();
session.FlushMode = FlushMode.Never;

for (int i = 0; i < knownIds; i += 1000)
{
  var page = knownIds.Skip(i).Take(1000).ToArray();
  loadedEntities.AddRange(
    Repository.GetAll()
      .Where(x => page.Contains(x.ID)));
}

session.FlushMode = FlushMode.Auto;

Общая реализация с использованием критериев (фильтрация только одного свойства, что является распространенным случаем):

public IList<T> GetMany<TEntity, TProp>(
  Expression<Func<TEntity, TProp>> property,
  IEnumerable<TProp> values)
{
    string propertyName = ((System.Linq.Expressions.MemberExpression)property.Body).Member.Name;

    List<T> loadedEntities = new List<T>();

    // only flush the session once. 
    session.Flush();
    var previousFlushMode = session.FlushMode;
    session.FlushMode = FlushMode.Never;

    for (int i = 0; i < knownIds; i += 1000)
    {
      var page = knownIds.Skip(i).Take(1000).ToArray();

      loadedEntities.AddRange(session
        .CreateCriteria(typeof(T))
        .Add(Restriction.PropertyIn(propertyName, page)
        .List<TEntity>();
    }

    session.FlushMode = previousFlushMode;
    return loadedEntities;
}

Для использования следующим образом:

int[] ids = new [] {1, 2, 3, 4, 5 ....};
var entities = GetMany((MyEntity x) => x.Id, ids);

string[] names = new [] {"A", "B", "C", "D" ... };
var users = GetMany((User x) => x.Name, names);
2 голосов
/ 09 июня 2011

WHERE IN не должно быть нормой и должно использоваться только в особых и ограниченных случаях. Если вы часто используете его, это, вероятно, указывает на проблему с вашей моделью данных. Что я, вероятно, сделал бы в вашем случае - это получить ВСЕ сущности из базы данных в ленивой загрузке, а затем, когда я перебираю имеющиеся у меня идентификаторы, вытаскиваю их из коллекции сущностей. Таким образом, снижение производительности распределяется по многим запросам, и вы не достигнете порога WHERE IN.

Просто чтобы заметить, что если идентификаторы будут представлять большую часть сущностей, а не небольшое подмножество (то есть вы знаете, что в любом случае получите их все, или большую часть), тогда не ленивая загрузка.

Редактировать на основе вашего обновления

Если вы говорите о 36 000 записей через 1 год, НО вы имеете дело только с нагрузками в последнее время, тогда стремитесь загрузить последние записи, которые вас интересуют. Я бы сделал что-то вроде: создать критерии для загрузки записей за прошедший ... месяц? Тогда у меня будут все записи, которые мне могут понадобиться, сопоставить их с идентификаторами в файле с помощью кода и бинго-банго-бонго.

Таблица будет со временем определенно увеличиваться в размере, поэтому не имеет смысла всегда возвращать все назад, но если у вас есть способ сказать «мне важны только эти записи», то SQL может сделать это ограничение для вас.

2 голосов
/ 09 июня 2011

http://ayende.com/blog/2583/nhibernates-xml-in имеет возможное решение, передавая параметры в виде XML (к сожалению, большинство ссылок на странице не работает ..)

1 голос
/ 09 июня 2011

Единственное место, где я когда-либо видел такой код, где идентификаторы распространяются на тысячи, - это когда список идентификаторов только что был загружен из базы данных как отдельный запрос.Вместо этого он должен быть создан как DetachedCriteria, а затем использован с использованием критерия Subqueries.PropertyNotIn или PropertyIn (а не LINQ).


Еще один способ взглянуть на подобные вещи- 2100 параметров ощущается как произвольный предел.Я уверен, что SQL Server можно изменить, чтобы он принимал больше параметров (но я уверен, что запрос Connect будет закрыт почти сразу), или вы можете использовать обходные пути (такие как отправка XML или предварительное заполнение таблицы) для передачи этогомного параметров.Но если вы достигаете этого предела, разве вы не должны отступить и подумать, что в ваших действиях что-то нарушено?

0 голосов
/ 05 декабря 2017

Я столкнулся с той же проблемой в Oracle, который не допускает более 1000 элементов в состоянии IN.Ошибка: «ORA-01795: максимальное количество выражений в списке - 1000».Вот мое решение:

    //partition an IEnumerable into fixed size IEnumerables
    public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int partitionSize)
    {
        return source
            .Select((value, index) => new { Index = index, Value = value })
            .GroupBy(i => i.Index / partitionSize)
            .Select(i => i.Select(i2 => i2.Value));
    }

    public IEnumerable<T> Get(List<long> listOfIDs)
    {
        var partitionedList = listOfIDs.Partition(1000).ToList();
        List<ICriterion> criterions = new List<ICriterion>();
        foreach (var ids in partitionedList)
        {
            criterions.Add(Restrictions.In("Id", ids.ToArray()));
        }
        var criterion = criterions.Aggregate(Restrictions.Or);
        var criteria = session.CreateCriteria<T>().Add(criterion);
        return criteria.Future<T>();
    }

Первая часть - это метод расширения до IEnumerable, для разделения большого списка на списки фиксированного размера.Во второй части используются критерии NHibernate для динамической генерации нескольких условий IN для последующего соединения с условиями OR.

0 голосов
/ 10 июня 2011

Я бы определенно рекомендовал использовать временную таблицу для такого рода данных.

Вы получите возможность проверить правильность параметров, запросив эту временную таблицу.И у вас могут быть ограничения по внешнему ключу, так что вы запутаетесь перед плохими идентификаторами.И вы можете иметь историю их в своей базе данных.

0 голосов
/ 09 июня 2011

Вы не можете сделать список IN только одним параметром (например, массивом), потому что это не поддерживается SQL. Единственный известный мне способ иметь более 1000 элементов в списке IN - это разместить подзапрос там.
Сказав это, одним из обходных путей было бы поместить известные идентификаторы во временную таблицу и изменить инструкцию NHibernate, чтобы использовать эту таблицу, чтобы это привело к подзапросу в инструкции SQL.

...