none
Detach in Linq to SQL RRS feed

  • Вопрос

  • Как выполнить detach для прекешированной таблицы. Очень хочется пользоваться методом Attach для выполнения операции Insert или Update

    var context = new MyDataCotext(some_connection_string); //MyDataCotext построен кодогенератором sqlmetal
    context.Connection.Open();

    если выполняю такой код:

    var erritem = new ErrorsLog {Id = 5, Message = "Dummy", Date = DateTime.Now};

    context.ErrorsLog.Attach(erritem);
    context.Refresh(RefreshMode.KeepCurrentValues, erritem);

    var changeset = context.GetChangeSet();
    Console.Write(changeset);
               
    context.SubmitChanges();

    убеждаюсь, что данные применились нормально, апдейт произошел успешно (естественно кеша в этом случае нет)

    но чаще бывает ситуация такая:

     var erritemold = context.ErrorsLog.Where(p => p.Id == 5).Single();
     //const BindingFlags FLAGS = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
     //var method = context.GetType().GetMethod("ClearCache", FLAGS);
     //method.Invoke(context, null);

     var erritem = new ErrorsLog {Id = 5, Message = "Dummkopf", Date = DateTime.Now};

     context.ErrorsLog.Attach(erritem); //тут ошибка duplicate key
     context.Refresh(RefreshMode.KeepCurrentValues, erritem);

     var changeset = context.GetChangeSet();
     Console.Write(changeset);
                
     context.SubmitChanges();

    оно, понятно, что LINQ использует, вероятно, Hashtable для мапирования данных, и попытка добавить туда запись с существующим primaryKey вызовет эту ошибку.

    1. Как можно детачить запись без удаления из БД, а только из кеша?

    2. Закомментареный код чистит кеш, но я не уверен что это не приведет к дальнейшему крешу или неправильной работе датаконтекста. Или такое действие вполне возможно?

    • Перемещено SachinW 1 октября 2010 г. 21:38 MSDN Forums Consolidation (От:Начинающие разработчики)
    8 апреля 2010 г. 19:17

Ответы

  • Так много вопросов.... :)

    Вы решаете не ту проблему и не теми методами:

    0. 20 запросов в секунду - это не большая пиковая нагрузка :)

    1. DataContext-ы - это просто кэши объектов. Размер свежесозданного DataContext - 3440 байт, с учетов всего на что он там ссылается, на x64.

    2. Создание DataContext - это дешевая операция. При создании DataContext не происходит открытие соедниения и авторизация. Напишите в цикле создание пару десятков тысяч DataContext, без ручного открытия соединения, и оцените затраты. 0.00003 секунды на создание одного контекста на моей скромной машине.

    3. DataContext-у не нужно открывать соединение вручную. Он сам откроет, по необходимости. И сразу же закроет. Причем он откроет SqlConnection. Открытие SqlConnection - это не обязательно открытие соединения с сервером баз данных.

    4. DataContext использует стандартные SqlConnection.

    5. SqlConnection использует внутренний кэш соединений (connection pool). Размер пула практически ограничивает количество одновременных открытых SqlConnection-ов. Размер по умолчанию - 100. При попытке открыть 101-е содинение - ожидание закрытия уже открытиых, и потом таймаут.

    Не совсем понятно, но скорее всего задача у вас примерно такая:

    Написать сервис с двумя функциями - GetObject и SaveObject.

    Как пытаетесь решить задачу вы:

    GetObject:

    1. Создать DataContext

    2. Открыть соединение

    3. Выбрать объект, отдать куда-то на сторону

    4. Кэшировать DataContext, не закрывая соединения

    SaveObject:

    5. Взять какой-то "закэшированный" DataContext с уже открытым соединением

    6. Создать "измененный" объект

    7. Приаттачить объект

    8. Сохранить изменения

    Соединение у вас открыто и не возвращается в пул от пункта 2 до бесконечности.

    Как надо сделать:

    GetObject:

    1. Создать новый DataContext с ObjectTrackingEnabled = false и DeferredLoadingEnabled = false. Эдакий Read-only контекст.

    2. Не открывать соединение вручную

    3. Выбрать объект

    4. Вернуть объект клиенту.

    5. Ничего не кэшировать. DataContext задиспоузить.

    SaveObject

    6. Создать новый DataContext. 

    7. Не открывать соединение вручную

    8. Приаттачить в него объект. В маппинге объекта прописать UpdateCheck="Never" для всех полей, кроме ID. Тогда при сохранении L2S не будет вписывать дурацкие условия.

    9. Сохранить

    10. Задиспоузить контекст.

    Разница с вашим вариантом:

    Работает на 0.00003 секунды дольше.

    DataContext-ы (и кэши в них) живут не дольше одного вызова.

    GetObject вообще ничего не кэширует

    Все так же используется одно соединение с сервером баз данных, но "захватывается" оно только в моменты собственно выборки (3) и сохранения (9). Все остальное время оно лежит в пуле. Т.е. ограничение на нагрузку - не больше 100 одновременно выполняющихся пунктов 3 и 9.

    Совсем правильное решение - найти готовую реализацию Repository Pattern для Linq 2 SQL на каком-нибудь codeplex. Точно знаю что она там есть, сам пол года назад заливал. Проверил, успевает сделать 400 инсертов в секунду на моей машине, в один поток, на жалком SQL Express. Да, создавая при этом 400 датаконтекстов.

    PS. И 530 в секунду - если в два потока, но ядра у меня всего два, и express одно из них кушает :)

    Код сократится до:

    GetObject(int id)
    {
     return this.Repository.All<EventLog>().Where(e => e.Id == id).Single();
    }
    
    SaveObject(EventLog err)
    {
     return this.Repository.Update<EventLog>(err);
    }
    • Помечено в качестве ответа I.Vorontsov 19 апреля 2010 г. 9:26
    17 апреля 2010 г. 16:32

Все ответы

  • context.ErrorsLog.Attach(erritem, erritemold)
    9 апреля 2010 г. 14:17
  • static void Main(string[] args)
    {
    	var context = new MyDatacontext("Data Source=MyDataSource;Initial Catalog=MyShema;Integrated Security=True");
    	context.Connection.Open();
    
    	//context.DeferredLoadingEnabled = false;
    
    	var erritemold = context.ErrorsLog.Where(p => p.Id == 5).Single();
    	var erritem = new ErrorsLog {Id = 5, Message = "Dummy", Date = DateTime.Now};
    
    	context.ErrorsLog.Attach(erritem, erritemold); // стабильная ошибка: "Не удается добавить объект, который содержит уже использующийся ключ."
    	context.Refresh(RefreshMode.KeepCurrentValues, erritem);
    
    	var changeset = context.GetChangeSet();
    	Console.Write(changeset);
    	
    	context.SubmitChanges();
    }
    
    я понимаю, что я чего то не знаю, но чего? Впринципе ошибка не исчезла. Просто формулировка немного другая. Но, ИМХО в коде вначале пытаются сделать hastable.Add(erritem) а только потом hashtable.Remove(erritemold). 
    9 апреля 2010 г. 16:27
  • DataContext - это не просто кэш, это unit of work + identity map. Он отслеживает изменения во всех загруженных через него сущностях. Кроме того, он гарантирует что каждая запись ErrorLog в базе в каждый момент времени представлена не более чем одним объектом в .NET. После такого вот аттача одна запись будет представлена двумя объектами, что делает объектную модель не совсем целостной.

    Оторвать один объект - не проблема. Но что если:

    1. Объект был загружен не напрямую, а как часть lazyload-коллекции. Коллекция, например, user.ErorrLogs, ссылается на erritem. созданный с нуля erritem на тот же объект user при этом не ссылается.

    2. Объект erritemold менялся до аттача и "очистки их кэша". Должны ли сохраниться изменения?

    DataContext создается на время бизнес-операции. Если erritemold не нужно было менять в пределах операции - то зачем его загружать? Если его нужно было менять - то нужно именно поменять его - скопировать новые значения полей, а не пересоздать заново.

    12 апреля 2010 г. 13:37
  • то, что DataContext не просто кэш это ясно, почему и был задан вопрос не приведет ли в дальнейшем к крешу попытка:

     //const BindingFlags FLAGS = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
     //var method = context.GetType().GetMethod("ClearCache", FLAGS);
     //method.Invoke(context, null);

    так как там и правда много связей и не факт, что очистка одного из EntitySet не нарушит связей внутри DataContext а. К тому же не зря разработчики спрятали метод ClearCache.

    ок, тут есть два варианта решения:

    1. Просто создавать новый DataContext по сути это не просто создание объекта (точнее многих объектов в памяти) это ещё и установление соединения с БД, авторизация и т.д. К тому же при достаточно больших количествах соединений с БД по IP сервер может начинать существенно притормаживать. Это стоит времени.

    2. просто доставать объект из коллекции, и тупо начинать копировать поля из отсоединенного объекта в присоединенный, по времени быстрее, но код получается объемным. Ладно, можно написать процедуру, которая с помощью reflection-а, аттрибутов и какой то матери попереписывает значения by Value в соответсвующие поля целевого обьекта. Но это чревато боком, т.к. ошибки могут быть.

    проблема собственно в том, что данные от сервера отправляются клиенту, клиент что то менят в этих данных, и эти данные отправляются серверу. ИМХО, LINQToSQL должен быть на это рассчитан. Методы Attach тоже не от балды придуманы. Вероятно их можно как то пользовать? Либо это баг или недоделка, и просто надо подождать, пока девелоперы из Редмонда(?) не допишут функционал.

    Далее, а в чем проблема отрыва объекта даже если он был загружен как lazyload? Кстати, при  lazyload загружается вся таблица или только те обьекты на которые ссылаются посторонние? Если так, то что мешает  lazyload контроллеру перевыполнить запрос? хотя, это вопрос внутренней логики, которую я не знаю, и которая, возможно, не документирована. Я заглядывал слегка в тот код, там есть свои сложности, наверное. Кстати, создание объекта erritem это попытка эмуляции появления обекта, приехавшего через WCF на сервер. И, да, он был прочитан из этого, или другого датаконтекста которые находятся в пуле свободных, чисто для оптимизации проблемы описанной в п.1

    кстати... "DataContext создается на время бизнес-операции" - правда? ок, теперь представим что у нас пиковая нагрузка в 20-30 запросов в секунду на бизнесоперации (пусть даже и маленькие). Вы можете посчитать с какого момента сервер БД перейдет в состояние DDOS Fault? Я провел стресс тест на нашем оборудовании (промышленная recmount стойка с несколькими blade ами и полноценным RAID ом, точно не буду вдаваться в подробности) так вот, сервер БД перестал отвечать на запросы уже через 2-3 секунды. Точнее, не то что перестал, а таймауты соединения стали превышать таймауты ожидания  запросов через WCF сервис от клиента. проблема, возможно, в настройке IP их банально можно посмотреть через netstat -na с консоли, после закрытия соединения с БД IP пара находится в стостоянии "ожидание закрытия". Чтобы не попадать в такую ситуацию, остается кэшировать DataContext ы, и выдавать из по запросу процессам, которые их возвращают обратно в пул. Или использовать кластер БД с лоадбалансером... что весьма накладно.

    В принципе ок, думаю что этот вопрос скоро сам собой решится.  Daniel Oor не рекомендовал нам пользоваться LINQTOSQL для промышленных систем с достаточно большой пиковой нагрузкой. В этом случае, по его словам, он рекомендует пользовать Native SQL, а LINQToSQL годится для prototyping =)

     

    17 апреля 2010 г. 10:46
  • Так много вопросов.... :)

    Вы решаете не ту проблему и не теми методами:

    0. 20 запросов в секунду - это не большая пиковая нагрузка :)

    1. DataContext-ы - это просто кэши объектов. Размер свежесозданного DataContext - 3440 байт, с учетов всего на что он там ссылается, на x64.

    2. Создание DataContext - это дешевая операция. При создании DataContext не происходит открытие соедниения и авторизация. Напишите в цикле создание пару десятков тысяч DataContext, без ручного открытия соединения, и оцените затраты. 0.00003 секунды на создание одного контекста на моей скромной машине.

    3. DataContext-у не нужно открывать соединение вручную. Он сам откроет, по необходимости. И сразу же закроет. Причем он откроет SqlConnection. Открытие SqlConnection - это не обязательно открытие соединения с сервером баз данных.

    4. DataContext использует стандартные SqlConnection.

    5. SqlConnection использует внутренний кэш соединений (connection pool). Размер пула практически ограничивает количество одновременных открытых SqlConnection-ов. Размер по умолчанию - 100. При попытке открыть 101-е содинение - ожидание закрытия уже открытиых, и потом таймаут.

    Не совсем понятно, но скорее всего задача у вас примерно такая:

    Написать сервис с двумя функциями - GetObject и SaveObject.

    Как пытаетесь решить задачу вы:

    GetObject:

    1. Создать DataContext

    2. Открыть соединение

    3. Выбрать объект, отдать куда-то на сторону

    4. Кэшировать DataContext, не закрывая соединения

    SaveObject:

    5. Взять какой-то "закэшированный" DataContext с уже открытым соединением

    6. Создать "измененный" объект

    7. Приаттачить объект

    8. Сохранить изменения

    Соединение у вас открыто и не возвращается в пул от пункта 2 до бесконечности.

    Как надо сделать:

    GetObject:

    1. Создать новый DataContext с ObjectTrackingEnabled = false и DeferredLoadingEnabled = false. Эдакий Read-only контекст.

    2. Не открывать соединение вручную

    3. Выбрать объект

    4. Вернуть объект клиенту.

    5. Ничего не кэшировать. DataContext задиспоузить.

    SaveObject

    6. Создать новый DataContext. 

    7. Не открывать соединение вручную

    8. Приаттачить в него объект. В маппинге объекта прописать UpdateCheck="Never" для всех полей, кроме ID. Тогда при сохранении L2S не будет вписывать дурацкие условия.

    9. Сохранить

    10. Задиспоузить контекст.

    Разница с вашим вариантом:

    Работает на 0.00003 секунды дольше.

    DataContext-ы (и кэши в них) живут не дольше одного вызова.

    GetObject вообще ничего не кэширует

    Все так же используется одно соединение с сервером баз данных, но "захватывается" оно только в моменты собственно выборки (3) и сохранения (9). Все остальное время оно лежит в пуле. Т.е. ограничение на нагрузку - не больше 100 одновременно выполняющихся пунктов 3 и 9.

    Совсем правильное решение - найти готовую реализацию Repository Pattern для Linq 2 SQL на каком-нибудь codeplex. Точно знаю что она там есть, сам пол года назад заливал. Проверил, успевает сделать 400 инсертов в секунду на моей машине, в один поток, на жалком SQL Express. Да, создавая при этом 400 датаконтекстов.

    PS. И 530 в секунду - если в два потока, но ядра у меня всего два, и express одно из них кушает :)

    Код сократится до:

    GetObject(int id)
    {
     return this.Repository.All<EventLog>().Where(e => e.Id == id).Single();
    }
    
    SaveObject(EventLog err)
    {
     return this.Repository.Update<EventLog>(err);
    }
    • Помечено в качестве ответа I.Vorontsov 19 апреля 2010 г. 9:26
    17 апреля 2010 г. 16:32