- Регистрация
- 27.12.2016
- Сообщения
- 289
- Благодарностей
- 404
- Баллы
- 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;}
}
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;}
}
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;
Сюда же, в методы класса можно было бы добавить еще проверку полученной строки на работоспособность, и перед сохраненением прокси в переменную проекта проверять, что она рабочаяя, но я ведь не рабочий инструмент писал, а показывал возможности, так что это оставлю для самостоятельной работы.
Спасибо за внимание всем, кто осилил до конца
- Тема статьи
- Нестандартные хаки
- Номер конкурса статей
- Шестнадцатый конкурс статей
Вложения
-
872,1 КБ Просмотры: 262
Для запуска проектов требуется программа ZennoPoster или ZennoDroid.
Это основное приложение, предназначенное для выполнения автоматизированных шаблонов действий (ботов).
Подробнее...
Для того чтобы запустить шаблон, откройте нужную программу. Нажмите кнопку «Добавить», и выберите файл проекта, который хотите запустить.
Подробнее о том, где и как выполняется проект.
Последнее редактирование модератором: