Pull to refresh

Перепиливаем JDBC DB2 под .NET

Reading time7 min
Views4.3K
В очень странное время мы живем .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.
Tags:
Hubs:
+3
Comments3

Articles

Change theme settings