- Регистрация
- 27.12.2016
- Сообщения
- 289
- Реакции
- 410
- Баллы
- 63
Всем добра и веселого Нового года без последствий!
В статье пойдет речь о том, как упростить взаимодействие с БД, структурировать данные в шаблоне, сделать код чище, а расширение функционала и поддержку шаблона проще. А делать это мы будем используя самописные классы в общем коде, БД MySQL и бесплатные библиотеки Dapper и Dapper.Contrib.
И сразу пару слов о монетизации с использованием данного материала — чем проще написать и прочитать (особенно спустя время) код, чем легче его дебажить и редактировать, тем меньше времени на это нужно. А время, как известно — это деньги. И чем меньше времени затрачивается на написание и поддержку одного шаблона, тем больше можно заработать. По-моему так, как говаривал Винни-Пух из советского мультика.
ПОЛЬЗОВАТЕЛЬСКИЕ КЛАССЫ
Прежде чем начать рассказ об использовании собственных классов, приведу пример кода:
Что здесь можно увидеть, если попытаться проанализировать этот код?
Мы видим здесь переменные с url Ленты, путями xpath, регулярку, и обработку результатов get-запроса, после которой собранная информация собирается в массив и сохраняется в строку таблицы. А что это за информация такая? Опять же, по переменным типа StandartPriceRub и StandartPriceKop, мы можем предположить, что парсится здесь товар.
Легко ли читается такой код, видна ли в нем логика выполняемых действий? Имхо, код сложно читаем, а логика теряется среди объявления путей xpath. И наличие комментариев, не спасет ситуацию (я попробовал коментить, — стало только хуже). А теперь еще один пример кода, делающего ровно то же самое, что и предыдущий:
Что изменилось? У нас появился какой-то новый тип данных Product prod, в свойства которого мы сохраняем всю информацию о товаре, и из него же берем xpath, regex. Откуда он взялся? А ниоткуда — я написал в общем коде 3 класса, описывающих типы данных Product, ProductXpath и ProductRegex. Класс Product имеет все свойства, которые мы парсим со страницы, а так же вложенные классы ProductXpath и ProductRegex, с описанием путей Xpat и Regex-выражений соответственно. И добавил классу Product собственный конструктор класса, который при создании объекта класса Product, создает в нем объекты классов ProductXpath и ProductRegex:
В данном случае класс Product нужен только чтобы структурировать всю информацию о товаре, больше ничего он не умеет, хотя возможности у классов значительно шире. Но даже в таком виде, применение класса в кубике значительно упростило восприятие информации — после создания нового объекта prod класса Product мы обращаемся к его свойствам, указывая что свойство принадлежит конкретному объекту prod (например prod.id). Это может быть неочевидно сразу, но через 1-2 недели открыв кубик с первым примером, нам придется потратить время, чтобы вникнуть что же мы тут делаем, тогда как открыв пример в котором есть объект prod класса Product, нам и через месяц будет понятно, что к чему, а написание комментариев в общем коде, которые отображаются в коде кубиков как всплывающие подсказки при наведениии курсора мыши, еще больше упрощают понимание.
Еще один плюс к использованию своих классов — при объявлении нами экземпляра класса
Далее. Если объекты класса Product используются в нескольких разных кубиках шаблона, и нам вдруг понадобилось изменить, скажем xpath для получения описания товара, ну или добавить свойство metatitle, нам нужно будет править только описание класса в общем коде (правда получение свойства metetitle все же придется дописать в кубике).
Так же, внутри класса можно создать методы для работы со свойствами и полями членов класса, которые позволят еще сильнее упростить работу — ну например написать метод, который будет возвращать строковый массив свойств класса в нужном порядке для вставки в таблицу. И как можно было заметить выше, членами класса могут быть объявлены не только простые типы данных (string, int и т.д.), но и другие созданные нами классы. В приведенном мной примере, чтобы не усложнять его, я не стал собирать характеристики товара и его категорию. А ведь у категории тоже имеются свойства — как минимум, нам может понадобиться: имя и url (а еще id, изображение, описание, метатеги)— вот уже и готовый пример вложенного класса, т.к. пихать все это в класс Product, значит переместить свалку нетипизированных данных из кубика в созданный класс.
Несколько примеров типов данных:
БАЗА ДАННЫХ MYSQL И ЕЕ СТАНДАРТНАЯ БИБЛИОТЕКА ДЛЯ C# MYSQL CONNECTOR/NET
Во вступительной части я уже упоминал о полезной статье из предыдущих конкурсов по работе с MySQL с использованием стандартной библиотеки MySQL Connector/NET, которую настоятельно рекомендую к прочтению, чтобы понимать о чем пойдет речь дальше. Хотя мы не будем пользоваться классами и методами библиотеки (почти), она по-прежнему имеет важность, выполняя роль моста между нашим шаблоном и БД, к тому же, для сравнения, я буду показывать, как бы выглядел запрос к БД через MySQL Connector/NET.
Все что написано ниже, предполагает, что у вас установлена БД MySQL (или имеется доступ к MySQL на VDS/VPS/хостинге — но при траблах тут я не помощник, у меня все на локали). Отличный вариант — Open Server любой редакции (я пользуюсь именно ним).
Какие задачи выполняет библиотека MySQL\Connector? Единственная решаемая ею задача — это возможность работы с БД посредством SQL-запросов непосредственно из кода NET-приложений.
Запросы к БД с использованием либы громоздки и многострочны, в ней полностью отсутствуют механизмы сопоставления пользовательских классов таблицам БД.
Рассмотрим пример запроса к БД с использованием данной библиотеки и класса Proxy.
Начальные условия:
У нас имеется база данных dapperlearn в которой имеется таблица proxy (отойдем от товара, и поработаем с другой сущностью). В этой таблице мы создали следующие колонки: id, protocol, ip, login, pass.
А в общем коде проекта ZP мы описываем класс Proxy, имеющий такие же свойства, и однозначно описывающий объект:
Теперь, напишем код, добавляющий в таблицу БД объекты класса Proxy, которые лежат в списке proxyList (т.е каждая строка списка proxyList это объект класса Proxy со свойствами id, protocol и т.д.):
По-моему, для одного запроса кода многовато, я бы даже сказал дофига. А если надо не только добавить но и получить данные — получится еще больше. Очевидно, что работа с БД посредством этой либы тяжела как рабский труд, а вероятность ошибок при таком количестве кода значительно повышается. Конечно, за неимением лучшего работать с ней можно, но...
А точно ли ничего лучшего нет?
МИКРО-ORM DAPPER — РАБОТАЕМ С БД MYSQL БЕЗ БУБНОВ И ПОРТЯНОК КОДА
А лучшее есть. И называется это лучшее — ORM (англ. Object-Relational Mapping, рус. объектно-реляционное отображение, или преобразование, или, мапинг на слэнге) — технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных» (Wiki). Существует много разных ORM, самые известные из которых Entity Framework от Microsoft и NHibernate. Проблема этих систем в огромном функционале (совершенно избыточном в нашем случае), а следовательно, сложности его освоения (именно такое мнение я читал о них в инете).
А еще есть небольшая либа, «микро-ORM» как позиционируют ее авторы, и называется она Dapper. Она обеспечивает сопоставление классов таблицам БД, сохраняя при этом высокую производительность и многопотчность, а еще упрощает выполнение запросов, и работает из коробки, без мучительных настроек. Dapper был создан для Stack Overflow — одного из самых посещаемых сайтов, созданных на продуктах Microsoft, К тому же, она абслоютно бесплатна.
Итак, ниже пойдет речь о работе с библиотекой Dapper, а так же ее расширении Dapper.Contrib написанной теми же разработчиками. Основная задача Dapper — сопоставлять (мапить) данные программы C# с таблицами БД, а помимо мапинга, Dapper еще и берет на себя часть работы по созданию объектов MySql Connector, необходимых для подключения к БД. Dapper.Contrib дополняет функционал Dapper и упрощает выполнение некоторых запросов, СОВСЕМ освобождая от необходимости писать SQL (хотя и теряя при этом в гибкости).
Идеальный вариант это их совместное использование — иногда что-то удобнее выполнить на Dapper, иногда на Contrib, а в комплекте получается незаменимая штука, экономящая километры строк кода.
Но хватит лирики, пора окунаться в магию Dapper. А чтобы острее почувствовать ее, начнем с рутины — настройки проекта.
Для работы с БД MySQL нам понадобится установленный на ПК MySQL (у меня стоит Open Server) или доступ к удаленному хосту с MySQL, а так же следующие библиотеки:
Наш проект готов к работе.
Ну а теперь пора приступать к изучению возможностей Dapper и Dapper.Contrib.
Мы рассмотрим лишь основные доступные у Dapper методы для отправки-получения объектов(или отдельных их свойств), иначе может получиться роман в нескольких книгах:
Во вложенном архиве имеется шаблон dapperlearn_mysql.zp в котором есть все приведенные ниже примеры работы с dapper и dapper.contrib с подробными коментами. Структура шаблона выглядит следующим образом: В самой левой колонке создается тестовая БД с таблицами user и proxy, proxyserver, таблицы наполняются тестовой информацией. В трех следующих колонках приведены примеры одного и того же запроса на MySQL Connector/Dapper/Dapper.Contrib. Все запросы которые я публикую здесь в урезанном виде (без строки подключения и создания объектов) там представлены полностью. И последний раздел — это пример работы с методами, написанными в общем коде для класса Proxyserever (о нем чуть ниже), с целью облегчить взаимодействие с данными класса.
Выше я показывал, как выглядит запрос на добавление списка объектов Proxy в БД (INSERT). Теперь сделаем это на Dapper:
Специально посчитал запрос через MySQL Connector от строки с директивой using до закрываюшей ее скобки — получилось 22 строки. А на dapper 5 строчек — разница более чем в 4 раза. Теперь, тот же запрос на Contrib:
Тоже неплохо, верно? Хочу обратить внимание — в данном случае самого SQL-запроса нет вообще, все делает Dapper.Contrib.
Здесь следует пояснить назначение строки
Dapper.Contrib по-умолчанию выполняет сопостовление по следующей схеме — получает имя класса которое нужно передать в БД, добавляет к нему англ. букву "s" (Proxy => proxys, User => users), и пытается найти в подключенной БД таблицу с этим именем. А так как сам Dapper сопоставляет класс и таблицу без изменений (Proxy => proxy, User => user), то в Contib предусмотрен механизм, позволяющий восстановить прямое сопоставление, как в Dapper. Вот эта директива и показывает Contib, что мапить нужно по имени класса, без лишних "s".
Написать ее нужно всего лишь один раз (например где-то в начале шаблона) и она будет работать до перезагрузки программы. Так же, у Contrib можно подменить имя класса, подставив любое указанное имя(Proxy => proxyserver, User => people). Это сработает при условии, что объект имеет свойства и их названия, соотв. колонками и названиям таблицы. Для того чтобы замапить, например, класс Proxy в таблицу proxyserver (и попутно отменить сопоставление по умолчанию):
Еще одно важное замечание — я не нашел однозначной информации о блокировании таблиц БД Dapper-ом и Contib при вставке и обновлении данных. Поэтому, при его использовании в рабочих проектах, наверное лучше перебздеть и залочить их принудительно, чтобы не было мучительно больно, для этого перед запросом к данным, следует выполнить еще один на блокировку, а после, на разблокировку таблицы.
Продолжим изучать запросы. Теперь вставим в таблицу один объект Proxy (INSERT). Dapper:
Тот же запрос — Dapper.Contrib:
Как мы видим, вставка объектов и списков с ними проста и там и там. А в contrib еще и SQL-запросы писать не нужно. И тогда нафига он нужен, чистый dapper, Но вот заметил какую штуку — метод Insert вставляет строку в БД железобетонно, независимо от того, есть там уже такая запись или нет. Так что, в рабочем проекте, перед вставкой, стоит проверить наличие по какому-либо уникальному свойству, а для этого нужен чистый dapper. Так же недостатки contib можно увидеть на запросах, которые делают выборку из БД — он может или получить одну строку по id, или все строки таблицы.
Чтобы не расслабляться, теперь поработаем с таблицей user и классом User:
У таблицы user, имеются колонки с наименованиями и типами данных соответствующими именам и типам данных объекта User. Посмотрим как выглядит запрос UPDATE по условию на Dapper :
Такой запрос обновит все найденные записи начинающиеся на указанную букву, строками из списка. Правда, если записей будет найдено больше чем объектов в списке — появятся дубли (но это проблема запроса, а не dapper) А теперь contrib:
Тут уже начинается чехарда — методов для выборки по условию в contrib нет, поэтому приходится получать все строки из таблицы, LINQ-ом выбирать строки начинающиеся на нужную букву, менять id у тех объектов, которыми мы будем обновлять выборку на тот id у которых мы будем менять, короче тяжко все это (допускаю, что можно сделать изящнее, но чет в попыхах не придумалось).
Обновление по id. Dapper:
В ответе dapper возвращает значение int, которое указывает кол-во строк, затронутых запросом. Теперь contrib:
Вывод: Contrib удобнее использовать, когда не нужно выполнять действия, связанные с выборками данных из БД. А еще лучше, когда выборка получается на dapper, а обновление — на contrib.
Вернемся к нашим объектам Proxy и таблице proxy в БД и продолжим изучение и сравнение методов Dapper и Contrib. Получение (SELECT) всех строк из таблицы на Dapper:
То же на Contib:
Получение одной строки показывать здесь не буду, примеры есть в шаблоне. Повторю лишь, что используемый для этого метод Dapper (QuerySingle) вернет ошибку, если в ответе будет более 1 строки.
Далее опять будем работать с объектами User и таблицей с соотв. его св-вам колонками id, login, pass, email (я их в самом начале сделал, не простаивать же им). Получение 1-го поля из одной строки (аналог ExecuteScalar MySQL Connector).
Напомню, Contrib не предусмотрен для получения выборок, поэтому здесь мы получаем весь объект (строку таблицы) и возвращаем нужное нам свойство этого объекета:
Следующее действие — удаление по условию нескольких строк из таблицы (SQL - DELETE). Пример по удалению одной строки здесь показывать не буду, есть в шаблоне. На Dapper:
То же на Contib:
Ну и напоследок метод DeleteAll Contrib, удаляющий все строки из таблицы, которой соотв. указанный объект. Тут все совсем просто:
В завершении статьи предлагаю вернуться к классу Proxyserver из примера комбинированного запроса, и немного расширить его функционал, чтобы с ним было удобно работать.
На всякий случай повторю условия:
у нас имеется класс Proxyserver со следующими свойствами — id, protocol, ip, port, login, pass, isuse, и таблица proxyserver в БД, с колонками, наименования которых соотв. именам свойств класса. Описание класса добавляем в общий код, создав для него пространство имен DataClasses.
Не забываем прописать в директивы using наше новое простраство имен —
В примерах выше мне настолько не нравилось писать строку подключения к БД, что вместо нее я писал комментарий. Чтобы не заниматься этой писаниной и дальше, сделаем свой конструктор класса, в котором будет создаваться эта строка, а так же будет отменяться сопсотавление данных по-умолчанию для Contrib. Отдельно создадим и пустой конструктор, чтобы можно было создать пустой объект Proxyserver.
И напишам несколько методов для получения и обновления данных из/в БД, для передачи нужных свойств в строку в формате protocol://login:pass@ip:port и присвоения значений свойствам объекта из такой строки. В итоге у меня получился такой класс Proxyserver в общем коде:
Методы Dapper и Contrib оказались недоступны внутри namespace DataClasses, пришлось прописать using.
А работа с объектами класса в кубиках выглядит теперь так (коменты здесь в принципе не нужны, т.к. они присутствуют в общем коде):
И получение использованной строки прокси в объект и разблокировка соотв. строки в таблице БД:
Сюда же, в методы класса можно было бы добавить еще проверку полученной строки на работоспособность, и перед сохраненением прокси в переменную проекта проверять, что она рабочаяя, но я ведь не рабочий инструмент писал, а показывал возможности, так что это оставлю для самостоятельной работы.
Спасибо за внимание всем, кто осилил до конца
Каждый, кто коннектил свои шаблоны с MySQL, используя стандартный MySQL Connector/NET, работа с которым описана в одном из предыдущих конкурсов, знает — чтобы в рамках 1 сессии выполнить работу с БД, надо написать полкилометра кода, в дебрях которого легко потерять и логику шаблона и собственный мозг. А если для взаимодействия со страницей нужно оперировать десятком-другим переменных, время от времени пихать и извлекать их в/из БД? А если шаб используется регулярно и развивается? А если клиенты, которые его юзают, периодически просят добавить очередную хотелку, а сайт-донор с завидной регулярностью меняет верстку? Жуть какая-то! Как же при такой жизни найти место для прекрасной незнакомки, стаканчика-другого вискаря с друзьями, интересных путешествий и всего прочего, что рисовало воображение?!
В статье пойдет речь о том, как упростить взаимодействие с БД, структурировать данные в шаблоне, сделать код чище, а расширение функционала и поддержку шаблона проще. А делать это мы будем используя самописные классы в общем коде, БД MySQL и бесплатные библиотеки Dapper и Dapper.Contrib.
И сразу пару слов о монетизации с использованием данного материала — чем проще написать и прочитать (особенно спустя время) код, чем легче его дебажить и редактировать, тем меньше времени на это нужно. А время, как известно — это деньги. И чем меньше времени затрачивается на написание и поддержку одного шаблона, тем больше можно заработать. По-моему так, как говаривал Винни-Пух из советского мультика.
ПОЛЬЗОВАТЕЛЬСКИЕ КЛАССЫ
Прежде чем начать рассказ об использовании собственных классов, приведу пример кода:
C#:
string url = "https://lenta.com/product/el-iskusstvennaya-pvh-d70sm-200-vetok-120sm-plastikpodstavka-ap04a200t-kitajj-377302/";
string get = ZennoPoster.HTTP.Request
(
method:ZennoLab.InterfacesLibrary.Enums.Http.HttpMethod.GET,
url:url,
Encoding:@"UTF-8",
respType:ZennoLab.InterfacesLibrary.Enums.Http.ResponceType.HeaderAndBody,
Timeout:70000,
throwExceptionOnError:true
);
string xImgSrc = @"//img[@itemprop='image']";
string rImgSrc = @".*(?=\?preset=fulllossywhite)";
string xTitle = @"//h1";
string xSku = @"//div[@class='sku-page__code-info']";
string xStandartPriceRub = @"//div[contains(@class, 'sku-price--regular')]//span[@class='price-label__integer']";
string xStandartPriceKop = @"//div[contains(@class, 'sku-price--regular')]//*[@class='price-label__fraction']";
string xPrimPriceRub = @"//div[contains(@class, 'sku-price--primary')]//span[@class='price-label__integer']";
string xPrimPriceKop = @"//div[contains(@class, 'sku-price--primary')]//*[@class='price-label__fraction']";
string xAval = @"//div[contains(@class, 'sku-store-container__stock')]";
string xDescr = @"//div[@itemprop='description']";
if(string.IsNullOrEmpty(get)) throw new Exception("Get-запрос вернул пустоту");
string imgSrc = ZennoPoster.Parser.ParseByXpath(get, xImgSrc, "src").ElementAt(0);
imgSrc = Regex.Match(imgSrc, rImgSrc).Value;
string title = ZennoPoster.Parser.ParseByXpath(get, xTitle, "innertext").ElementAt(0);
string sku = ZennoPoster.Parser.ParseByXpath(get, xSku, "innertext").ElementAt(0);
string standartPriceRub = ZennoPoster.Parser.ParseByXpath(get, xStandartPriceRub, "innertext").ElementAt(0);
string standartPriceKop = ZennoPoster.Parser.ParseByXpath(get, xStandartPriceKop, "innertext").ElementAt(0);
string standartPrice = standartPriceRub + "." + standartPriceKop;
string primPriceRub = ZennoPoster.Parser.ParseByXpath(get, xPrimPriceRub, "innertext").ElementAt(0);
string primPriceKop = ZennoPoster.Parser.ParseByXpath(get, xPrimPriceKop, "innertext").ElementAt(0);
string primPrice = primPriceRub + "." + primPriceKop;
string avalaible = ZennoPoster.Parser.ParseByXpath(get, xAval, "innertext").ElementAt(0);
string descr = ZennoPoster.Parser.ParseByXpath(get, xDescr, "innertext").ElementAt(0);
string[] totable = new string[] {sku, title, descr, avalaible, standartPrice};
var table = project.Tables["result"];
table.AddRow(totable);
Что здесь можно увидеть, если попытаться проанализировать этот код?
Мы видим здесь переменные с url Ленты, путями xpath, регулярку, и обработку результатов get-запроса, после которой собранная информация собирается в массив и сохраняется в строку таблицы. А что это за информация такая? Опять же, по переменным типа StandartPriceRub и StandartPriceKop, мы можем предположить, что парсится здесь товар.
Легко ли читается такой код, видна ли в нем логика выполняемых действий? Имхо, код сложно читаем, а логика теряется среди объявления путей xpath. И наличие комментариев, не спасет ситуацию (я попробовал коментить, — стало только хуже). А теперь еще один пример кода, делающего ровно то же самое, что и предыдущий:
C#:
string url = "https://lenta.com/product/el-iskusstvennaya-pvh-d70sm-200-vetok-120sm-plastikpodstavka-ap04a200t-kitajj-377302/";
string get = ZennoPoster.HTTP.Request
(
method:ZennoLab.InterfacesLibrary.Enums.Http.HttpMethod.GET,
url:url,
Encoding:@"UTF-8",
respType:ZennoLab.InterfacesLibrary.Enums.Http.ResponceType.HeaderAndBody,
Timeout:70000,
throwExceptionOnError:true
);
Product prod = new Product();
prod.title = ZennoPoster.Parser.ParseByXpath(get, prod.x.titleXpath, "innertext").ElementAt(0);
prod.descr = ZennoPoster.Parser.ParseByXpath(get, prod.x.descrXpath, "innertext").ElementAt(0);
prod.sku = ZennoPoster.Parser.ParseByXpath(get, prod.x.skuXpath, "innertext").ElementAt(0);
prod.standartPrice = string.Format("{0}.{1}",
ZennoPoster.Parser.ParseByXpath(get, prod.x.standartPriceRubXpath, "innertext").ElementAt(0),
ZennoPoster.Parser.ParseByXpath(get, prod.x.standartPriceKopXpath, "innertext").ElementAt(0));
prod.primPrice = string.Format("{0}.{1}",
ZennoPoster.Parser.ParseByXpath(get, prod.x.primPriceRubXpath, "innertext").ElementAt(0),
ZennoPoster.Parser.ParseByXpath(get, prod.x.primPriceKopXpath, "innertext").ElementAt(0));
prod.avalaible = ZennoPoster.Parser.ParseByXpath(get, prod.x.avalaibleXpath, "innertext").ElementAt(0);
prod.imgSrc = Regex.Match(ZennoPoster.Parser.ParseByXpath(get, prod.x.imgSrcXpath, "src").ElementAt(0),
prod.r.imgSrcRegex).Value;
string[] totable = new string[] {prod.title, prod.descrб prod.sku, descr, prod.standartPrice, prod.primPrice, prod.avalaible};
var table = project.Tables["result"];
table.AddRow(totable);
Что изменилось? У нас появился какой-то новый тип данных Product prod, в свойства которого мы сохраняем всю информацию о товаре, и из него же берем xpath, regex. Откуда он взялся? А ниоткуда — я написал в общем коде 3 класса, описывающих типы данных Product, ProductXpath и ProductRegex. Класс Product имеет все свойства, которые мы парсим со страницы, а так же вложенные классы ProductXpath и ProductRegex, с описанием путей Xpat и Regex-выражений соответственно. И добавил классу Product собственный конструктор класса, который при создании объекта класса Product, создает в нем объекты классов ProductXpath и ProductRegex:
C#:
public class Product
{
public string title{get; set;}
public string descr{get; set;}
public string sku{get; set;}
public string standartPrice{get; set;}
public string primPrice{get; set;}
public string imgSrc{get; set;}
public string avalaible{get; set;}
public ProductXpath x;
public ProductRegex r;
public Product()
{
x = new ProductXpath();
r = new ProductRegex();
}
}
/// <summary>
/// Содержит пути Xpath для получения нужных св-в товара
/// </summary>
public class ProductXpath
{
/// <summary>
/// Xpath для получения ссылки на фото
/// </summary>
public string imgSrcXpath = @"//img[@itemprop='image']";
public string titleXpath = @"//h1";
public string descrXpath = @"//div[@itemprop='description']";
public string skuXpath = @"//div[@class='sku-page__code-info']";
public string standartPriceRubXpath = @"//div[contains(@class, 'sku-price--regular')]//span[@class='price-label__integer']";
public string standartPriceKopXpath = @"//div[contains(@class, 'sku-price--regular')]//*[@class='price-label__fraction']";
/// <summary>
/// Xpath для получения стоимости товара (рубли)
/// </summary>
public string primPriceRubXpath = @"//div[contains(@class, 'sku-price--primary')]//span[@class='price-label__integer']";
public string primPriceKopXpath = @"//div[contains(@class, 'sku-price--primary')]//*[@class='price-label__fraction']";
public string avalaibleXpath = @"//div[contains(@class, 'sku-store-container__stock')]";
}
/// <summary>
/// Содержит выражения Regex для обработки свойств товара
/// </summary>
public class ProductRegex
{
public string imgSrcRegex = @".*(?=\?preset=fulllossywhite)";
}
В данном случае класс Product нужен только чтобы структурировать всю информацию о товаре, больше ничего он не умеет, хотя возможности у классов значительно шире. Но даже в таком виде, применение класса в кубике значительно упростило восприятие информации — после создания нового объекта prod класса Product мы обращаемся к его свойствам, указывая что свойство принадлежит конкретному объекту prod (например prod.id). Это может быть неочевидно сразу, но через 1-2 недели открыв кубик с первым примером, нам придется потратить время, чтобы вникнуть что же мы тут делаем, тогда как открыв пример в котором есть объект prod класса Product, нам и через месяц будет понятно, что к чему, а написание комментариев в общем коде, которые отображаются в коде кубиков как всплывающие подсказки при наведениии курсора мыши, еще больше упрощают понимание.
Еще один плюс к использованию своих классов — при объявлении нами экземпляра класса
Product prod = new Product(); все его свойства инициализируются автоматически — их не нужно объявлять дополнительно.Далее. Если объекты класса Product используются в нескольких разных кубиках шаблона, и нам вдруг понадобилось изменить, скажем xpath для получения описания товара, ну или добавить свойство metatitle, нам нужно будет править только описание класса в общем коде (правда получение свойства metetitle все же придется дописать в кубике).
Так же, внутри класса можно создать методы для работы со свойствами и полями членов класса, которые позволят еще сильнее упростить работу — ну например написать метод, который будет возвращать строковый массив свойств класса в нужном порядке для вставки в таблицу. И как можно было заметить выше, членами класса могут быть объявлены не только простые типы данных (string, int и т.д.), но и другие созданные нами классы. В приведенном мной примере, чтобы не усложнять его, я не стал собирать характеристики товара и его категорию. А ведь у категории тоже имеются свойства — как минимум, нам может понадобиться: имя и url (а еще id, изображение, описание, метатеги)— вот уже и готовый пример вложенного класса, т.к. пихать все это в класс Product, значит переместить свалку нетипизированных данных из кубика в созданный класс.
Несколько примеров типов данных:
- Прокси. Данный объект обладает следующими свойствами — протокол (http, https, soks4, soks5), ip, порт, логин, пароль. Сюда же можно добавить дату последнего использования, id акка, к которому он может быть привязан и т.д.
- Аккаунт и его свойства — login, password, email, телефон, id, состояние (рабочий, отлежка и т.д.), дата последнего использования
- Email — логин, пароль, имя, фамилия, контрольный вопрос, ответ на него, дата последнего получения почты и т.д.
- User (спаршенная инфа) — имя, фамилия, id, телефон, день рождения, ссылка на профиль, ссылка (или id) профиля мужа/жены и т.д.
- Много чего еще.
БАЗА ДАННЫХ MYSQL И ЕЕ СТАНДАРТНАЯ БИБЛИОТЕКА ДЛЯ C# MYSQL CONNECTOR/NET
Во вступительной части я уже упоминал о полезной статье из предыдущих конкурсов по работе с MySQL с использованием стандартной библиотеки MySQL Connector/NET, которую настоятельно рекомендую к прочтению, чтобы понимать о чем пойдет речь дальше. Хотя мы не будем пользоваться классами и методами библиотеки (почти), она по-прежнему имеет важность, выполняя роль моста между нашим шаблоном и БД, к тому же, для сравнения, я буду показывать, как бы выглядел запрос к БД через MySQL Connector/NET.
Все что написано ниже, предполагает, что у вас установлена БД MySQL (или имеется доступ к MySQL на VDS/VPS/хостинге — но при траблах тут я не помощник, у меня все на локали). Отличный вариант — Open Server любой редакции (я пользуюсь именно ним).
Какие задачи выполняет библиотека MySQL\Connector? Единственная решаемая ею задача — это возможность работы с БД посредством SQL-запросов непосредственно из кода NET-приложений.
Запросы к БД с использованием либы громоздки и многострочны, в ней полностью отсутствуют механизмы сопоставления пользовательских классов таблицам БД.
Рассмотрим пример запроса к БД с использованием данной библиотеки и класса Proxy.
Начальные условия:
У нас имеется база данных dapperlearn в которой имеется таблица proxy (отойдем от товара, и поработаем с другой сущностью). В этой таблице мы создали следующие колонки: id, protocol, ip, login, pass.
А в общем коде проекта ZP мы описываем класс Proxy, имеющий такие же свойства, и однозначно описывающий объект:
C#:
public class Proxy
{
public int id {get; set;}
public string protocol {get; set;}
public string ip {get; set;}
public string login {get; set;}
public string pass {get; set;}
}
Теперь, напишем код, добавляющий в таблицу БД объекты класса Proxy, которые лежат в списке proxyList (т.е каждая строка списка proxyList это объект класса Proxy со свойствами id, protocol и т.д.):
C#:
//список, в котором лежат объекты Proxy
List<Proxy> proxyList = new List<Proxy>();
//Строка подключения к БД
string connString = String.Format("Data Source={0};UserId={1};Password={2};database={3};Charset={4};SSL Mode=None",
project.Variables["DB_host"].Value,
project.Variables["DB_user"].Value,
project.Variables["DB_pass"].Value,
project.Variables["DB_name"].Value,
project.Variables["DB_charset"].Value);
//запрос к БД, где @protocol, @ip, @login, @pass параметры,
string query = "INSERT proxy (protocol, ip, login, pass) VALUES (@protocol, @ip, @login, @pass)";
int res = 0;;
//Создаем объект MySqlCommand
using(MySqlCommand command = new MySqlCommand())
{
//Подключаемся к БД
command.Connection = new MySqlConnection(connString);
//Открываем сессию
command.Connection.Open();
//блокируем таблицу для др. потоков
command.CommandText = "LOCK TABLES proxy WRITE";
command.ExecuteNonQuery();
foreach(Proxy proxy in proxyList)
{
//передаем в объект cummand строку запроса
command.CommandText = query;
command.Parameters.Clear();
//и параметры запроса
command.Parameters.AddWithValue("@protocol", proxy.protocol);
command.Parameters.AddWithValue("@ip", proxy.ip);
command.Parameters.AddWithValue("@login", proxy.login);
command.Parameters.AddWithValue("@pass", proxy.pass);
//отпарвляем запрос на добавление строк
res += command.ExecuteNonQuery();
}
//разблокируем таблицу
command.CommandText = "UNLOCK TABLES;";
command.ExecuteNonQuery();
}
По-моему, для одного запроса кода многовато, я бы даже сказал дофига. А если надо не только добавить но и получить данные — получится еще больше. Очевидно, что работа с БД посредством этой либы тяжела как рабский труд, а вероятность ошибок при таком количестве кода значительно повышается. Конечно, за неимением лучшего работать с ней можно, но...
А точно ли ничего лучшего нет?
МИКРО-ORM DAPPER — РАБОТАЕМ С БД MYSQL БЕЗ БУБНОВ И ПОРТЯНОК КОДА
А лучшее есть. И называется это лучшее — ORM (англ. Object-Relational Mapping, рус. объектно-реляционное отображение, или преобразование, или, мапинг на слэнге) — технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных» (Wiki). Существует много разных ORM, самые известные из которых Entity Framework от Microsoft и NHibernate. Проблема этих систем в огромном функционале (совершенно избыточном в нашем случае), а следовательно, сложности его освоения (именно такое мнение я читал о них в инете).
А еще есть небольшая либа, «микро-ORM» как позиционируют ее авторы, и называется она Dapper. Она обеспечивает сопоставление классов таблицам БД, сохраняя при этом высокую производительность и многопотчность, а еще упрощает выполнение запросов, и работает из коробки, без мучительных настроек. Dapper был создан для Stack Overflow — одного из самых посещаемых сайтов, созданных на продуктах Microsoft, К тому же, она абслоютно бесплатна.
Итак, ниже пойдет речь о работе с библиотекой Dapper, а так же ее расширении Dapper.Contrib написанной теми же разработчиками. Основная задача Dapper — сопоставлять (мапить) данные программы C# с таблицами БД, а помимо мапинга, Dapper еще и берет на себя часть работы по созданию объектов MySql Connector, необходимых для подключения к БД. Dapper.Contrib дополняет функционал Dapper и упрощает выполнение некоторых запросов, СОВСЕМ освобождая от необходимости писать SQL (хотя и теряя при этом в гибкости).
Идеальный вариант это их совместное использование — иногда что-то удобнее выполнить на Dapper, иногда на Contrib, а в комплекте получается незаменимая штука, экономящая километры строк кода.
Но хватит лирики, пора окунаться в магию Dapper. А чтобы острее почувствовать ее, начнем с рутины — настройки проекта.
Для работы с БД MySQL нам понадобится установленный на ПК MySQL (у меня стоит Open Server) или доступ к удаленному хосту с MySQL, а так же следующие библиотеки:
- MySQL Connector (MySql.Data.xml, MySql.Data.dll). Я приложил файлы в архив. Если они не подойдут (у меня так было с либой из предыдущей статьи) можно поставить на ПК MySQL Connector/NET, и взять либу из него (в моем случае с умолчальными путями это папка Program Files (x86)\MySQL\Connector NET 8.0\Assemblies\v4.5.2)
- Библиотека Dapper (Dapper.dll, Dapper.xml). Опять же добавил во вложение. И ссыль на Nuget актуальной на данный момент версии. Из этого пакета я ставил версию net461.
- Библиотека Dapper.Contrib(Dapper.Contrib.dll, Dapper.Contrib.xml) Как и в предыдущих пунктах, добавлена во вложении. Ссылка на загрузку nuget-пакета, версия та же net461.
- Файлы библиотек (и xml тоже) закидываем в папку ExternalAssemblies (Путь до программы\ZennoLab\RU\ZennoPoster Pro V7\7.4.0.0\Progs\ExternalAssemblies)
- Создаем новый шаблон.
- Выбираем Добавить ссылки из GAC/Добавить/Обзор
- Добавляем MySql.Data.dll, Dapper.dll, Dapper.Contrib.dll.
- Открываем «Директивы using и общий код»/вкладка Директивы using, и прописываем следующие простраства имен:
C#:
using Dapper;
using Dapper.Contrib;
using Dapper.Contrib.Extensions;
using MySql.Data.MySqlClient;
Наш проект готов к работе.
Ну а теперь пора приступать к изучению возможностей Dapper и Dapper.Contrib.
Мы рассмотрим лишь основные доступные у Dapper методы для отправки-получения объектов(или отдельных их свойств), иначе может получиться роман в нескольких книгах:
- Execute (аналог ExecuteNonQuery в MySQL Connector) — подходит для отправки запросов INSERT, UPDATE, DELETE (в ответе приходит int число, показывающее, сколько строк было затронуто выполненным запросом).
- ExecuteScalar (аналог ExecuteScalar в MySQL Connector) - возвращает значение одного из полей (столбцов, аналог ячейки таблицы) одной строки — по усолчанию в формате object, так что желательно указывать тип данных, который ожидается.
- Query — возвращает IEnumerable коллекцию строк. Позволяет получать из БД многострочные выборки SQL-запросами SELECT.
- QuerySingle — возвращает одну строку из таблицы БД в указанный тип данных. Если запрос составлен так, что в ответе возвращается более 1 строки или ни одной — получим исключение. Для однострочных выборок по условию (SELECT).
- Get — возвращает единичную строку по ее id
- GetAll — возвращает в список объектов указанного типа все записи соответствующей таблицы.
- Insert — добавляет в соотв. таблицу один или несколько объектов указанного типа. Ответ возвращае в виде числа типа long, и соотв. количеству добавленных строк.
- Update — обновляеет одну или несколько строк, сопоставляя указанный объект(или объекты) соотв. таблице БД. Ответ — тип bool обозначающий удалось (true) или нет (false) обновить указанную строку(строки).
- Delete — одну или несколько строк по указанному условию.
- DeleteAll - удаляет все строки указанного типа данных.
Во вложенном архиве имеется шаблон dapperlearn_mysql.zp в котором есть все приведенные ниже примеры работы с dapper и dapper.contrib с подробными коментами. Структура шаблона выглядит следующим образом: В самой левой колонке создается тестовая БД с таблицами user и proxy, proxyserver, таблицы наполняются тестовой информацией. В трех следующих колонках приведены примеры одного и того же запроса на MySQL Connector/Dapper/Dapper.Contrib. Все запросы которые я публикую здесь в урезанном виде (без строки подключения и создания объектов) там представлены полностью. И последний раздел — это пример работы с методами, написанными в общем коде для класса Proxyserever (о нем чуть ниже), с целью облегчить взаимодействие с данными класса.
Выше я показывал, как выглядит запрос на добавление списка объектов Proxy в БД (INSERT). Теперь сделаем это на Dapper:
C#:
//Строка подключения к БД
string connString = String.Format("Data Source={0};UserId={1};Password={2};database={3};Charset={4};SSL Mode=None",
project.Variables["DB_host"].Value,
project.Variables["DB_user"].Value,
project.Variables["DB_pass"].Value,
project.Variables["DB_name"].Value,
project.Variables["DB_charset"].Value);
//Список объектов Proxy, который наполняется тестовыми данными в шаблоне с примерами
List<Proxy> proxyList = new List<Proxy>();
string query = "INSERT INTO proxy VALUES (@id, @protocol, @ip, @login, @pass);";
using(MySqlConnection conn = new MySqlConnection(connString))
{
conn.Open();
int resp = conn.Execute(query, proxyList);
}
Специально посчитал запрос через MySQL Connector от строки с директивой using до закрываюшей ее скобки — получилось 22 строки. А на dapper 5 строчек — разница более чем в 4 раза. Теперь, тот же запрос на Contrib:
C#:
//Строку connString подключения не указываю, но она есть!
//список объектов Proxy содержит некоторый набор объектов клаасса Proxy с валидными св-вами
List<Proxy> proxyList = new List<Proxy>();
SqlMapperExtensions.TableNameMapper = (type) => type.Name;
//отправляем в БД список объектов User в те же 2 строки кода что и с dapper.contrib
using(MySqlConnection conn = new MySqlConnection(connString))
{
conn.Open();
long resp = conn.Insert(proxyList);
}
Тоже неплохо, верно? Хочу обратить внимание — в данном случае самого SQL-запроса нет вообще, все делает Dapper.Contrib.
Здесь следует пояснить назначение строки
SqlMapperExtensions.TableNameMapper = (type) => type.Name;Dapper.Contrib по-умолчанию выполняет сопостовление по следующей схеме — получает имя класса которое нужно передать в БД, добавляет к нему англ. букву "s" (Proxy => proxys, User => users), и пытается найти в подключенной БД таблицу с этим именем. А так как сам Dapper сопоставляет класс и таблицу без изменений (Proxy => proxy, User => user), то в Contib предусмотрен механизм, позволяющий восстановить прямое сопоставление, как в Dapper. Вот эта директива и показывает Contib, что мапить нужно по имени класса, без лишних "s".
Написать ее нужно всего лишь один раз (например где-то в начале шаблона) и она будет работать до перезагрузки программы. Так же, у Contrib можно подменить имя класса, подставив любое указанное имя(Proxy => proxyserver, User => people). Это сработает при условии, что объект имеет свойства и их названия, соотв. колонками и названиям таблицы. Для того чтобы замапить, например, класс Proxy в таблицу proxyserver (и попутно отменить сопоставление по умолчанию):
C#:
//такой код, если принимает тип данных (type) с именем Proxy, вернет имя ProxyServer (т.е. поиск таблицы будет выполняться именно имени ProxyServer, а не по имени класса Proxy)
//Во всех остальных случаях, будет возвращено имя соответствующего типа данных (а в случае если в запрос передается интерфейс, то у него будет отрезана 1-я буква "I" — IProxy => Proxy)
SqlMapperExtensions.TableNameMapper = (type) =>
{
switch (type.Name)
{
case "Proxy":
return "ProxyServer";
default:
var name = type.Name;
if (type.IsInterface && name.StartsWith("I"))
name = name.Substring(1);
return name;
}
};
Еще одно важное замечание — я не нашел однозначной информации о блокировании таблиц БД Dapper-ом и Contib при вставке и обновлении данных. Поэтому, при его использовании в рабочих проектах, наверное лучше перебздеть и залочить их принудительно, чтобы не было мучительно больно, для этого перед запросом к данным, следует выполнить еще один на блокировку, а после, на разблокировку таблицы.
Продолжим изучать запросы. Теперь вставим в таблицу один объект Proxy (INSERT). Dapper:
C#:
//Напомню — строка подключения не указана, но она есть,
//а объект Proxy у нас не пустой и обладает валидными значениями для каждого свойства
Proxy proxy = new Proxy();
string query = "INSERT INTO proxy (protocol, ip, login, pass) VALUES (@protocol, @ip, @login, @pass)";
int res; //ответ
using(var conn = new MySqlConnection(connString))
{
conn.Open();//открыли сессию
//Отправили запрос Execute,
res = conn.Execute(query, proxy);
}
Тот же запрос — Dapper.Contrib:
C#:
//Напомню — строка подключения не указана, но она есть,
//а объект Proxy у нас не пустой и обладает валидными значениями для каждого свойства
Proxy proxy = new Proxy();
//Ищем таблицу по имени типа данных
SqlMapperExtensions.TableNameMapper = (type) => type.Name;
long res; //ответ
//создали объект класса MySql.Data.MySqlClient.MySqlConnection и подключились к БД, передав ему в качестве параметра строку подключения
using(var conn = new MySqlConnection(connString))
{
conn.Open();//открыли сессию
//Отправили запрос Insert
res = conn.Insert<Proxy>(proxy);
}
Как мы видим, вставка объектов и списков с ними проста и там и там. А в contrib еще и SQL-запросы писать не нужно. И тогда нафига он нужен, чистый dapper, Но вот заметил какую штуку — метод Insert вставляет строку в БД железобетонно, независимо от того, есть там уже такая запись или нет. Так что, в рабочем проекте, перед вставкой, стоит проверить наличие по какому-либо уникальному свойству, а для этого нужен чистый dapper. Так же недостатки contib можно увидеть на запросах, которые делают выборку из БД — он может или получить одну строку по id, или все строки таблицы.
Чтобы не расслабляться, теперь поработаем с таблицей user и классом User:
C#:
/// <summary>
/// Тестовый класс для разбора работы с Dapper и Contrib
/// </summary>
public class User
{
public int id {get; set;}
public string login {get; set;}
public string pass {get; set;}
public string email {get; set;}
}
У таблицы user, имеются колонки с наименованиями и типами данных соответствующими именам и типам данных объекта User. Посмотрим как выглядит запрос UPDATE по условию на Dapper :
C#:
//Напомню — строка подключения не указана, но она есть,
//а список с User у нас не пустой и каждый объект списка обладает валидными значениями для каждого свойства
List<User> usrNewList = new List<User>();
//sql-запрос
string query = "UPDATE user SET login = @login, pass = @pass, email = @email WHERE login LIKE 'i%';";
long res = 0; //ответ
//С dapper это легко
using(var conn = new MySqlConnection(connString))
{
conn.Open();//открыли сессию
//обновили все записи, начинающиеся на i
res = conn.Execute(query, usrNewList);
}
Такой запрос обновит все найденные записи начинающиеся на указанную букву, строками из списка. Правда, если записей будет найдено больше чем объектов в списке — появятся дубли (но это проблема запроса, а не dapper) А теперь contrib:
C#:
//Напомню — строка подключения не указана, но она есть,
//а список с User у нас не пустой и каждый объект списка обладает валидными значениями для каждого свойства
List<User> usrNewList = new List<User>();
//Указываем, что нужно сопоставить имя класса таблице с соотв. именем
SqlMapperExtensions.TableNameMapper = (type) => type.Name;
bool res; //ответ
//Методами dapper.contrib задача не решается
using(var conn = new MySqlConnection(connString))
{
conn.Open();//открыли сессию
//получили все строки из таблицы user
List<User> usrTmpList = conn.GetAll<User>().ToList();
List<User> usrOldList = usrTmpList.Where(u => u.login.StartsWith("y")).Take(3).ToList();
foreach(User unew in usrNewList)
{
foreach(User uold in usrOldList)
{
unew.id = uold.id;
}
}
//обновили выбранные записи
res = conn.Update(usrTmpList);
}
Тут уже начинается чехарда — методов для выборки по условию в contrib нет, поэтому приходится получать все строки из таблицы, LINQ-ом выбирать строки начинающиеся на нужную букву, менять id у тех объектов, которыми мы будем обновлять выборку на тот id у которых мы будем менять, короче тяжко все это (допускаю, что можно сделать изящнее, но чет в попыхах не придумалось).
Обновление по id. Dapper:
C#:
//Напомню — строка подключения не указана, но она есть,
//а объект User у нас не пустой и обладает валидными значениями для каждого свойства
User usr = new User();
//Запрос к БД
string query = "UPDATE user SET login = @login, pass = @pass, email = email WHERE id=10;";
int res; //ответ
//создали объект класса MySql.Data.MySqlClient.MySqlConnection и подключились к БД, передав ему в качестве параметра строку подключения
using(var conn = new MySqlConnection(connString))
{
conn.Open();//открыли сессию
//Отправили запрос Execute, сопоставили полученную строку объекту класса Proxy. Если запрос вернул более 1 строки — получили исключение
res = conn.Execute(query, usr);
}
В ответе dapper возвращает значение int, которое указывает кол-во строк, затронутых запросом. Теперь contrib:
C#:
//Напомню — строка подключения не указана, но она есть,
//а объект User у нас не пустой и обладает валидными значениями для каждого свойства
User usr = new User();
//Указываем, что нужно сопоставить имя класса таблице с соотв. именем
SqlMapperExtensions.TableNameMapper = (type) => type.Name;
bool res; //ответ
//создали объект класса MySql.Data.MySqlClient.MySqlConnection и подключились к БД, передав ему в качестве параметра строку подключения
using(var conn = new MySqlConnection(connString))
{
conn.Open();//открыли сессию
//Отправили запрос. В данном случае, обновление происходит по id, поэтому свойство id класса User не должно быть пустым
//Иначе запрос не будет выполнен, но исключения не вызовет, вернув в переменную ответа false
res = conn.Update(usr);
}
Вывод: Contrib удобнее использовать, когда не нужно выполнять действия, связанные с выборками данных из БД. А еще лучше, когда выборка получается на dapper, а обновление — на contrib.
В примере ниже у нас есть класс Proxyserver и соотв. ему таблица. В отличии от Proxy из пред. примера у таблицы proxyserver есть доп. колонка(а у объекта св-во) - "usenow", принимающее значение bool, и обозначающее что прокся в работе, и др. потокам получить ее нельзя если она true, и что брать можно, если false, и попробуем получить несколько проскей по условию usenow = false, после чего установим его в true для выбранных записей:
Комбинированный запрос (SELECT WHERE + UPDATE) Dapper + Contrib:
C#:
//новый класс с колонкой isuse в общем коде
public class Proxyserver
{
public int id {get; set;}
public string protocol {get; set;}
public string ip {get; set;}
public string login {get; set;}
public string pass {get; set;}
public bool isuse {get; set;}
}
Комбинированный запрос (SELECT WHERE + UPDATE) Dapper + Contrib:
C#:
string query = "SELECT * FROM proxyserver WHERE isuse=false LIMIT 5;";
//отправляем в БД список объектов User в те же 2 строки кода что и с dapper.contrib
using(MySqlConnection conn = new MySqlConnection(connString))
{
conn.Open();
//получили выборку из БД на dapper
List<Proxyserver> tmp = conn.Query<Proxyserver>(query).ToList();
//установили у полученных объектов св-во isuse=true
tmp.ForEach(x => x.isuse = true);
//обновили записи через contrib
conn.Update(tmp);
}
Вернемся к нашим объектам Proxy и таблице proxy в БД и продолжим изучение и сравнение методов Dapper и Contrib. Получение (SELECT) всех строк из таблицы на Dapper:
C#:
//Напомню — строка подключения не указана, но она есть
//Пустой список объектов Proxy
List<Proxy> proxyList = new List<Proxy>();
using(var conn = new MySqlConnection(connString))
{
conn.Open();//открыли сессию
//Отправили запрос, сопоставили каждую полученную строку объекту класса Proxy и сохранили все получившиеся объекты в список
proxyList = conn.Query<Proxy>(request).ToList<Proxy>();
}
То же на Contib:
C#:
//Напомню — строка подключения не указана, но она есть
//Пустой список объектов Proxy
List<Proxy> proxyList = new List<Proxy>();
//Указываем, что нужно сопоставить имя класса таблице с соотв. именем
SqlMapperExtensions.TableNameMapper = (type) => type.Name;
//создали объект класса MySql.Data.MySqlClient.MySqlConnection и подключились к БД, передав ему в качестве параметра строку подключения
using(var conn = new MySqlConnection(connString))
{
conn.Open();//открыли сессию
//Отправили запрос, сопоставили каждую полученную строку объекту класса Proxy и сохранили все получившиеся объекты в список.
proxyList = conn.GetAll<Proxy>().ToList();
}
Получение одной строки показывать здесь не буду, примеры есть в шаблоне. Повторю лишь, что используемый для этого метод Dapper (QuerySingle) вернет ошибку, если в ответе будет более 1 строки.
Далее опять будем работать с объектами User и таблицей с соотв. его св-вам колонками id, login, pass, email (я их в самом начале сделал, не простаивать же им). Получение 1-го поля из одной строки (аналог ExecuteScalar MySQL Connector).
C#:
//ЗАДАЧА: получить поле email таблицы user, у которого значение столбца id равно usr.id
User usr = new User()
{
id = 17
};
//sql-запрос
string query = "SELECT email FROM user WHERE id = @id;";
using(var conn = new MySqlConnection(connString))
{
//создали экземпляр объекта Dapper.DynamicParameters
DynamicParameters param = new DynamicParameters();
//добавили в него параметр @id со значением usr.id
param.Add("@id", usr.id);
conn.Open();//открыли сессию
//получили значение поля email по id
usr.email = conn.ExecuteScalar<string>(query, param);
}
Напомню, Contrib не предусмотрен для получения выборок, поэтому здесь мы получаем весь объект (строку таблицы) и возвращаем нужное нам свойство этого объекета:
C#:
//ЗАДАЧА: получить поле email таблицы user, у которого значение столбца id равно usr.id
User usr = new User()
{
id = 17
};
//Указываем, что нужно сопоставить имя класса таблице с соотв. именем
SqlMapperExtensions.TableNameMapper = (type) => type.Name;
using(var conn = new MySqlConnection(connString))
{
conn.Open();//открыли сессию
//получили обект User по id
usr = conn.Get<User>(usr.id);
}
return usr.email;
Следующее действие — удаление по условию нескольких строк из таблицы (SQL - DELETE). Пример по удалению одной строки здесь показывать не буду, есть в шаблоне. На Dapper:
C#:
//Напомню — строка подключения не указана, но она есть
string query = "DELETE FROM user WHERE login LIKE 'c%';";
int res;
using(var conn = new MySqlConnection(connString))
{
conn.Open();//открыли сессию
//удалили строку по id
res = conn.Execute(query);
}
return res.ToString();
То же на Contib:
C#:
//Указываем, что нужно сопоставить имя класса таблице с соотв. именем
SqlMapperExtensions.TableNameMapper = (type) => type.Name;
bool del = false;
using(var conn = new MySqlConnection(connString))
{
conn.Open();//открыли сессию
//получили все строки из табл.
List<User> response = conn.GetAll<User>().ToList();
//выбрали из них те, что начинаются на "u"
List<User> delUsr = response.Where(u => u.login.StartsWith("u")).ToList();
//удалили
foreach(User usrTodel in delUsr)
{
del = conn.Delete(usrTodel);
if(del) res += 1; //получаем количество удаленных строк если ответ true
}
}
return del.ToString();
Ну и напоследок метод DeleteAll Contrib, удаляющий все строки из таблицы, которой соотв. указанный объект. Тут все совсем просто:
C#:
bool res = false;
using(var conn = new MySqlConnection(connString))
{
conn.Open();//открыли сессию
//удалили из БД все строки соотв. классу User
res = conn.DeleteAll<User>();
}
В завершении статьи предлагаю вернуться к классу Proxyserver из примера комбинированного запроса, и немного расширить его функционал, чтобы с ним было удобно работать.
На всякий случай повторю условия:
у нас имеется класс Proxyserver со следующими свойствами — id, protocol, ip, port, login, pass, isuse, и таблица proxyserver в БД, с колонками, наименования которых соотв. именам свойств класса. Описание класса добавляем в общий код, создав для него пространство имен DataClasses.
Не забываем прописать в директивы using наше новое простраство имен —
using DataClasses; В примерах выше мне настолько не нравилось писать строку подключения к БД, что вместо нее я писал комментарий. Чтобы не заниматься этой писаниной и дальше, сделаем свой конструктор класса, в котором будет создаваться эта строка, а так же будет отменяться сопсотавление данных по-умолчанию для Contrib. Отдельно создадим и пустой конструктор, чтобы можно было создать пустой объект Proxyserver.
И напишам несколько методов для получения и обновления данных из/в БД, для передачи нужных свойств в строку в формате protocol://login:pass@ip:port и присвоения значений свойствам объекта из такой строки. В итоге у меня получился такой класс Proxyserver в общем коде:
C#:
namespace DataClasses
{
using Dapper;
using Dapper.Contrib;
using Dapper.Contrib.Extensions;
/// <summary>
/// Класс выполняет операции с БД и подгтовку проки к использованию
/// </summary>
public class Proxyserver
{
public int id {get; set;}
/// <summary>
/// протокол работы
/// </summary>
public string protocol {get; set;}
/// <summary>
/// ip-адрес прокси
/// </summary>
public string ip {get; set;}
/// <summary>
/// Порт прокси
/// </summary>
public int port{get; set;}
/// <summary>
/// логин
/// </summary>
public string login {get; set;}
/// <summary>
/// пароль
/// </summary>
public string pass {get; set;}
/// <summary>
/// Св-во показывает, доступен ли в БД текущий объект Proxy для получения другим потокам
/// </summary>
public bool isuse {get; set;}
public string connString;
private IZennoPosterProjectModel project;
/// <summary>
/// Пустой конструктор класса
/// </summary>
public Proxyserver()
{
}
/// <summary>
/// Конструктор класса. Создает строку подключения к ДБ и отменяет мапинг по умолчанию для Dapper.Contrib
/// </summary>
/// <param name="project"></param>
public Proxyserver(IZennoPosterProjectModel project)
{
Dapper.Contrib.Extensions.SqlMapperExtensions.TableNameMapper = (type) => type.Name;
this.project = project;
connString = String.Format("Data Source={0};UserId={1};Password={2};database={3};Charset={4};SSL Mode=None",
project.Variables["DB_host"].Value,
project.Variables["DB_user"].Value,
project.Variables["DB_pass"].Value,
project.Variables["DB_name"].Value,
project.Variables["DB_charset"].Value);
}
/// <summary>
/// Метод возвращает в текущий объект строку таблицы с указанным в параметре типом протокола, и выставляет в этой строке БД isuse = true
/// </summary>
/// <param name="protocolparam">тип протокола, строку с которым нужно получить </param>
/// <param name="isuseparam">Значение ячейки isuse в БД, по умолчанию false</param>
public void GetProxyserverFromDb(string protocolparam, bool isuseparam=false)
{
string query = "SELECT * FROM proxyserver WHERE protocol = @protocol AND isuse = @isuse LIMIT 1;";
Proxyserver proxy = new Proxyserver();
using(var conn = new MySql.Data.MySqlClient.MySqlConnection(connString))
{
conn.Open();
proxy = conn.QuerySingle<Proxyserver>(query, new{protocol = protocolparam, isuse = protocolparam});
this.id = proxy.id;
this.protocol = proxy.protocol;
this.ip = proxy.ip;
this.port = proxy.port;
this.login = proxy.login;
this.pass = proxy.pass;
conn.Execute("UPDATE proxyserver SET isuse = true WHERE id=@id", proxy);
proxy = null;
}
}
/// <summary>
/// Метод возвращает строку прокси вида protocol://login:pass@ip:port
/// </summary>
/// <returns></returns>
public string ProxyserverToString()
{
if(string.IsNullOrEmpty(this.protocol) || string.IsNullOrEmpty(this.ip) || string.IsNullOrEmpty(this.port.ToString())) throw new Exception("protocol or ip or port is empty!");
string tostr = string.Format("{0}{1}:{2}@{3}:{4}", this.protocol, this.login, this.pass, this.ip, this.port.ToString());
return tostr;
}
/// <summary>
/// Метод разбирает строку с прокси вида protocol://login:pass@ip:port на объект Proxyserver
/// </summary>
/// <param name="proxystring">строка с прокси вида protocol://login:pass@ip:port</param>
public void ProxyserverFromString(string proxystring)
{
string protoreg = @".*//";
this.protocol = new Regex(protoreg).Match(proxystring).Value;
proxystring = ZennoLab.Macros.TextProcessing.Replace(proxystring, protoreg, "", "Regex", "First");
string[] split = proxystring.Split('@');
this.login = split[0].Split(':')[0];
this.pass = split[0].Split(':')[1];
this.ip = split[1].Split(':')[0];
this.port = Int32.Parse(split[1].Split(':')[1]);
}
/// <summary>
/// Метод обновляет в БД поле isuse, присваивая ему значение false
/// </summary>
public void ReturnProxyserverToDb()
{
using(var conn = new MySql.Data.MySqlClient.MySqlConnection(connString))
{
conn.Open();
conn.Update(new Proxyserver{
id = this.id,
protocol = this.protocol,
ip = this.ip,
port = this.port,
login = this.login,
pass = this.pass,
isuse = this.isuse});
this.project.SendInfoToLog("SET isuse = false item where id = " + this.id, false);
}
}
}
}
Методы Dapper и Contrib оказались недоступны внутри namespace DataClasses, пришлось прописать using.
А работа с объектами класса в кубиках выглядит теперь так (коменты здесь в принципе не нужны, т.к. они присутствуют в общем коде):
C#:
//Создали новый объект класса Proxyserver
Proxyserver proxy = new Proxyserver(project);
//Получили из БД строку у которой protocol = 'socks5://', а isuse=false
//и передали ее в текущий объект
proxy.GetProxyserverFromDb("socks5://");
//передали объект proxy в переменную проекта, в виде требуемом для использования
project.Variables["PROXY_string"].Value = proxy.ProxyserverToString();
//сохранили в переменную проекта proxy.id для дальнейшей работы
project.Variables["PROXY_id"].Value = proxy.id.ToString();
project.SendInfoToLog(proxy.id.ToString() + " - " + prxy, false);
//освободили память от объекта proxy
proxy = null;
И получение использованной строки прокси в объект и разблокировка соотв. строки в таблице БД:
C#:
//Создали новый объект класса Proxyserver
Proxyserver proxy1 = new Proxyserver(project);
//Разобрали строку из переменной проекта в объект Proxyserver
proxy1.ProxyserverFromString(project.Variables["PROXY_string"].Value);
//получили proxy1.id из переменной проекта
proxy1.id = Int32.Parse(project.Variables["PROXY_id"].Value);
//установили proxy1.isuse = false, чтобы прокся была доступна др. потокам в БД
proxy1.isuse = false;
//Обновили запись в БД
proxy1.ReturnProxyserverToDb();
project.SendInfoToLog(proxy1.id.ToString() + " - " + proxy1.ip, false);
//освободили память от объекта proxy
proxy1 = null;
Сюда же, в методы класса можно было бы добавить еще проверку полученной строки на работоспособность, и перед сохраненением прокси в переменную проекта проверять, что она рабочаяя, но я ведь не рабочий инструмент писал, а показывал возможности, так что это оставлю для самостоятельной работы.
Спасибо за внимание всем, кто осилил до конца

- Номер конкурса статей
- Шестнадцатый конкурс статей
- Тема статьи
- Нестандартные хаки
Вложения
Последнее редактирование модератором:





Перескажу кратко — работа с MySQL через стандартную либу MySqlData.dll достаточно неудобна и требует написания большого количества кода, что ухудшает читаемость кода, усложняет его поддержку и изменение. А при создании собственных классов и использовании dapper все значительно упрощается — отправлять SQL-запросы удобнее, код чище, логика шаблона нагляднее.
