5 место Z-TehnOman Part1. Cобственные классы, MySQL и Dapper — взаимодействие с БД проще, код чище, а нервы крепче

semafor

Client
Регистрация
27.12.2016
Сообщения
288
Благодарностей
385
Баллы
63
Всем добра и веселого Нового года без последствий!

Каждый, кто коннектил свои шаблоны с MySQL, используя стандартный MySQL Connector/NET, работа с которым описана в одном из предыдущих конкурсов, знает — чтобы в рамках 1 сессии выполнить работу с БД, надо написать полкилометра кода, в дебрях которого легко потерять и логику шаблона и собственный мозг. А если для взаимодействия со страницей нужно оперировать десятком-другим переменных, время от времени пихать и извлекать их в/из БД? А если шаб используется регулярно и развивается? А если клиенты, которые его юзают, периодически просят добавить очередную хотелку, а сайт-донор с завидной регулярностью меняет верстку? Жуть какая-то! Как же при такой жизни найти место для прекрасной незнакомки, стаканчика-другого вискаря с друзьями, интересных путешествий и всего прочего, что рисовало воображение?!

В статье пойдет речь о том, как упростить взаимодействие с БД, структурировать данные в шаблоне, сделать код чище, а расширение функционала и поддержку шаблона проще. А делать это мы будем используя самописные классы в общем коде, БД MySQL и бесплатные библиотеки Dapper и Dapper.Contrib.

86583


И сразу пару слов о монетизации с использованием данного материала — чем проще написать и прочитать (особенно спустя время) код, чем легче его дебажить и редактировать, тем меньше времени на это нужно. А время, как известно — это деньги. И чем меньше времени затрачивается на написание и поддержку одного шаблона, тем больше можно заработать. По-моему так, как говаривал Винни-Пух из советского мультика.



ПОЛЬЗОВАТЕЛЬСКИЕ КЛАССЫ

Прежде чем начать рассказ об использовании собственных классов, приведу пример кода:

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, нам и через месяц будет понятно, что к чему, а написание комментариев в общем коде, которые отображаются в коде кубиков как всплывающие подсказки при наведениии курсора мыши, еще больше упрощают понимание.

86585


Еще один плюс к использованию своих классов — при объявлении нами экземпляра класса Product prod = new Product(); все его свойства инициализируются автоматически — их не нужно объявлять дополнительно.

Далее. Если объекты класса Product используются в нескольких разных кубиках шаблона, и нам вдруг понадобилось изменить, скажем xpath для получения описания товара, ну или добавить свойство metatitle, нам нужно будет править только описание класса в общем коде (правда получение свойства metetitle все же придется дописать в кубике).

Так же, внутри класса можно создать методы для работы со свойствами и полями членов класса, которые позволят еще сильнее упростить работу — ну например написать метод, который будет возвращать строковый массив свойств класса в нужном порядке для вставки в таблицу. И как можно было заметить выше, членами класса могут быть объявлены не только простые типы данных (string, int и т.д.), но и другие созданные нами классы. В приведенном мной примере, чтобы не усложнять его, я не стал собирать характеристики товара и его категорию. А ведь у категории тоже имеются свойства — как минимум, нам может понадобиться: имя и url (а еще id, изображение, описание, метатеги)— вот уже и готовый пример вложенного класса, т.к. пихать все это в класс Product, значит переместить свалку нетипизированных данных из кубика в созданный класс.

Несколько примеров типов данных:
  1. Прокси. Данный объект обладает следующими свойствами — протокол (http, https, soks4, soks5), ip, порт, логин, пароль. Сюда же можно добавить дату последнего использования, id акка, к которому он может быть привязан и т.д.
  2. Аккаунт и его свойства — login, password, email, телефон, id, состояние (рабочий, отлежка и т.д.), дата последнего использования
  3. Email — логин, пароль, имя, фамилия, контрольный вопрос, ответ на него, дата последнего получения почты и т.д.
  4. User (спаршенная инфа) — имя, фамилия, id, телефон, день рождения, ссылка на профиль, ссылка (или id) профиля мужа/жены и т.д.
  5. Много чего еще.
Правда, наиболее полно мощь такой организации данных раскроется не просто при взаимодействии с базой данных, а при возможности оперировать в работе с БД именно пользовательскими объектами. Но об этом в следующих частях.



БАЗА ДАННЫХ 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, а так же следующие библиотеки:

  1. MySQL Connector (MySql.Data.xml, MySql.Data.dll). Я приложил файлы в архив. Если они не подойдут (у меня так было с либой из предыдущей статьи) можно поставить на ПК MySQL Connector/NET, и взять либу из него (в моем случае с умолчальными путями это папка Program Files (x86)\MySQL\Connector NET 8.0\Assemblies\v4.5.2)
  2. Библиотека Dapper (Dapper.dll, Dapper.xml). Опять же добавил во вложение. И ссыль на Nuget актуальной на данный момент версии. Из этого пакета я ставил версию net461.
  3. Библиотека Dapper.Contrib(Dapper.Contrib.dll, Dapper.Contrib.xml) Как и в предыдущих пунктах, добавлена во вложении. Ссылка на загрузку nuget-пакета, версия та же net461.
Подготовка шаблона

  1. Файлы библиотек (и xml тоже) закидываем в папку ExternalAssemblies (Путь до программы\ZennoLab\RU\ZennoPoster Pro V7\7.4.0.0\Progs\ExternalAssemblies)
  2. Создаем новый шаблон.
  3. Выбираем Добавить ссылки из GAC/Добавить/Обзор
  4. Добавляем MySql.Data.dll, Dapper.dll, Dapper.Contrib.dll.
  5. Открываем «Директивы 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).
У Dapper.Contib рассмотрим следуюшие методы:
  • Get — возвращает единичную строку по ее id
  • GetAll — возвращает в список объектов указанного типа все записи соответствующей таблицы.
  • Insert — добавляет в соотв. таблицу один или несколько объектов указанного типа. Ответ возвращае в виде числа типа long, и соотв. количеству добавленных строк.
  • Update — обновляеет одну или несколько строк, сопоставляя указанный объект(или объекты) соотв. таблице БД. Ответ — тип bool обозначающий удалось (true) или нет (false) обновить указанную строку(строки).
  • Delete — одну или несколько строк по указанному условию.
  • DeleteAll - удаляет все строки указанного типа данных.
Прежде чем погрузиться в примеры, небольшое соглашение — после следующего примера я больше не буду указывать строку подключения connString, но не стоит забывать, что она у нас есть, и в нее добавлены необходимые для подключения к БД данные. А еще я не буду показывать как формируются объекты пользовательского типа данных (или списки с ними). И если я указываю что у нас есть объект Proxy, то мы подразумеваем, что свойства этого объекта не пусты (в случае с запросами INSERT, UPDATE).

Во вложенном архиве имеется шаблон dapperlearn_mysql.zp в котором есть все приведенные ниже примеры работы с dapper и dapper.contrib с подробными коментами. Структура шаблона выглядит следующим образом: В самой левой колонке создается тестовая БД с таблицами user и proxy, proxyserver, таблицы наполняются тестовой информацией. В трех следующих колонках приведены примеры одного и того же запроса на MySQL Connector/Dapper/Dapper.Contrib. Все запросы которые я публикую здесь в урезанном виде (без строки подключения и создания объектов) там представлены полностью. И последний раздел — это пример работы с методами, написанными в общем коде для класса Proxyserever (о нем чуть ниже), с целью облегчить взаимодействие с данными класса.

86586


Выше я показывал, как выглядит запрос на добавление списка объектов 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 для выбранных записей:

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:[email protected]: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 [email protected]", proxy);
                proxy = null;
            }
        }
      
        /// <summary>
        /// Метод возвращает строку прокси вида protocol://login:[email protected]: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:[email protected]:port на объект Proxyserver
        /// </summary>
        /// <param name="proxystring">строка с прокси вида protocol://login:[email protected]: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;

Сюда же, в методы класса можно было бы добавить еще проверку полученной строки на работоспособность, и перед сохраненением прокси в переменную проекта проверять, что она рабочаяя, но я ведь не рабочий инструмент писал, а показывал возможности, так что это оставлю для самостоятельной работы.


Спасибо за внимание всем, кто осилил до конца 8-)
 
Тема статьи
Нестандартные хаки
Номер конкурса статей
Шестнадцатый конкурс статей

Вложения

Для запуска проектов требуется программа ZennoPoster или ZennoDroid.
Это основное приложение, предназначенное для выполнения автоматизированных шаблонов действий (ботов).
Подробнее...

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

Последнее редактирование модератором:

AndrewDev

Client
Регистрация
19.03.2021
Сообщения
82
Благодарностей
6
Баллы
8
Вау вкустно!
Я изначально был на этапе написания sql запросов через либу, но просто решил психануть и написать сервис для взаимодействия с бд на .Net Core, где ты был раньше?)
Про дапер вовсе забыл
Материал Круто подан
 
  • Спасибо
Реакции: semafor

semafor

Client
Регистрация
27.12.2016
Сообщения
288
Благодарностей
385
Баллы
63
Вау вкустно!
Я изначально был на этапе написания sql запросов через либу, но просто решил психануть и написать сервис для взаимодействия с бд на .Net Core, где ты был раньше?)
Про дапер вовсе забыл
Материал Круто подан
Ахаха! У меня ровно наоборот вышло — сел писать свой мапер, но психанул и вспомнил про дапер )))
 

radv

Client
Регистрация
11.05.2015
Сообщения
2 917
Благодарностей
1 509
Баллы
113
Полезная информация. :ay:
 
  • Спасибо
Реакции: semafor

semafor

Client
Регистрация
27.12.2016
Сообщения
288
Благодарностей
385
Баллы
63
Ценная информация! У меня мосг перегрелся на половине текста, за один заход не осилить
:bf: Перескажу кратко — работа с MySQL через стандартную либу MySqlData.dll достаточно неудобна и требует написания большого количества кода, что ухудшает читаемость кода, усложняет его поддержку и изменение. А при создании собственных классов и использовании dapper все значительно упрощается — отправлять SQL-запросы удобнее, код чище, логика шаблона нагляднее.
 
  • Спасибо
Реакции: Deiccide и Alexmd

Viking01

Client
Регистрация
19.08.2017
Сообщения
219
Благодарностей
149
Баллы
43
Спасибо за даппер, даже не знал про него.
Хоть одна действительно полезная статья в этом конкурсе :-)
По мне, конечно, не настолько красиво, как было бы с Entity Framework, но все равно проще, чем запросы и ридеры ручками писать.
А INSERT IGNORE или INSERT ... ON DUBLICATE KEY UPDATE он поддерживает?

Насчет локов тоже интересно, есть ли лок строк к примеру, SELECT ... FOR UPDATE ?
 
  • Спасибо
Реакции: semafor

semafor

Client
Регистрация
27.12.2016
Сообщения
288
Благодарностей
385
Баллы
63
Хоть одна действительно полезная статья в этом конкурсе
Спасибо за лестный отзыв.
По мне, конечно, не настолько красиво, как было бы с Entity Framework
По сравнению с Entity dapper значительно быстрее, даже с нахлобучками типа contrib-ов и прочих crud-ов (а их дофига всяких понаписано)
А INSERT IGNORE или INSERT ... ON DUBLICATE KEY UPDATE он поддерживает?
И INSERT IGNORE и INSERT ... ON DUBLICATE KEY UPDATE точно поддерживает

Про SELECT ... FOR UPDATE пока не отвечу
 

ZennoLab Team

Super Moderator
Команда форума
Регистрация
22.01.2019
Сообщения
691
Благодарностей
2 778
Баллы
93
Добавлено видео от автора.
Размещено в конце первого поста.
 

semafor

Client
Регистрация
27.12.2016
Сообщения
288
Благодарностей
385
Баллы
63
Тут эта... Несколько раз меня накрывало, что чего-то в статье не хватает, но все не мог догнать чего. И сегодня осенило — ведь ссылки!!!
1. Github Dapper. Есть примеры, например по работе с динамическими объектами, еще интересен раздел вопросов(issues). Ну и код даппера тоже можно поковырять.
2. Github Dapper.Contrib. Тоже примеры, вопросы и код изнутри
3. Dapper Tutorial. Здесь есть примеры по самому Dapper, по Contrib, а так же по еще нескольким полезным либам дополняющим dapper.
4. StackOverflow наше все — тут множество примеров с коментами и подробными пояснениями. Правда не по-нашему, но код есть код
 
  • Спасибо
Реакции: djaga

bigloafer

Client
Регистрация
23.07.2020
Сообщения
230
Благодарностей
71
Баллы
28
Вот есть MySql и SqlLite. Будет PostgreSql?
 
  • Спасибо
Реакции: semafor

semafor

Client
Регистрация
27.12.2016
Сообщения
288
Благодарностей
385
Баллы
63
Вот есть MySql и SqlLite. Будет PostgreSql?
На самом деле, методы либ абсолютно одинаковы с любой реляционной СУБД. Различия только в стандартных библиотеках, сопрягающих NET и БД. Я посмотрю, что покажет голосование — есть ли смысл тратить время на подробные мануалы, если они не очень востребованы в комьюнити
 
  • Спасибо
Реакции: bigloafer

Phoenix78

Client
Регистрация
06.11.2018
Сообщения
10 735
Благодарностей
5 120
Баллы
113
На самом деле, методы либ абсолютно одинаковы с любой реляционной СУБД. Различия только в стандартных библиотеках, сопрягающих NET и БД. Я посмотрю, что покажет голосование — есть ли смысл тратить время на подробные мануалы, если они не очень востребованы в комьюнити
на текущий момент, судя по голосованию, техническая часть шаблонов, чуть ли не на первом месте оценивается.
 
  • Спасибо
Реакции: semafor

semafor

Client
Регистрация
27.12.2016
Сообщения
288
Благодарностей
385
Баллы
63
на текущий момент, судя по голосованию, техническая часть шаблонов, чуть ли не на первом месте оценивается.
Спасибо за инсайд )) Я в начале голосования посмотрел, и пока больше не заглядывал — и со временем как всегда напряг, да и раньше срока чего заглядывать.

Своим появлением на конкурсе эта статья обязана тем, что мне самому понадобилось как-то упростить мапинг данных в БД, а так как я частенько пишу памятки по работе с тем или иным инструментом для себя, решил попутно накалякать и для сообщества. И, честно говоря, если бы я уже пользовался dapper-ом, и не имел какого-никакого конспекта, совсем не уверен, что взялся бы за этот труд, да и ценность инструмента которым пользуешься постоянно, имхо, становится не так очевидна со временем.
 
  • Спасибо
Реакции: djaga

semafor

Client
Регистрация
27.12.2016
Сообщения
288
Благодарностей
385
Баллы
63
@Viking01 спрашивал о поддержке SQL-запроса INSERT IGNORE, который, перед вставкой объекта проверяет существование такого объекта по первичному ключу. Если в таблице найден объект с таким же ключом, то вставка в таблицу не выполняется. Добавляю пример такого запроса для объекта Proxy:

C#:
//Строку подключения не указываю, но она есть
//объект Proxy, наличие которого нужно проверить в табл. и вставить,
//если объекта с таким id нет, или проигнорить вставку, если есть
Proxy proxy = new Proxy()
{
    id = 22,
    ip = "111.000.111.222",
    protocol = "socks5://",
    login = "knock-knock",
    pass = "ohWhoIsThis"
};

//Запрос к БД
string query = "INSERT IGNORE INTO proxy (id, protocol, ip, login, pass) VALUES (@id, @protocol, @ip, @login, @pass)";
int res; //ответ
//создали объект класса MySql.Data.MySqlClient.MySqlConnection и подключились к БД, передав ему в качестве параметра строку подключения
using(var conn = new MySqlConnection(connString))
{
    conn.Open();//открыли сессию
    //Отправили запрос Execute, сохранив в БД объект класса Proxy
    res = conn.Execute(query, proxy);
}
Если в переменной res после выполнения запроса вернется 0 — значит строка с указанным id уже имеется в таблице и вставка не была выполнена.

И запрос INSERT ... ON DUBLICATE KEY UPDATE, который как и предыдущий, перед вставкой строки, проверяет существование строки с указанным id, но если строка существует не игнорит вставку, а обновляет id, так как указано в запросе:

C#:
Proxy proxy = new Proxy()
{
    ip = "111.000.111.222",
    protocol = "socks5://",
    login = "knock-knock",
    pass = "ohWhoIsThis"
    };



//Запрос к БД
string query = "INSERT INTO proxy (id, protocol, ip, login, pass) VALUES (@id, @protocol, @ip, @login, @pass) ON DUPLICATE KEY UPDATE id = @id+1";
int res; //ответ
//создали объект класса MySql.Data.MySqlClient.MySqlConnection и подключились к БД, передав ему в качестве параметра строку подключения
using(var conn = new MySqlConnection(connString))
{
    conn.Open();//открыли сессию
    //получили максимальное значение id
    proxy.id = conn.ExecuteScalar<int>("SELECT MAX(id) FROM proxy;");
    //Отправили запрос Execute, сохранив в БД объект класса Proxy
    res = conn.Execute(query, proxy);
}
return res.ToString();
 
  • Спасибо
Реакции: Viking01

bigloafer

Client
Регистрация
23.07.2020
Сообщения
230
Благодарностей
71
Баллы
28
Дак блокировка таблицы происходит?
 

semafor

Client
Регистрация
27.12.2016
Сообщения
288
Благодарностей
385
Баллы
63
Дак блокировка таблицы происходит?
Не совсем понял вопрос. Если вы о INSERT IGNORE и INSERT...ON DUPLICATE KEY UPDATE, то это 2 разных запроса, и речь там не о блокировке таблиц, а об отказе вставки в случае совпадения первичных ключей в случае ignore, и о вставке с измененным по указанному алгоритму первичного ключа в случае on duplicate key update. Т.е. в первом случае строка не вставляется вовсе, во втором вставляется, но с измененным первичным ключом, чтобы исключить его совпадения с др. ПК в таблице.
 

Viking01

Client
Регистрация
19.08.2017
Сообщения
219
Благодарностей
149
Баллы
43
Не совсем понял вопрос. Если вы о INSERT IGNORE и INSERT...ON DUPLICATE KEY UPDATE, то это 2 разных запроса, и речь там не о блокировке таблиц, а об отказе вставки в случае совпадения первичных ключей в случае ignore, и о вставке с измененным по указанному алгоритму первичного ключа в случае on duplicate key update. Т.е. в первом случае строка не вставляется вовсе, во втором вставляется, но с измененным первичным ключом, чтобы исключить его совпадения с др. ПК в таблице.
да-да, все верно, другими словами это звучит так -
INSERT IGNORE ="если есть уже такая запись, то не вставлять".
INSERT...ON DUPLICATE KEY UPDATE ="если такая запись уже есть, то новую еще раз не вставлять, вместо этого у текущей просто обновить определенные поля".
 
  • Спасибо
Реакции: semafor

semafor

Client
Регистрация
27.12.2016
Сообщения
288
Благодарностей
385
Баллы
63
я про это
C#:
command.CommandText = "LOCK TABLES proxy WRITE";
C#:
command.CommandText = "UNLOCK TABLES;";
Здесь я явно блокирую и разблокирую таблицу (если верно помню, это из примера со штатным MySqlConnector). Исходя из того что я ничего не нашел о блокировках таблиц в Dapper, в статье я высказываю предположение, что решение о необходимости явной блокировки таблиц должен принимать разработчик, то есть мы с вами. Поэтому, при работе с MySQL через Dapper, чтобы исключить искажение или потерю данных, при определенных запросах нужно явно блокировать таблицу перед выполнением запроса. Определять, в каких случаях блокировка нужна, а в каких нет, должны тоже мы, исходя из логики шаблона. Но это только 1 сторона медали.

В остальных сторонах этой многосторонней медали я пока только пытаюсь разобраться — помимо явной блокировки таблиц существуют транзакции и уровни изоляции данных при них. А еще есть запросы, которые предполагают блокирование строк, к которым они обращены, например SELECT ... LOCK IN SHARE MODE, SELECT ... FOR UPDATE. Короче тут еще есть в чем разбираться...
 
  • Спасибо
Реакции: bigloafer

Ptereks

Client
Регистрация
07.03.2019
Сообщения
50
Благодарностей
16
Баллы
8
Не давно начал изучать MySQL, понравилась ваша статья, проголосовал! :ay:

Пытался на основе ваших примеров сделать вставку данных в таблицу, но появляется ошибка:
87176



Insert:
var list = project.Lists["proxy"];//список
string temp;
List<ProxyDB> proxyList = new List<ProxyDB>();
//Строка подключения к БД
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);


for (int i = 0; i<list.Count; i++)
{
    if (list[i].Contains("Ptereks")) temp = "MSK";
    else temp = "EKB";

    ProxyDB myproxy = new ProxyDB()
    {
        id = i,
        proxy = list[i],
            type = temp,
            threads = 0,
        id_thr = ""
    };
    proxyList.Add(myproxy);
}

string query = "INSERT INTO proxy VALUES (@id, [USER=35353]@proxy[/USER], @type, @threads, @id_thr);";

//отправляем в БД список объектов User в те же 2 строки кода что и с dapper.contrib
using (MySqlConnection conn = new MySqlConnection(connString))
{
    conn.Open();

    int resp = conn.Execute(query, proxyList);
    project.SendInfoToLog(resp.ToString(), false);
}
Не подскажите из-за чего происходит ошибка и что я делаю не так?:(
87177
 

semafor

Client
Регистрация
27.12.2016
Сообщения
288
Благодарностей
385
Баллы
63
Не давно начал изучать MySQL, понравилась ваша статья, проголосовал! :ay:

Пытался на основе ваших примеров сделать вставку данных в таблицу, но появляется ошибка:
Посмотреть вложение 87176


Insert:
var list = project.Lists["proxy"];//список
string temp;
List<ProxyDB> proxyList = new List<ProxyDB>();
//Строка подключения к БД
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);


for (int i = 0; i<list.Count; i++)
{
    if (list[i].Contains("Ptereks")) temp = "MSK";
    else temp = "EKB";

    ProxyDB myproxy = new ProxyDB()
    {
        id = i,
        proxy = list[i],
            type = temp,
            threads = 0,
        id_thr = ""
    };
    proxyList.Add(myproxy);
}

string query = "INSERT INTO proxy VALUES (@id, [USER=35353]@proxy[/USER], @type, @threads, @id_thr);";

//отправляем в БД список объектов User в те же 2 строки кода что и с dapper.contrib
using (MySqlConnection conn = new MySqlConnection(connString))
{
    conn.Open();

    int resp = conn.Execute(query, proxyList);
    project.SendInfoToLog(resp.ToString(), false);
}
Не подскажите из-за чего происходит ошибка и что я делаю не так?:(
Посмотреть вложение 87177
У вас в строке подключения ошибка:

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);
Вы указываете четыре переменных проекта, и пять арнументов в строке "{0}...{4}"
 
  • Спасибо
Реакции: Ptereks

MaxMan

Client
Регистрация
15.02.2021
Сообщения
62
Благодарностей
48
Баллы
18
Уважаемый ТС, подскажите, в процессе подбора MySql.Data.dll вы не сталкивались с такой ошибкой:

C#:
Выполнение действия CSharp OwnCode: Get rows from the table using dapper . [Строка: 24; Cтолбец: 0] Не удалось загрузить файл или сборку "MySql.Data, Version=8.0.24.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" либо одну из их зависимостей. Найденное определение манифеста сборки не соответствует ссылке на сборку. (Исключение из HRESULT: 0x80131040)
По Вашему совету взял из папки "..\mysql-connector-net-8.0.24-noinstall\v4.5.2".Также пытался использовать другие версии, но ошибка повторяется. Все ссылки GAC и using прописаны.

Пол года назад я не победил эту проблему и все решил через кубик работы с базой, но сейчас нужна именно работа из C#.
 

semafor

Client
Регистрация
27.12.2016
Сообщения
288
Благодарностей
385
Баллы
63
Уважаемый ТС, подскажите, в процессе подбора MySql.Data.dll вы не сталкивались с такой ошибкой:

C#:
Выполнение действия CSharp OwnCode: Get rows from the table using dapper . [Строка: 24; Cтолбец: 0] Не удалось загрузить файл или сборку "MySql.Data, Version=8.0.24.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" либо одну из их зависимостей. Найденное определение манифеста сборки не соответствует ссылке на сборку. (Исключение из HRESULT: 0x80131040)
По Вашему совету взял из папки "..\mysql-connector-net-8.0.24-noinstall\v4.5.2".Также пытался использовать другие версии, но ошибка повторяется. Все ссылки GAC и using прописаны.

Пол года назад я не победил эту проблему и все решил через кубик работы с базой, но сейчас нужна именно работа из C#.
А сам MySQL Connector у вас устанавлен? Я точно не помню, но вроде у меня тоже сыпались ошибки, пока я явно не поставил его (при этом стоял OpenServer и Workbench).
 

MaxMan

Client
Регистрация
15.02.2021
Сообщения
62
Благодарностей
48
Баллы
18
Нет, не был установлен. Поставил последнюю версию, но не заработало. А вот перенес MySql.Data.dll и MySql.Data.xml в папку ExternalAssemblies ZP и тогда заработало. Спасибо, буду дальше продвигаться :ay:
 

braind

Client
Регистрация
10.10.2012
Сообщения
119
Благодарностей
10
Баллы
18
Получаю такую же ошибку, победить пока не получилось.
Файлы положил в папку ExternalAssemblies
установил MySQL Connector 8.0.29
Какие еще варианты?
 

MaxMan

Client
Регистрация
15.02.2021
Сообщения
62
Благодарностей
48
Баллы
18
Получаю такую же ошибку, победить пока не получилось.
Файлы положил в папку ExternalAssemblies
установил MySQL Connector 8.0.29
Какие еще варианты?
У вас одна версия Зенки?
У меня была ошибка, когда я добавлял dll в ExternalAssemblies другой версии (была про и обычная).
Проверьте GAC и using - все ли корректно.
 

braind

Client
Регистрация
10.10.2012
Сообщения
119
Благодарностей
10
Баллы
18
У вас одна версия Зенки?
У меня была ошибка, когда я добавлял dll в ExternalAssemblies другой версии (была про и обычная).
Проверьте GAC и using - все ли корректно.
Да, версия одна 7.4.0.0
Я использую ваш шаблон, там уже было все прописано.
В итоге я как-то победил, сам точно не понял как) заменил MySql.Data.dll свежей версии (из коннектора) везде - и в корне зенки и в ExternalAssemblies. На всякий случай удалил и добавил заново ссылки из GAC и заработало.
 

MaxMan

Client
Регистрация
15.02.2021
Сообщения
62
Благодарностей
48
Баллы
18
Отлично, что все заработало :ay:
Но шаблон не мой, а semafor
 

woodoo1

Client
Регистрация
10.06.2014
Сообщения
21
Благодарностей
6
Баллы
3

Компиляция кода Ошибка в действии "CS0246" "The type or namespace name 'User' could not be found (are you missing a using directive or an assembly reference?)". [Строка: 13; Cтолбец: 7]


Кто подскажет почему так? Все директивы прописаны, файлы добавил в ExternalAssemblies.
 

Кто просматривает тему: (Всего: 1, Пользователи: 0, Гости: 1)