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

semafor

Client
Регистрация
27.12.2016
Сообщения
289
Реакции
410
Баллы
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: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;

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


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

Вложения

Последнее редактирование модератором:
Вау вкустно!
Я изначально был на этапе написания sql запросов через либу, но просто решил психануть и написать сервис для взаимодействия с бд на .Net Core, где ты был раньше?)
Про дапер вовсе забыл
Материал Круто подан
 
  • Спасибо
Реакции: semafor
Вау вкустно!
Я изначально был на этапе написания sql запросов через либу, но просто решил психануть и написать сервис для взаимодействия с бд на .Net Core, где ты был раньше?)
Про дапер вовсе забыл
Материал Круто подан
Ахаха! У меня ровно наоборот вышло — сел писать свой мапер, но психанул и вспомнил про дапер )))
 
Полезная информация. :ay:
 
  • Спасибо
Реакции: semafor
Ценная информация! У меня мосг перегрелся на половине текста, за один заход не осилить
:bf: Перескажу кратко — работа с MySQL через стандартную либу MySqlData.dll достаточно неудобна и требует написания большого количества кода, что ухудшает читаемость кода, усложняет его поддержку и изменение. А при создании собственных классов и использовании dapper все значительно упрощается — отправлять SQL-запросы удобнее, код чище, логика шаблона нагляднее.
 
  • Спасибо
Реакции: Deiccide и Alexmd
Спасибо за даппер, даже не знал про него.
Хоть одна действительно полезная статья в этом конкурсе :)
По мне, конечно, не настолько красиво, как было бы с Entity Framework, но все равно проще, чем запросы и ридеры ручками писать.
А INSERT IGNORE или INSERT ... ON DUBLICATE KEY UPDATE он поддерживает?

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

Про SELECT ... FOR UPDATE пока не отвечу
 
Добавлено видео от автора.
Размещено в конце первого поста.
 
Тут эта... Несколько раз меня накрывало, что чего-то в статье не хватает, но все не мог догнать чего. И сегодня осенило — ведь ссылки!!!
1. Github Dapper. Есть примеры, например по работе с динамическими объектами, еще интересен раздел вопросов(issues). Ну и код даппера тоже можно поковырять.
2. Github Dapper.Contrib. Тоже примеры, вопросы и код изнутри
3. Dapper Tutorial. Здесь есть примеры по самому Dapper, по Contrib, а так же по еще нескольким полезным либам дополняющим dapper.
4. StackOverflow наше все — тут множество примеров с коментами и подробными пояснениями. Правда не по-нашему, но код есть код
 
  • Спасибо
Реакции: rodgers и djaga
Вот есть MySql и SqlLite. Будет PostgreSql?
 
  • Спасибо
Реакции: semafor
Вот есть MySql и SqlLite. Будет PostgreSql?
На самом деле, методы либ абсолютно одинаковы с любой реляционной СУБД. Различия только в стандартных библиотеках, сопрягающих NET и БД. Я посмотрю, что покажет голосование — есть ли смысл тратить время на подробные мануалы, если они не очень востребованы в комьюнити
 
  • Спасибо
Реакции: bigloafer
На самом деле, методы либ абсолютно одинаковы с любой реляционной СУБД. Различия только в стандартных библиотеках, сопрягающих NET и БД. Я посмотрю, что покажет голосование — есть ли смысл тратить время на подробные мануалы, если они не очень востребованы в комьюнити
на текущий момент, судя по голосованию, техническая часть шаблонов, чуть ли не на первом месте оценивается.
 
  • Спасибо
Реакции: semafor
на текущий момент, судя по голосованию, техническая часть шаблонов, чуть ли не на первом месте оценивается.
Спасибо за инсайд )) Я в начале голосования посмотрел, и пока больше не заглядывал — и со временем как всегда напряг, да и раньше срока чего заглядывать.

Своим появлением на конкурсе эта статья обязана тем, что мне самому понадобилось как-то упростить мапинг данных в БД, а так как я частенько пишу памятки по работе с тем или иным инструментом для себя, решил попутно накалякать и для сообщества. И, честно говоря, если бы я уже пользовался dapper-ом, и не имел какого-никакого конспекта, совсем не уверен, что взялся бы за этот труд, да и ценность инструмента которым пользуешься постоянно, имхо, становится не так очевидна со временем.
 
  • Спасибо
Реакции: djaga
@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
Дак блокировка таблицы происходит?
 
Дак блокировка таблицы происходит?
Не совсем понял вопрос. Если вы о INSERT IGNORE и INSERT...ON DUPLICATE KEY UPDATE, то это 2 разных запроса, и речь там не о блокировке таблиц, а об отказе вставки в случае совпадения первичных ключей в случае ignore, и о вставке с измененным по указанному алгоритму первичного ключа в случае on duplicate key update. Т.е. в первом случае строка не вставляется вовсе, во втором вставляется, но с измененным первичным ключом, чтобы исключить его совпадения с др. ПК в таблице.
 
Не совсем понял вопрос. Если вы о INSERT IGNORE и INSERT...ON DUPLICATE KEY UPDATE, то это 2 разных запроса, и речь там не о блокировке таблиц, а об отказе вставки в случае совпадения первичных ключей в случае ignore, и о вставке с измененным по указанному алгоритму первичного ключа в случае on duplicate key update. Т.е. в первом случае строка не вставляется вовсе, во втором вставляется, но с измененным первичным ключом, чтобы исключить его совпадения с др. ПК в таблице.
да-да, все верно, другими словами это звучит так -
INSERT IGNORE ="если есть уже такая запись, то не вставлять".
INSERT...ON DUPLICATE KEY UPDATE ="если такая запись уже есть, то новую еще раз не вставлять, вместо этого у текущей просто обновить определенные поля".
 
  • Спасибо
Реакции: semafor
я про это
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
Не давно начал изучать 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
 
Не давно начал изучать 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
Уважаемый ТС, подскажите, в процессе подбора 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.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).
 
Нет, не был установлен. Поставил последнюю версию, но не заработало. А вот перенес MySql.Data.dll и MySql.Data.xml в папку ExternalAssemblies ZP и тогда заработало. Спасибо, буду дальше продвигаться :ay:
 
Получаю такую же ошибку, победить пока не получилось.
Файлы положил в папку ExternalAssemblies
установил MySQL Connector 8.0.29
Какие еще варианты?
 
Получаю такую же ошибку, победить пока не получилось.
Файлы положил в папку ExternalAssemblies
установил MySQL Connector 8.0.29
Какие еще варианты?

У вас одна версия Зенки?
У меня была ошибка, когда я добавлял dll в ExternalAssemblies другой версии (была про и обычная).
Проверьте GAC и using - все ли корректно.
 
У вас одна версия Зенки?
У меня была ошибка, когда я добавлял dll в ExternalAssemblies другой версии (была про и обычная).
Проверьте GAC и using - все ли корректно.
Да, версия одна 7.4.0.0
Я использую ваш шаблон, там уже было все прописано.
В итоге я как-то победил, сам точно не понял как) заменил MySql.Data.dll свежей версии (из коннектора) везде - и в корне зенки и в ExternalAssemblies. На всякий случай удалил и добавил заново ссылки из GAC и заработало.
 
Отлично, что все заработало :ay:
Но шаблон не мой, а semafor
 
Компиляция кода Ошибка в действии "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.
 

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