FreeTDS ile SqlServer Bağlantısı
Linux tabanlı çözüm kümelerinde her ne kadar pek kullanım alanı bulmasa da, ticari dünyada zaman zaman Microsoft SQL Server veritabanı sunucusuna bağlanmanız ve üzerinde işlem yapmanız gerekebilmektedir.
Java gibi yüksek seviyeli dillerde ODBC sürücüleri üzerinden çeşitli çözümler olmakla beraber bu bölümde biz C üzerinden en alt seviyede native protokolü kullanarak SqlServer ile haberleşme konusu üzerinde duracağız.
FreeTDS
FreeTDS, TDS (Tabular Data Stream) protokolünün LGPL lisanslı özgür bir gerçekleştirimidir.
Tabular Data Stream (TDS), bir veritabanı sunucusu ile ona bağlı istemciler arasındaki iletişim ve veri transferini modelleyen, TCP/IP tabanlı bir protokoldür.
Protokol Sybase Inc. tarafından geliştirilmiş ve ilk olarak 1984 yılında Sybase SQL Server ürününde kullanılmıştır.
1990 yılında Sybase ve Microsoft firmalarının aralarında yapmış oldukları teknoloji işbirliği anlaşmasını takiben, Microsoft firması da Sybase SQL Server kodunu temel alarak kendi veritabanı sunucusu olan SQL Server ürününü geliştirdi. Bu nedenle Microsoft SQL Server ürününde de TDS protokolü kullanılmaktadır.
Linux platformlarında TDS protokolünün FreeTDS gerçekleştirimi oldukça kararlı durumda olup, C dilinin yanı sıra Php, Ruby, C++ vb. dillerde de alt katmanda FreeTDS kullanan farklı kütüphane alternatifleri mevcuttur.
TDS protokolünün 5.0 versiyonu Sybase tarafından dokümante edilmiş olmakla birlikte, diğer versiyonlarına dair bilgiler genel kullanıma açılmamıştı. 2008 yılında Microsoft, daha önce hayata geçirdiği Open Specification Promise doğrultusunda TDS protokol detaylarını genel kullanıma açtı ve bu tarihten sonra kütüphaneler daha güvenilir hale geldi.
Not: TDS 5.0 versiyonu ile Sybase sunuculara bağlanılabiliyor olmasına karşın bu versiyon Microsoft tarafından desteklenmemektedir. Microsoft SQL Server bağlantıları için protokolün 7.X versiyonları kullanılmalıdır.
Konsol İstemcisi - Sqsh
Linux sistemlerde kullanılmak üzere, Sybase tarafından geliştirilen isql konsol arayüzündeki temel fonksiyonaliye ve ek olarak kullanım kolaylığı açısından bazı yeni fonksiyonlara sahip sqsh uygulaması geliştirilmiştir. Uygulamayı paket yöneticinizle aşağıdaki gibi sisteminize kurabilirsiniz:
$ sudo apt-get install sqsh
Sqsh ile bir sunucuya bağlanırken temel olarak aşağıdaki parametreler kullanılır:
Parametre | Açıklama |
---|---|
-S | Sunucu adresi |
-U | Kullanıcı Adı |
-P | Parola (parametre olarak girilmez ise konsolda tekrar sorulacaktır) |
-D | Veritabanı Adı |
Örnek olarak 172.16.2.139 ip adresindeki example_db veritabanına testuser kullanıcı adı ve tstpwd123 parolasıyla bağlanalım ve bolgeler tablosundaki kayıtları görelim:
$ sqsh -S 172.16.2.139 -U testuser -P tstpwd123 -D example_db
sqsh-2.1.7 Copyright (C) 1995-2001 Scott C. Gray
Portions Copyright (C) 2004-2010 Michael Peppler
This is free software with ABSOLUTELY NO WARRANTY
For more information type '\warranty'
1> select * from bolgeler
2> go
id isim
----------- ---------------------------------------
1 Akdeniz B�lgesi
2 Dogu Anadolu B�lgesi
3 Ege B�lgesi
4 G�neydogu Anadolu B�lgesi
5 I� Anadolu B�lgesi
6 Marmara B�lgesi
7 Karadeniz B�lgesi
Yukarıdaki sonuç kümesine baktığımızda bazı karakterlerin düzgün görüntülenmediğini, bazılarının ise değiştirildiğini görmekteyiz (ğ -> g vb.)
Sorunun çözümü için sunucuya bağlantı kurarken kullanılacak karakter seti kümesi olarak UTF-8'i belirtmemiz gereklidir. Her ne kadar sqsh uygulamasının yardım sayfasında -J UTF-8 gibi bir parametre geçirmek suretiye bu işlemin yapılabildiği yazsa da kullandığımız versiyonda (2.1.7) bu şekilde çözüm üretemedik. Karakter problemini, sunucu bazlı genel ayarlamaların yapılmasına imkan veren freetds.conf
dosyası üzerinden yapacağımız tanımlamalarla çözeceğiz.
freetds.conf
FreeTDS kütüphanesi ile çalışırken öntanımlı olarak /etc/freetds/freetds.conf
dosyası okunmaktadır.
Bu dosyada genel olarak kütüphanenin davranışını değiştirebilecek tanımlamalar bulunmaktadır. Ayrıca belirli bir SQL sunucu için özel ayarların da buradan yapılmasına imkan verilmektedir.
Dosyanın genel içeriği ve örnek sunucu bazlı tanımlamalar aşağıdaki gibidir:
[global]
# TDS protocol version
; tds version = 4.2
# Whether to write a TDSDUMP file for diagnostic purposes
# (setting this to /tmp is insecure on a multi-user system)
; dump file = /tmp/freetds.log
; debug flags = 0xffff
# Command and connection timeouts
; timeout = 10
; connect timeout = 10
# If you get out-of-memory errors, it may mean that your client
# is trying to allocate a huge buffer for a TEXT field.
# Try setting 'text size' to a more reasonable limit
text size = 64512
# A typical Sybase server
[egServer50]
host = symachine.domain.com
port = 5000
tds version = 5.0
# A typical Microsoft server
[egServer70]
host = ntmachine.domain.com
port = 1433
tds version = 7.0
[mssql]
host = 172.16.2.139
port = 1433
tds version = 7.0
client charset = UTF-8
Yukarıda anlaşılabileceği üzere, tüm sunucuları etkileyecek ayarlar [global]
bölümü altında yer almakta, aynı zamanda [mssql]
örneğindeki gibi belirli bir sunucua isim verilerek (DNS ismi olması gerekmiyor), sunucu bazlı ek ayarlamalar yapma şansı da bulunmaktadır.
Örneğimizde mssql adında bir sunucu ismi tanımladık ve client charset değerini UTF-8 olacak şekilde değiştirdik.
Bu tanım sonrasında hem sqsh uygulamasından hem de freetds kullanan diğer uygulamalarda, sunucu isim/ip parametresinde mssql ismini kullanabilir ve konfigürasyon dosyasında bu bölümde belirtilmiş ayarların aktif olmasını sağlayabiliriz. Bir önceki select örneğimizi tekrar edelim:
$ sqsh -S mssql -U testuser -P tstpwd123 -D example_db
1> select * from bolgeler
2> go
id isim
----------- ---------------------------------------
1 Akdeniz Bölgesi
2 Doğu Anadolu Bölgesi
3 Ege Bölgesi
4 Güneydoğu Anadolu Bölgesi
5 İç Anadolu Bölgesi
6 Marmara Bölgesi
7 Karadeniz Bölgesi
.sqshrc
Sqsh ile çalışırken kullanım ortamınızı daha konforlu hale getirmek için ek ayarlamaları ev dizininiz altındaki .sqshrc
dosyası üzerinden tanımlayabilirsiniz (henüz hiç ayar yapılmadı ise dosyanın oluşturulması gerekecektir)
Örneğin yukarıdaki çıktı formatı yerine öntanımlı MySQL konsol arayüzündekine benzer bir fomrmat kullanılmasını istiyorsanız, go komutunu -m pretty parametresi ile çalıştırmalısınız. Bu komutu her çalıştırdığımızda parametresini girmek zorunda kalmamak için bir alias tanımlayabiliriz.
Aşağıdaki satırı ~/.sqshrc
dosyanıza girin:
\alias go='\go -m pretty'
Şimdi tekrar bölge listesini sorgulayalım:
$ sqsh -S mssql -U testuser -P tstpwd123 -D example_db
1> select * from bolgeler
2> go
+=============+==================================================+
| id | isim |
+=============+==================================================+
| 1 | Akdeniz Bölgesi |
+-------------+--------------------------------------------------+
| 2 | Doğu Anadolu Bölgesi |
+-------------+--------------------------------------------------+
| 3 | Ege Bölgesi |
+-------------+--------------------------------------------------+
| 4 | Güneydoğu Anadolu Bölgesi |
+-------------+--------------------------------------------------+
| 5 | İç Anadolu Bölgesi |
+-------------+--------------------------------------------------+
| 6 | Marmara Bölgesi |
+-------------+--------------------------------------------------+
| 7 | Karadeniz Bölgesi |
+-------------+--------------------------------------------------+
Diğer bazı kullanışlı örnekler için http://www.sypron.nl/sqsh.html adresine bakabilirsiniz.
Kütüphane Kullanımı
FreeTDS kütüphanesini C uygulamalarında kullanabilmek için aşağıdaki komutla geliştirme paketini sisteminize yükleyebilirsiniz:
$ sudo apt-get install freetds-dev
Aşağıdaki örnek uygulamayı mssql_connect.c adıyla kaydedip şu şekilde derleyebilirsiniz:
$ gcc -o mssql_connect mssql_connect.c -lsybdb
Örnek kodumuzu listeleyip önemli yerlerini detaylandırmaya çalışalım:
/* mssql_connect.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sybfront.h>
#include <sybdb.h>
#include "../common/debug.h"
struct mssql_column {
char *name;
char *buffer;
int type;
int size;
int status;
};
void display_usage (const char *name)
{
printf("Usage: %s SERVER USER PASS DATABASE QUERY\n", name);
}
int database_mssql_errhandler (DBPROCESS * dbproc, int severity, int dberr,
int oserr, char *dberrstr, char *oserrstr)
{
(void) dbproc;
(void) oserr;
(void) oserrstr;
if (dberr) {
errorf("Sqlserver Msg %d, Level %d", dberr, severity);
errorf("%s", dberrstr);
} else {
debugf("%s", dberrstr);
}
return INT_CANCEL;
}
int main (int argc, char *argv[])
{
LOGINREC *login;
DBPROCESS *dbproc;
struct mssql_column *columns = NULL;
struct mssql_column *pcol = NULL;
int row_code;
int ncols;
int nrows;
int ret;
int c;
if (argc != 6) {
display_usage(argv[0]);
exit(1);
}
const char *server = argv[1];
const char *username = argv[2];
const char *password = argv[3];
const char *database = argv[4];
const char *query = argv[5];
if (dbinit() == FAIL) {
errorf("Couldn't init MSSQL library");
exit(1);
}
/* Maximum 5 seconds for sql login */
dbsetlogintime(5);
/* Set error handler callback function */
dberrhandle(database_mssql_errhandler);
login = dblogin();
dbsetluser(login, username);
dbsetlpwd(login, password);
if ( (dbproc = dbopen(login, server)) == NULL) {
errorf("Couldn't open sqlserver db connection");
exit(1);
}
if (dbuse(dbproc, database) == FAIL) {
errorf("Couldn't change to db: %s", database);
exit(1);
}
if (dbfcmd(dbproc, query) == FAIL) {
errorf("Couldn't create sql statement");
exit(1);
}
if (dbsqlexec(dbproc) == FAIL) {
errorf("Couldn't execute sql query");
exit(1);
}
ncols = dbnumcols(dbproc);
infof("%d columns found", ncols);
while ( (ret = dbresults(dbproc)) != NO_MORE_RESULTS) {
if (ret == FAIL) break;
if ( (columns = calloc(ncols, sizeof(struct mssql_column))) == NULL) {
errorf("Couldn't allocate columns");
break;
}
for (pcol = columns; pcol - columns < ncols; pcol++) {
c = pcol - columns + 1;
pcol->name = dbcolname(dbproc, c);
pcol->type = dbcoltype(dbproc, c);
pcol->size = dbcollen(dbproc, c);
if (pcol->type != SYBCHAR) {
pcol->size = dbwillconvert(pcol->type, SYBCHAR);
}
debugf("col: %d, type: %d, size: %d, name: %s", c, pcol->type, pcol->size, pcol->name);
if ( (pcol->buffer = malloc(pcol->size + 1)) == NULL) {
errorf("Couldn't allocate space for row buffer");
break;
}
if (dbbind(dbproc, c, NTBSTRINGBIND, pcol->size + 1, (BYTE*)pcol->buffer) == FAIL) {
errorf("Couldn't bind to %s", pcol->name);
break;
}
if (dbnullbind(dbproc, c, &pcol->status) == FAIL) {
errorf("Couldn't make null bind to %s", pcol->name);
break;
}
}
while ((row_code = dbnextrow(dbproc)) != NO_MORE_ROWS) {
switch (row_code) {
case REG_ROW:
for (pcol=columns; pcol - columns < ncols; pcol++) {
char *buffer = pcol->status == -1 ? "NULL" : pcol->buffer;
printf("%s: %s\t", pcol->name, buffer);
}
printf("\n");
break;
case BUF_FULL:
errorf("buffer full");
break;
case FAIL:
errorf("failed");
exit(1);
break;
default:
printf("Data for computeid %d ignored\n", row_code);
}
}
}
/* Free metadata and data */
for (pcol = columns; pcol - columns < ncols; pcol++) {
free(pcol->buffer);
}
free(columns);
/* Get row count if available */
if ( (nrows = dbcount(dbproc)) > -1) {
debugf("Affected rows: %d", nrows);
}
dbclose(dbproc);
dbfreebuf(dbproc);
dbloginfree(login);
return 0;
}
Kütüphanenin İlklendirilmesi: dbinit
FreeTDS kütüphanesinin kullanıldığı uygulamalarda, kütüphane içerisinden herhangi bir fonksiyon çağrılmadan önce, dbinit()
fonksiyonunun çağrılmış olması şarttır.
dbinit()
dahili bazı veri yapılarının doldurulmasını ve yerel spesifik tarih vb. format bilgilerini okumak için freetds içerisinden çıkan -varsa- /etc/freetds/locales.conf
dosyasını okur.
locales.conf
dosyasının bu şekilde okunması deprecated bir özellik olmuştur. Güncel kütüphane versiyonları sistemin yerel (locale) ayarlarından bu bilgileri temin etmektedir. Ancak gene delocales.conf
dosyası bulunursa işlenmektedir.
Bu işlemin uygulamanın main fonksiyonu içerisinde yapılmasında fayda vardır. Ancak herhangi bir sebeple dbinit işleminin bir fonksiyon içerisinden koşullu olarak sonradan yapılması gerekiyorsa, mutlaka statik bir değişkenle kütüphanenin ilklendirme işleminin daha önce yapılıp yapılmadığını tutmanız zorunludur. İlklendirme işleminin tekrar edilmesi, takibi zor hatalara yol açabilmektedir.
Hata İşleme: dberrhandle
Kütüphanenin hata ve uyarı durumlarında çağıracağı calback fonksiyonunu, dberrhandle()
fonksiyonu ile belirtilmelidir.
Bu fonksiyonun prototipi aşağıdaki gibidir:
typedef int (*EHANDLEFUNC) (DBPROCESS * dbproc, int severity,
int dberr, int oserr, char *dberrstr, char *oserrstr);
Fonksiyon çağrıldığında dberr
parametresi 0'dan farklı ise, kritik bir veritabanı hatası olduğu anlaşılır.
Bağlantı Kurma ve Veritabanı Seçimi
Bağlantı kurmak için öncelikle kullanıcı adı ve parola bilgileri LOGINREC
veriyapısı içerisine doldurulmalıdır.
Bunun için öncelikle LOGINREC
tipinde bir değişken, dblogin()
fonksiyonu ile ilklendirilir, ardındandbsetluser ve dbsetlpwd fonksiyonları ile ilgili parametreleri ayarlanır.
Sonraki adımda hazırlanan LOGINREC
veri yapısı ve sunucu bilgisini (burada IP adresi, DNS üzerinden çözülebilen bir hostname veya freetds.conf
içerisinde tanımlanmış bir sunucu adı kullanılabilir) parametre olarak alıp, geriye sürecin ilerleyen aşamalarında sürekli kullanılacak olan DBPROCESS
handle döndürecek olan dbopen()
fonksiyonu çağrılır.
Herhangi bir sebeple hata alınırsa, ilgili hata mesajının detayı hata işlemeleri için önceden belirlenmiş olan callback fonksiyonundan alınabilir.
Bağlantı zaman aşımı süresini kontrol altına almak isterseniz, dbopen()
fonksiyonu çağırmadan önce dbsetlogintime(int seconds)
prototipindeki fonksiyonu kullanarak saniye cinsinden bir limit de tanımlayabilirsiniz.
Bağlantı gerçekleştikten sonra dbuse()
fonksiyonu ile üzerinde çalışılacak olan veritabanı seçimi işlemi yapılmaktadır.
Sorgu Çalıştırma ve Yanıt İşleme
Veritabanı üzerinde çalıştırılacak olan sorgu, öncelikle dbfcmd()
fonksiyonu ile hazırlanır. Ardından dbsqlexec()
fonksiyonu ile çalıştırılır.
Sorgu bu şekilde işletildikten sonra geriye dönen değerlerin saklanacağı uygun veri yapıları oluşturulmalıdır. Bunun için uygulama kaynak kodumuzun ilk bölümünde, struct mssql_column
şeklinde bir yapı tanımladık. Bu yapıyı ihtiyaçlarınız doğrultusunda genişletebilirsiniz.
Tanımladığımız yapıyı, işletmiş olduğumuz sorgunun yanıt kümesindeki her bir sütun ile ilgili veri tipi, uzunluk ve sütun ismi bilgilerini işlemek için kullanacağız.
Örneğimizi geri dönen sütun sayısını önceden bilemeyeceğimiz, her türlü sorgu için çalışacak şekilde hazırladık. Dolayısıyla öncelikle göndermiş olduğumuz sorgu yanıtının kaç sütundan oluştuğunu öğrenmemiz gerekiyor. Bu işlem için dbnumcols()
fonksiyonunu kullanıyoruz.
Sütun sayısını öğrendikten sonra ilgili bilgileri hazırlamış olduğumuz struct mssql_column
veri yapısında saklamak üzere bellekte yer ayırıyoruz.
Ardından sorgu yanıtındaki satırları işlemeye geçmeden hemen önce, sütunlarla ilgili sütun ismi, tipi ve veri uzunluğu bilgilerini sırasıyla dbcolname()
, dbcoltype()
ve dbcollen()
fonksiyonlarıyla elde ediyoruz.
Sütun ile ilgili bilgileri bu şekilde öğrendikten sonra, hazırlamış olduğumuz veri yapısında yanıtları saklayacağımız yerleri hazırlıyoruz. Bu noktada kodumuzu kısa tutmak adına, metin dışındaki tiplerin, dbwillconvert()
fonksiyonuyla metin tabanlı bir formata dönüştürüldüğünde gereken uzunluğu hesaplatıp, metne dönüştüğü zamanki uzunluğu için yetecek kadar bellekte alan açıyoruz. Örnek olarak 4 byte'lık INT
tipindeki bir sütun için dbwillconvert()
sonrası sütun boyutunun 11 olarak geleceğini göreceğiz zira 4 byte'lık bir işaretli INT değerini metne dönüştürüp saklayabilmek için 11 byte uzunluğunda bir alan gereklidir.
Gerçek ortamda gönderdiğiniz sorgularla ilgili bilgi sahibi olacağınızdan, tüm sütunları metin tabanlı dönüşüme zorlamak yerine, struct mssql_column
veri yapısı içerisindeki genel amaçlı buffer
değişkenini bir union
yapısı ile değiştirip, sütun tipine göre union
içerisinde INT, FLOAT vb. veri tipleri kullanabilir ve metin dönüşümü yapmadan doğrudan bu alanların içerisine yazılmasını sağlayabilirsiniz.
Bellekteki alanlar hazır edildikten sonra her bir sütunu dbbind()
fonksiyonu ile yanıt setine nasıl bağladığımızı belirtmemiz gerekiyor. Örneğimizde bind tipi olarak hep NTBSTRINGBIND
değerini kullandık. Yukarıdaki ek notumuz doğrultusunda eğer metin dönüşümü uygulamayacaksanız bunun yerine INTBIND
, REALBIND
, BIGINTBIND
vb. diğer veri tipleri için uygun binding değerlerini de kullanabilirsiniz.
Her bir sütun için gerekli bind işleminin yanı sıra NULL değerler için de dbnullbind()
fonksiyonuyla bir adet binding işleminin daha yapılması gereklidir.
Not: Sütun ve binding tipleri için sabitler, kütüphane içerisinden çıkan
sybdb.h
dosyası içerisinde yer almaktadır.
Şimdi artık sıra satırları işlemeye geldi. Bunun için dbnextrows()
fonksiyonu NO_MORE_ROWS
değeri döndürmediği müddetçe iterasyonla tüm bilgileri alabiliriz.
Örnek uyguladığımızda her bir satırda aldığımız değerleri, sütun ismi ile birlikte ekrana bastırdık.
Yanıt satırlarının işlenmesi tamamlandıktan sonra sistem kaynaklarını serbest bırakıyoruz.
Bağlantının Sonlandırılması
Veritabanı ile ilgili işlemlerimiz tamamlandıysa açık olan bağlantımızı dbclose()
fonksiyonuyla kapatmamız gerekir.
Son olarak kullandığımız DBPROCESS
ve LOGINREC
değişkenlerini de dbfreebuf()
ve dbloginfree()
fonksiyonlarıyla da geride artık kalmayacak şekilde temizliyoruz.
Örnek Kullanım
Hazırlamış olduğumuz uygulama ile bir miktar veri içeren bolgeler
ve iller
adında 2 tablo üzerinde INNER JOIN sorgusu çalıştıralım:
$ ./mssql_connect mssql testuser tstpwd123 example_db \ "SELECT iller.*, bolgeler.isim AS bolge_adi FROM iller \ INNER JOIN bolgeler ON iller.bolge_id=bolgeler.id ORDER BY isim" info: 5 columns found (main mssql_connect.c:91) debug: col: 1, type: 56, size: 11, name: id (main mssql_connect.c:110) debug: col: 2, type: 47, size: 8, name: plaka (main mssql_connect.c:110) debug: col: 3, type: 56, size: 11, name: bolge_id (main mssql_connect.c:110) debug: col: 4, type: 47, size: 400, name: isim (main mssql_connect.c:110) debug: col: 5, type: 47, size: 200, name: bolge_adi (main mssql_connect.c:110) id: 13 plaka: 06 bolge_id: 5 isim: Ankara bolge_adi: İç Anadolu Bölgesi id: 11 plaka: 07 bolge_id: 1 isim: Antalya bolge_adi: Akdeniz Bölgesi id: 1 plaka: 08 bolge_id: 7 isim: Artvin bolge_adi: Karadeniz Bölgesi id: 9 plaka: 16 bolge_id: 6 isim: Bursa bolge_adi: Marmara Bölgesi id: 4 plaka: 28 bolge_id: 7 isim: Giresun bolge_adi: Karadeniz Bölgesi id: 7 plaka: 34 bolge_id: 6 isim: İstanbul bolge_adi: Marmara Bölgesi id: 2 plaka: 53 bolge_id: 7 isim: Rize bolge_adi: Karadeniz Bölgesi id: 6 plaka: 55 bolge_id: 7 isim: Samsun bolge_adi: Karadeniz Bölgesi id: 15 plaka: 58 bolge_id: 5 isim: Sivas bolge_adi: İç Anadolu Bölgesi id: 3 plaka: 61 bolge_id: 7 isim: Trabzon bolge_adi: Karadeniz Bölgesi id: 8 plaka: 77 bolge_id: 6 isim: Yalova bolge_adi: Marmara Bölgesi debug: Affected rows: 15 (main mssql_connect.c:159)
Not:
debug.h
dosyasını Kaynak Dosyalar bölümünden edinebilirsiniz.