В очень странное время мы живем .NET становится кроссплатформенным, JAVA становится слаще. Но пока мы вместе движемся в общее светлое будущее есть много унаследованных решений которые необходимо поддерживать. И пока это удается с помощью лобзика и напильника…

Прежде чем


В текущий момент в драйвере поддерживается только тот функционал, который необходим в наших проектах. Вы всегда можете присоединится к проекту Wintegra.Data

История

(для нетерпеливых можно сразу переходить к пункту Пилим)

Хотя в последнее время реляционные базы данных как-то не в хайпе и всем подавай CAP системы – быстрые, удобные, иногда консистентные и масштабируемые. И всё же интерпрайз живет на старых и проверенных решениях, одним из который является база данных от «Голубого гиганта» — IBM и имя ей DB2.

В меру быстрая, надежная, не хуже и не лучше, чем Oracle и MS SQL. Что-то есть, чего-то нет, но в общем SQL нотацию поддерживает (хотя и без обиды на собачек не обошлось @)

Для работы со своей базой под .NET IBM поставляет ODBC драйвер, в общем-то не плохой, но использующий маленькую тележку нативного кода.

С другой стороны, под JAVA есть полностью работающий драйвер без какого-либо использования сторонних dll, имя ему JCC (JDBC 4.0 Driver (db2jcc4.jar)).

И не понятно, что мешает IBM реализовать «правильный» дайвер под .NET.

Поскольку .NET Core не за горами, а переписывать тонны кода под JAVA с C# выглядит более чем странно, то почему не убить двух зайцев – оставить код, и использовать драйвер JDBC.

Для начала воспользуемся IKVM.NET для конвертирования jar в dll и будем его использовать в .NET. Автор утверждает, что можно спокойно запускать Minecraft. Так что JDBC драйвер не вызвал ни каких проблем.

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

ikvmc.exe -classloader:ikvm.runtime.AppDomainAssemblyClassLoader -target:library db2jcc4.jar db2jcc_license_cu.jar -out:db2jcc4.dll

Теперь всё что нам надо, это подключить созданную dll к своему решению и использовать её.

JAVA код в .NET
using System;
using com.ibm.db2.jcc;
using java.sql;
using Thread = java.lang.Thread;
using Class = java.lang.Class;
using String = System.String;
using Connection = java.sql.Connection;
using Statement = java.sql.Statement;
using DriverManager = java.sql.DriverManager;
using ResultSet = java.sql.ResultSet;

namespace jdbc
{
	class Program
	{
		static void Main(string[] args)
		{
			Class.forName(
				typeof(com.ibm.db2.jcc.DB2Driver).AssemblyQualifiedName, true, 
				Thread.currentThread().getContextClassLoader());

			String url = "jdbc:db2://192.168.72.135:50000/DB1:user=root;password=password;";
			using (Connection conn = DriverManager.getConnection(url))
			{
				String sql = "SELECT * FROM TABLE(VALUES( CAST( :p AS VARCHAR(100)) , 'It is work')) AS T(ID, LOG)";
				using (var stmt = conn.prepareCall(sql))
				{
					stmt.setString("p","1234");
					using (ResultSet rs = stmt.executeQuery())
					{
						while (rs.next())
						{
							//Retrieve by column name
							String id = rs.getString("ID");
							String log = rs.getString("LOG");
							global::System.Console.WriteLine("LOG: " + id  + " : " + log);
						}
					}
				}
			}
		}
	}
}


Однако не думаю, что разработчики .NET оценят синтаксис в виде java.lang.*, а вспомнив про унаследованные проекты, которые явно работают как минимум поверх IDbConnetion и часто через Dapper, наш результат может быть интересен только в качестве факультативного развлечения.

Пилим


Подготовив лобзик и напильник будем делать свой велосипед, уж на сколько нам он нужен.

public sealed class Db2Connection : DbConnection, ICloneable

Вполне достаточно для создания подключения и работы Dapper-ом.

Db2Connection
Немного магии из мира JAVA

static Db2Connection()
{
	Class.forName(
		typeof(com.ibm.db2.jcc.DB2Driver).AssemblyQualifiedName, true, 
		Thread.currentThread().getContextClassLoader());
}

Мы загружаем JDBC драйвер, почти так же как в JAVA как

Class.forName("com.ibm.db2.jcc.DB2Driver");

Соединение с базой данных – один в один из JAVA:

connector = DriverManager.getConnection(_connectionString);

Стоит отметить, что строка подключения такая же как в JDBC (хотя по надобности можно использовать Db2ConnectionStringBuilder для задания своих или совместимых в ODBC параметров).

jdbc:db2://192.168.72.135:50000/DB1:currentSchema=DB01;user=root;password=password;fullyMaterializeLobData=true;DB2NETNamedParam=1;

Подключение к серверу, порт, база данных, схема, логин и пароль. Для настройки можно заглянуть на сайт «Голубого гиганта» URL format for IBM Data Server Driver for JDBC and SQLJ type 4 connectivity.

Вроде всё, что могло быть интересным…

Для нанесения выгоды в базу данных нужны команды, нужно – делаем:

public sealed class Db2Command : DbCommand, ICloneable

Где пришлось повозится так это с Dapper-ом.

Db2Command
Dapper упорно заменяет параметр :XML_BODY типа XmlDocument на невнятное (:XML_BODY1, :XML_BODY2) – пришлось написать не сложную регулярочку.

new Regex(@"\(:(?<n>\w+)\d+,:\1\d+\)");

Хотя да, можно было напилить TypeHandler, для любознательных Dapper — ulong throwing System.ArgumentException. Но во-первых обертка над ODBC уже работала с XmlDocument, а во-вторых использование магических команд вида, в начале приложения:

SqlMapper.AddTypeHandler(DapperULongHandler.Default);

всегда чревата ошибками.

Магический метод номер раз


internal static CallableStatement PrepareExecute(Db2Connection connection, string query, Db2ParameterCollection parameters)

Возвращает подготовленный к выполнению запрос.

Особого внимания заслуживает объяснение использования prepareCall (CallableStatement) в место prepareStatement (PreparedStatement). Использование позиционных параметров проще, но в ряде случаев не удобно, использование именованных требует некоторой дополнительной работы, что снижает быстро действие и увеличивает сложность кода. Предпочтение было отдано именованным параметрам, поскольку с ними проще отлаживать уходящие на сервер запросы и даёт бонус в случае когда выполняется мульти запрос.

Для своей магии использует два других.

Магический метод номер два


internal static string PrepareCommandText(string query, Db2ParameterCollection parameters, ref int parameter)

Заменяет позиционные параметры на именованные.

Небольшой обман Dapper-а обошелся аж в два goto. И хотя можно было обойтись без них, но решение простое и оставлено как удовлетворительное.

ref int parameter — понадобиться для Db2DataReader и реализации мульти запросов.

Последний магический метод


private static void PrepareParameters(CallableStatement stmt, Db2ParameterCollection parameters)

Устанавливает параметры.

Развесистый if и вызов множества методов stmt.setXXX(name,

Для команд нужны параметры:

public sealed class Db2ParameterCollection : DbParameterCollection, IList<Db2Parameter>

public sealed class Db2Parameter : DbParameter, ICloneable

Db2Parameter
Для Dapper-а необходимо немного поколдовать над методом:

public override DbType DbType

так как он используется для приведения типов, в остальном реализуем интерфейс.

И необходимо очень внимательно отнестись к методу:

public object Clone()

Поскольку нам нужно создавать копии параметров при мульти запросах.

Db2ParameterCollection
Заслуживает внимание только из-за наличия небольшой оптимизации метода:

public override int IndexOf(string parameterName)
в остальном ни чего существенного.

И в завершении:

internal sealed class Db2DataReader : DbDataReader

Реализующий выполнение мульти запросов через JDBC.

Db2DataReader
Возможно это не самое лучшее решение, но почему нет.

Во-первых, необходимо разбить запрос на подзапросы. Ни каких неожиданностей простое регулярное выражение

new Regex(@"(?<q>[^;]+);?");

Во-вторых, необходимо подготовить для каждого из подзапросов свой набор параметров. Воспользуемся

new Regex(@"(?<n>:\w+)");
и уже знакомым методом Db2Command.PrepareCommandText для замены позиционных параметров на их именованные (как бонус: Dapper может нормально отрабатывать именованные параметры в мульти запросах).

В-третьих, небольшой хак в public override bool NextResult(), для перехода от одной команды к следующей.
var b = _statement.getMoreResults();
if (b)
{
	_rs = _statement.getResultSet();
}
else
{
	_statement = PrepareStatementAndResultSet();
	if (_statement != null)
	{
		_rs = _statement.executeQuery();
		b = true;
	}
}


Итоги


Конечно пока проект не завершен, но в будущем есть желание заменить реализацию оболочки над db2jcc4.jar на «нормальную» реализацию драйвера под .NET без лишних приседаний.

Для чего может понадобиться?

Ну, во-первых, можно не ставить ODBC драйвер на машину пользователя, хотя да в объеме вы не выиграете.
Во-вторых, можно потихоньку перенести свой backend на linux с mono. Что очень удобно при небольшой шепотке Docker-а

И конечно не много для фана, да прибудет с вами многосложный запрос:

SELECT
SELECT 
    r.ID
    , r.INCOME
    , r.GU_CODE as GuCode
	, r.NO as No
    , r.DIVISION
	, d.NAME as DivisionName
	, u.LNAME || ' ' || u.FNAME || nvl(' ' || u.MNAME, '') as CreaterName
	, r.SIGNED
	, r.SIGNED_SYSUSER_ID as SignedID
	, su.LNAME || ' ' || su.FNAME || nvl(' ' || su.MNAME, '') as SignedName
    --, (select count(*) from PACK_ENTRY p where p.REGISTRY_ID = r.ID) as PackCount -- Use C# code
    --, (select count(*) 
    --    from PACK_ENTRY p 
    --    join FILE_ENTRY f on f.PACK_ID=p.ID
    --    where p.REGISTRY_ID = r.ID) as FileCount -- Use C# code
    -- TODO 2016-04-13 emiya: Добавить доп.поля для включения информции в таблицу REGISTRY_ENTRY
    , (select p.CLOSE_DATE from PACK_ENTRY p join REGISTRY_TO_PACK p2p on p2p.REGISTRY_ID = r.ID and p2p.PACK_ID = p.ID order by p.INCOME fetch first 1 rows only) as CloseDate
    , (select p.CATEGORY_ID from PACK_ENTRY p join REGISTRY_TO_PACK p2p on p2p.REGISTRY_ID = r.ID and p2p.PACK_ID = p.ID order by p.INCOME fetch first 1 rows only) as CategoryCode
    , (select l.SHORTNAME from PACK_ENTRY p 
        join CATEGORY_LIST l on l.ID = p.CATEGORY_ID
        join REGISTRY_TO_PACK p2p on p2p.REGISTRY_ID = r.ID and p2p.PACK_ID = p.ID
        order by p.INCOME fetch first 1 rows only) as CategoryName

   , p2p.REGISTRY_ID
   , p.ID
   , p.INCOME
   , p.CLOSE_DATE as CloseDate
   , p.CATEGORY_ID as CategoryCode
   , l.SHORTNAME as CategoryName
   , p.GU_CODE as GuCode
   , p.NO
   , p.DIVISION
   , p.SYSUSER_ID as SysUserID
   , (select count(*) from FILE_ENTRY where PACK_ID = p.ID) as Count

   , r.SIGNED
   , p.PLACE
   , p2p.STATUS
   , p2p.NOTE
from REGISTRY_ENTRY r
join DIVISION d on d.CODE=r.DIVISION
join SYSUSER u on u.ID=r.SYSUSER_ID
    LEFT JOIN SYSUSER su ON su.ID=r.SIGNED_SYSUSER_ID

	join REGISTRY_TO_PACK p2p on p2p.REGISTRY_ID = r.ID
		join PACK_ENTRY p on p.ID = p2p.PACK_ID
			join CATEGORY_LIST l on l.ID = p.CATEGORY_ID
WHERE r.ID=? AND @DIVISION
order by r.ID,p.GU_CODE,p.NO


Ссылка на пакет в NuGet.
Поделиться с друзьями
-->

Комментарии (3)


  1. PastorGL
    24.06.2016 00:08
    +5

    Кривая экзотическая гадость!

    Объясню. 4 года назад я имел редчайшее «удовольствие» плотно поработать с DB2 из-под WebSphere (и через JPA, и напрямки), и самолично нашёл пару-другую крайне неприятных багов в этом ихнем db2jcc4.jar.

    Написан это тдрайвер кем-то, по всей видимости, гениальным, но перманентно пьяным, задней левой ногой, и не иначе как в тёмную октябрьскую ночь. Базовые вещи работают как положено, но фичи чуть-чуть попродвинутей лучше даже не пытаться использовать. Нам вот, например, нужно было блобы в базу писать. В тот же оракл в блоб через JDBC можно без проблем гигабайты лить, а тут производительность деградировала квадратично с каждым пакетом, а в итоге коннект к базе вообще умирал. И сама база слегка портилась.

    Самое противное, что драйвер ещё и хорошенько обфусцирован в самых интересных местах. Пришлось хорошенько поломать голову над тем что выдал декомпилятор, прежде чем удалось понять, где там фигня написана. Зарепортили в IBM (это очень трудно и долго, но у нас была такая возможность), те пообещали поправить через пару релизов, в итоге не знаю чем закончилось, потому что ждать полгода нам было некогда, и блобы решили складывать не в базу, а на диск.

    Ещё интереснее, что у IBM'а, по ходу, один отдел не может договориться с другим. Потому что при подгрузке этого самого драйвера WebSphere патчит его на лету, и заменяет несколько классов (очевидно, бажных) собственной реализацией. Покусочно, прямо вот байт-код патчит. Я чуть в осадок не выпал, когда это обнаружил. Но факт есть факт.

    Не знаю, стало ли лучше за прошедшие 4 года, но мой совет на тот момент был — никогда эту гадость не используйте.


    1. Emiya
      24.06.2016 08:03

      ODBC тоже не лучший представитель, то же DBNull вообще не работает и сваливается в исключение, но жить как-то надо.

      Идеальным решением было бы переписать JDBC и ODBC под .NET, без всех их недостатков.


      1. PastorGL
        27.06.2016 04:25

        Теоретически можно. Протокол по которому работает DB2, называется DRDA и он стандартизирован IBMом. Вопрос в том, стоит ли овчинка выделки, коли уж сам IBM не может реализовать его без дурацких ошибок в своём же продукте.

        Впрочем, у оракла есть бридж, следовательно, и собственная реализация. Но то оракл, большая корпорация.