Кэшировать запрос Linq — возможно ли это?

Итак, это может показаться крайним случаем, но мне просто интересно, возможно ли это. У меня есть комбинация статического набора и отложенного набора, завернутого в IEnumerable, например:

public IEnumerable<T> MakeMyQuery<T>()
{
    // returns a List<T>
    var someStaticData = LoadMyStaticDataFromDatabase();

    // Returns IEnumerable<T>, but from another resource
    var deferredQuery = CreateADeferredQueryUsingYieldReturn(); 

    return someStaticData.Concat(deferredQuery);
}

Итак, что здесь происходит, так это то, что когда я вызываю .Take(someNumber) для моего перечисляемого, он сначала возвращает элементы из моих статических данных, прежде чем пытаться оценить отложенный компонент - фактически я «спрятал» некоторые потенциально трудоемкие задачи за перечисляемым, так что если Мне никогда не нужно получать эти элементы, они на самом деле никогда не оцениваются из-за отложенного характера LINQ.

Однако я не думаю, что можно кэшировать этот запрос для последующего использования (я не верю, что состояние итератора будет храниться в кеше, верно?) Или есть простой способ сделать это без перечисления результатов в спасти?

В идеале мой поток был бы таким:

public List<T> SomeMethod<T>(int numberOfGuys)
{
     IEnumerable<T> query = null;

     if(// Is in Cache)
       query = Cache["MyQuery"];
     else
     {
         query = MakeMyQuery();
         Cache["MyQuery"] = query;
     }

     return query.Take(numberOfGuys).ToList();
}

Таким образом, я могу повторно использовать один и тот же запрос снова и снова для запроса данных, но, возможно, мне никогда не придется повторно запрашивать БД. Есть ли способ сделать это?


person Tejs    schedule 15.02.2013    source источник
comment
Вы пытаетесь кэшировать результат LoadMyStaticDataFromDatabase? Это кажется независимым от того, как вы объединяете его с другими данными.   -  person default.kramer    schedule 16.02.2013
comment
На самом деле, я думаю, что это, вероятно, наиболее правильный способ осмысления этой проблемы. Я предполагаю, что я слишком усложняю вещи - все, что мне действительно нужно сделать, это кэшировать результат БД, а затем добавить результат отложенного выполнения к тому, что я получаю из кеша! Думаю, мне просто нужно было записать проблему, чтобы увидеть, что я слишком глубоко запутался. Если вы хотите сделать ответ, я приму это.   -  person Tejs    schedule 16.02.2013
comment
Тем не менее, мое любопытство также хотело бы знать, возможно ли такое вообще каким-то образом.   -  person Tejs    schedule 16.02.2013


Ответы (5)


Я думаю, вы хотите кэшировать результат query.Take(numberOfGuys).ToList() в новом списке. Перед вызовом MakeMyQuery() вы можете посмотреть количество элементов в вашем кэшированном списке (если он существует), и если количество элементов в вашем кэшированном списке больше или равно numberOfGuys, вы вернете numberOfGuys из вашего кэшированного списка. В противном случае вы бы заменили свой кешированный список новым результатом query.Take(numberOfGuys).ToList().

Как указал default.krammer, то, что вы, вероятно, действительно хотите кэшировать, является результатом LoadMyStaticDataFromDatabase(), поскольку, если numberOfGuys всегда меньше, чем LoadMyStaticDataFromDatabase(), вы в конечном итоге будете неоднократно обращаться к БД, пока numberOfGuys не станет больше, чем число, возвращаемое LoadMyStaticDataFromDatabase(). Таким образом, вы можете выполнить комбинацию кэширования LoadMyStaticDataFromDatabase() в методе MakeMyQuery<T>(), а также кэширования query.Take(numberOfGuys).ToList() в SomeMethod<T>(int numberOfGuys), что позволит вам обратиться к БД только один раз, но при этом воспользоваться отложенным выполнением вашего IEnumerable<T>.

person Jacob VanScoy    schedule 15.02.2013
comment
Собираюсь отметить это как ответ вместо публикации default.krammer, так как в конечном итоге это то, что я сделал. - person Tejs; 20.02.2013

Я знаю, что это может быть немного старомодно, но вы потенциально можете заполнить набор данных ADO.NET из базы данных, поскольку набор данных и таблица данных уровня ADO.NET являются отключенным уровнем. Затем набор данных может храниться в памяти приложением в течение определенного периода времени. Затем вы можете отправить его обратно в базу данных. Создайте свой набор данных, заполните его из объекта, подключенного слоя ADO.NET или слоя Linq to SQL, он существует заполненным, вы можете дополнительно заполнить его новыми данными по мере необходимости, а затем сравнить его в окончательном запросе обратно в базу данных для слияния перемены.

Я знаю, что некоторое время назад я сделал проект, в котором я использовал сериализацию Linq, ADO.NET и xml, чтобы в основном сериализовать данные из ADO.NET в файл xml со встроенной сериализацией xml ADO.NET. Затем прочитайте его с помощью Linq to XML. Это было похоже на то, что вы говорите, в том, что файл XML был кешем, по сути, в формате файла, и я просто обновил его с изменениями, подсчитав его отдельные элементы, представляющие значение ключа в базе данных. Если его счетчики были отключены, он обновлялся, в противном случае он оставался прежним. Это было неприменимо для больших наборов из миллионов строк, но для мелких вещей, которые я хотел ВСЕГДА иметь, доступ к ним был приятным и довольно быстрым.

Я знаю, что в книге MS Press 70-516 по доступу к данным .NET 4.0 в конце книги есть лабораторная работа по кэшированию, если вы можете найти ее в Интернете. В основном он нацелен на базу данных, собирает изменения с прошлого раза, работает с ними, а затем объединяется в конце. Таким образом, вы постоянно работаете с дифференциалом, который меньше в памяти, но отслеживает ваши рабочие изменения.

person djangojazz    schedule 15.02.2013

Возможно, я не совсем понимаю ваш вопрос. Если да, дайте мне знать, и я переформулирую свой ответ.

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

static IEnumerable<int> SuperExpensiveQuery()
{
    Console.WriteLine("super expensive query (#1)");
    yield return 100;
    Console.WriteLine("super expensive query (#2)");
    yield return 200;
    Console.WriteLine("super expensive query (#3)");
    yield return 300;
    Console.WriteLine("super expensive query (#4)");
}

static IEnumerable<int> MakeMyQuery()
{
    var someStaticData = new int[] { 1, 2, 3 };
    var deferredQuery = SuperExpensiveQuery();
    return someStaticData.Concat(deferredQuery);
}

static void Test()
{
    var query = MakeMyQuery();
    for (int i = 0; i <= 7; i++)
    {
        Console.WriteLine("BEGIN Take({0})", i);
        foreach (var n in query.Take(i))
            Console.WriteLine("    {0}", n);
        Console.WriteLine("END Take({0})", i);
    }
    Console.ReadLine();
}
person Timothy Shields    schedule 15.02.2013

У меня было подобное требование в одном из моих проектов. В итоге я создал базу кэша DataAccesLayer (DAL), которую я наследую в DAL для каждого из моих компонентов. У меня есть отдельный класс кэширования, который содержит кеш. Обратите внимание, что все мои объекты имели идентификатор и имя. Вы можете настроить базовый класс так, как вам нужно.

Базовый класс DAL:

public abstract class DALBaseCache<T>
    {
        public List<T> ItemList
        {
            get
            {
                List<T> itemList = DALCache.GetItem<List<T>>(typeof(T).Name + "Cache");

                if (itemList != null)
                    return itemList;
                else
                {
                    itemList = GetItemList();
                    DALCache.SetItem(typeof(T).Name + "Cache", itemList);
                    return itemList;
                }
            }
        }

        /// <summary>
        /// Get a list of all the Items
        /// </summary>
        /// <returns></returns>
        protected abstract List<T> GetItemList();

        /// <summary>
        /// Get the Item based on the ID
        /// </summary>
        /// <param name="name">ID of the Item to retrieve</param>
        /// <returns>The Item with the given ID</returns>
        public T GetItem(int id)
        {
            return (from item in ItemList
                    where (int)item.GetType().GetProperty("ID").GetValue(item, null) == id
                    select item).SingleOrDefault();
        }

        /// <summary>
        /// Get the Item based on the Name
        /// </summary>
        /// <param name="name">Name of the Item to retrieve</param>
        /// <returns>The Item with the given Name</returns>
        public T GetItem(string name)
        {
            return (from item in ItemList
                    where (string)item.GetType().GetProperty("Name").GetValue(item, null) == name
                    select item).SingleOrDefault();
        }
    }

Затем мой класс кэширования, который в основном содержит словарь моих запросов

public static class DALCache
{
    static Dictionary<string, object> _AppCache = new Dictionary<string, object>();

    public static T GetItem<T>(string key)
    {
        if(_AppCache.ContainsKey(key))
        {
            return (T) _AppCache[key];
        }
        else
        {
            return default(T);
        }
    }

    public static void SetItem(string key, object obj)
    {
        _AppCache.Add(key, obj);
    }
}

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

public class CustomerTypeDAL: DALBaseCache<CustomerType>
{
    protected override List<CustomerType> GetItemList()
    {
        DBEntities entities = new DBEntities();
        return Mapper.Map <List<CustomerType>>(entities.GetAllCustomerTypes().ToList());
    }
}

В любом месте вашего кода вы можете использовать его как:

CustomerTypeDAL customerTypeDAL = new CustomerTypeDAL();
List<CustomerType> custTypes = customerTypeDAL.ItemList;

В первый раз, когда вы его вызовете, он возьмет его из БД. После этого он пойдет за кешем.

person ZanderAdam    schedule 15.02.2013

Да, это возможно, если вы кэшируете значения во время выполнения итерации.

Это будет выглядеть так:

var lazyList = MakeMyQuery<int>().ToLazyList();
var list1 = lazyList.Take(2).Sum();
var list2 = lazyList.Take(3).Sum();
var list3 = lazyList.Take(1).Sum();

В этом случае:

  • Первые 3 элемента извлекаются из MakeMyQuery всего 1 раз.
  • Четвертый предмет не сдан.

Пример реализация ленивого списка здесь.

person Alex Siepman    schedule 12.10.2013