Sqlite ile Veritabanı Programlama¶
Giriş¶
Bu bölümde, Python’daki ileri düzey konulardan biri olan veritabanı programlamayı (database programming) inceleyeceğiz. Peki nedir bu “veritabanı” denen şey?
Esasında veritabanı, hiçbirimizin yabancısı olduğu bir kavram değil. Biz bu kelimeyi, teknik anlamının dışında, günlük hayatta da sıkça kullanıyoruz. Veritabanı, herkesin bildiği ve kullandığı anlamıyla, içinde veri barındıran bir “şey”dir. Günlük kullanımda, hakikaten, içinde veri barındıran her şeye veritabanı dendiğini duyarsınız.
Veritabanı kelimesinin günlük kullanımdaki anlamı dışında bir de teknik anlamı vardır. Bizi esas ilgilendiren de zaten terimin teknik anlamıdır. Mesela Vikipedi’de veritabanı şöyle tanımlanıyor:
Bilgisayar terminolojisinde, sistematik erişim imkânı olan, yönetilebilir, güncellenebilir, taşınabilir, birbirleri arasında tanımlı ilişkiler bulunabilen bilgiler kümesidir. Bir başka tanımı da, bir bilgisayarda sistematik şekilde saklanmış, programlarca işlenebilecek veri yığınıdır.
Yukarıdaki tanım, veritabanının ne demek olduğunu gayet iyi ifade ediyor. Ama esasında bizim veritabanı tanımı üzerinde fazlaca durmamıza gerek yok. Biz her zaman olduğu gibi işin teknik boyutuyla değil, taktik boyutuyla ilgilenmeyi tercih edeceğiz. O halde yavaş yavaş işe koyulmaya başlayalım.
Python’la veritabanı programlama işlemleri için pek çok alternatifimiz var. Python’la hangi veritabanı sistemlerini kullanabileceğinizi görmek için http://wiki.python.org/moin/DatabaseInterfaces adresindeki listeyi inceleyebilirsiniz. Biz bunlar içinde, sadeliği, basitliği ve kullanım kolaylığı nedeniyle Sqlite adlı veritabanı yönetim sistemini kullanacağız.
Neden Sqlite?¶
Dediğimiz gibi, Python’da veritabanı işlemleri için kullanabileceğiniz pek çok alternatif bulunur. Ama biz bütün bu alternatifler içinde Sqlite’ı tercih edeceğiz. Peki neden Sqlite?
Sqlite’ın öteki sistemlere göre pek çok avantajı bulunur. Gelin isterseniz Sqlite’ın bazı avantajlarına şöyle bir göz gezdirelim:
- Her şeyden önce Sqlite Python’un 2.5 sürümlerinden bu yana bu dilin bir parçasıdır. Dolayısıyla eğer kullandığınız Python sürümü 2.5 veya üstü ise Sqlite’ı Python’daki herhangi bir modül gibi içe aktarabilir ve kullanmaya başlayabilirsiniz.
- Sqlite herhangi bir yazılım veya sunucu kurulumu gerektirmez. Bu sayede, bu modülü kullanabilmek için sunucu konfigürasyonu yapmaya da gerek yoktur. Bazı veritabanlarını kullanabilmek için arka planda bir veritabanı sunucusu çalıştırıyor olmanız gerekir. Sqlite’ta ise böyle bir şey yapmazsınız.
- Sqlite, öteki pek çok veritabanı alternatifine göre basittir. Bu yüzden Sqlite’ı çok kısa bir sürede kavrayıp kullanmaya başlayabilirsiniz.
- Sqlite özgür bir yazılımdır. Bu yazılımın baştan aşağı bütün kodları kamuya açıktır. Dolayısıyla Sqlite kodlarının her zerresini istediğiniz gibi kullanabilir, değişikliğe uğratabilir, satabilir ve ticari veya ticari olmayan uygulamalarınızda gönül rahatlığıyla kullanabilirsiniz.
- Sqlite’ın “sade” ve “basit” olması sizi yanıltmasın. Bu özelliklerine bakarak, Sqlite’ın yeteneksiz bir veritabanı sistemi olduğunu düşünmeyin. Bugün Sqlite’ı aktif olarak kullanan pek çok büyük ve tanınmış şirket bulunur. Mesela, Adobe, Apple, Mozilla/Firefox, Google, Symbian ve Sun bu şirketlerden bazılarıdır. Hatta GNOME masaüstü ortamının sevilen müzik ve video çalarlarından Banshee’de de veritabanı olarak Sqlite kullanıldığını söyleyelim.
Yukarıdaki sebeplerden ötürü, veritabanı konusunu Sqlite üzerinden anlatacağız. O halde hemen yola koyulalım.
Sqlite’ın Yapısı¶
Bu bölümün en başında verdiğimiz veritabanı tanımından da anlaşılacağı gibi, veritabanları, verileri sonradan kullanılmak üzere içinde tutan bir sistemdir. Bütün ilişkisel veritabanlarında olduğu gibi, Sqlite da bu verileri bir tablo yapısı içinde tutar. Yani aslında bir Sqlite veritabanı içindeki veriler şöyle bir yapıya sahiptir:
Sütun 1 Sütun 2 Sütun 3 Sütun 4 Sütun 5 Değer 1/1 Değer 2/1 Değer 3/1 Değer 4/1 Değer 5/1 Değer 1/2 Değer 2/2 Değer 3/2 Değer 4/2 Değer 5/2 Değer 1/3 Değer 2/3 Değer 3/3 Değer 4/3 Değer 5/3 Değer 1/4 Değer 2/4 Değer 3/4 Değer 4/4 Değer 5/4
Sqlite içinde oluşturulan yukarıdakine benzer her tablonun bir de ismi vardır. Daha doğrusu, Sqlite ile bir tablo oluştururken, bu tabloya bir de ad vermemiz gerekir. Mesela yukarıdaki tabloya “Değerler” adını verdiğimizi varsayabilirsiniz...
Sqlite ile çalışırken veriler üzerinde yapacağımız işlemleri, yukarıdaki tablonun adını ve bu tablodaki sütunları kullanarak gerçekleştireceğiz. Bu yüzden Sqlite’ın yapısını anlamak büyük önem taşır. Gördüğünüz gibi, bu veritabanı sisteminin yapısını anlamak da öyle zor bir iş değildir.
Veritabanıyla Bağlantı Kurmak¶
Bu bölümde sqlite modülünü kullanarak bir veritabanına nasıl bağlanacağımızı, elimizde herhangi bir veritabanı yoksa veritabanını nasıl oluşturacağımızı inceleyeceğiz.
Dikkat ederseniz burada bir sqlite modülünden söz ettik. Dolayısıyla, tahmin edebileceğiniz gibi, bu modülü kullanabilmek için öncelikle modülü içe aktarmamız gerekiyor. Bu bölümün başında da söylediğimiz gibi, Sqlite, Python’un 2.5 sürümünden bu yana Python’un bir parçasıdır:
>>> import sqlite3
Python’da Sqlite veritabanı sistemine ait modül “sqlite3” adını taşır. Bu yüzden, bu modülü içe aktarmak için import sqlite3 ifadesini kullanmamız gerekiyor. Eğer bu isim size çok uzun geliyorsa veya modül adında sayıların ve harflerin birlikte bulunması nedeniyle hem sayı hem de harf girmeyi bir angarya olarak görüyorsanız elbette sqlite3 modülünü farklı bir adla da içe aktarabileceğinizi biliyorsunuz. Mesela:
>>> import sqlite3 as sql
Veya:
>>> import sqlite3 as lite
Böylece sqlite3 modülünü “sql” veya “lite” adıyla içe aktarmış olduk. Ancak ben konuyu anlatırken, okur açısından kafa karışıklığına sebep olmamak için, modülü import sqlite3 şeklinde içe aktarmışız gibi davranacağım.
Gelelim bu modül yardımıyla nasıl veritabanı oluşturulacağına... Bunun için sqlite3 modülünün connect() adlı fonksiyonundan yararlanacağız. Bu fonksiyonu şu şekilde kullanıyoruz:
>>> sqlite3.connect("veritabanı_adı")
connect() metoduna verdiğimiz “varitabanı_adı” adlı argüman kullanacağımız veritabanının adıdır. Eğer belirtilen isimde bir veritabanı sistemde bulunmuyorsa o adla yeni bir veritabanı oluşturulacaktır. Mesela:
>>> vt = sqlite3.connect("deneme.db")
Eğer bu komutu verdiğiniz dizin içinde deneme.db adlı bir veritabanı yoksa, bu ada sahip bir veritabanı oluşturulur. Eğer zaten bu adla bir veritabanı dosyanız varsa, sqlite3 bu veritabanına bağlanacaktır.
Elbette isterseniz connect() metoduna argüman olarak tam dosya yolu da verebilirsiniz:
>>> import sqlite3 as sq
>>> v = sq.connect("/home/istihza/test.db") #GNU/Linux
>>> v = sq.connect("c:/documents and settings/fozgul/desktop/test.db") #Windows
Bu komut yardımıyla sabit disk üzerinde bir dosya oluşturmuş veya varolan bir dosyaya bağlanmış oluyoruz. Ancak isterseniz sqlite3 ile geçici bir veritabanı da oluşturabilirsiniz:
>>> vt = sqlite3.connect(":memory:")
Oluşturduğunuz bu geçici veritabanı sabit disk üzerinde değil RAM (bellek) üzerinde çalışır. Veritabanını kapattığınız anda da bu geçici veritabanı silinir. Eğer arzu ederseniz, RAM üzerinde değil, disk üzerinde de geçici veritabanları oluşturabilirsiniz. Bunun için de şöyle bir komut kullanıyoruz:
>>> vt = sqlite3.connect("")
Gördüğünüz gibi, disk üzerinde geçici bir veritabanı oluşturmak için boş bir karakter dizisi kullandık. Tıpkı :memory: kullanımında olduğu gibi, boş karakter dizisiyle oluşturulan geçici veritabanları da veritabanı bağlantısının kesilmesiyle birlikte ortadan kalkacaktır.
Geçici veritabanı oluşturmak, özellikle çeşitli testler veya denemeler yaptığınız durumlarda işinize yarar. Sonradan nasıl olsa sileceğiniz, sırf test amaçlı tuttuğunuz bir veritabanını disk üzerinde oluşturmak yerine RAM üzerinde oluşturmayı tercih edebilirsiniz. Ayrıca, geçici veritabanları sayesinde, yazdığınız bir kodu test ederken bir hatayla karşılaşırsanız sorunun veritabanı içinde varolan verilerden değil, yazdığınız koddan kaynaklandığından da emin olabilirsiniz. Çünkü, dediğimiz gibi, programın her yeniden çalışışında veritabanı baştan oluşturulacaktır. Her şeyden öte, bellek üzerinde yapılan işlemler sabit disk üzerinde yapılan işlemlere göre daha hızlıdır...
Dikkatinizi çekmek istediğim bir nokta da şudur: Gördüğünüz gibi Sqlite, veritabanını o anda içinde bulunduğunuz dizin içinde oluşturuyor. Mesela MySQL kullanıyor olsaydınız, oluşturulan veritabanlarının önceden tanımlanmış bir dizin içine atıldığını görecektiniz. Örneğin GNU/Linux sistemlerinde, MySQL veritabanları /var/lib/mysql gibi bir dizinin içinde tutulur...
Böylece Sqlite ile nasıl veritabanı bağlantısı kuracağımızı ve nasıl yeni bir veritabanı oluşturacağımızı öğrenmiş olduk. Veritabanı oluşturduktan sonra, veritabanı üzerinde işlem yapabilmek için ilk adım olarak bir imleç oluşturmamız gerekir. İmleç oluşturmak için cursor() metodundan yararlanacağız:
>>> im = vt.cursor()
İmleci oluşturduktan sonra artık önümüz iyice açılıyor. Böylece, yukarıda oluşturduğumuz imlec nesnesinin execute() metodunu kullanarak SQL komutlarını çalıştırabileceğiz.
Veri Girişi¶
Önceki bölümün sonunda söylediğimiz gibi, bir imleç nesnesi oluşturduktan sonra bunun execute() metodunu kullanarak SQL komutlarını işletebiliyoruz.
Dilerseniz şimdi basit bir örnek yaparak neyin ne olduğunu anlamaya çalışalım:
>>> im.execute("CREATE TABLE adres_defteri (isim, soyisim)")
Hatırlarsanız, Sqlite veritabanı sisteminin tablo benzeri bir yapıya sahip olduğunu ve bu sistemdeki her tablonun da bir isminin bulunduğunu söylemiştik. İşte burada yaptığımız şey, “adres_defteri” adlı bir tablo oluşturup, bu tabloya “isim” ve “soyisim” adlı iki sütun eklemekten ibarettir. Yani aslında şöyle bir şey oluşturmuş oluyoruz:
isim soyisim
Ayrıca oluşturduğumuz bu tablonun adının da “adres_defteri” olduğunu unutmuyoruz...
Bu işlemleri nasıl yaptığımıza dikkat edin. Burada CREATE TABLE adres_defteri (isim, soyisim) tek bir karakter dizisidir. Bu karakter dizisindeki CREATE TABLE kısmı bir SQL komutu olup, bu komut bir tablo oluşturulmasını sağlar.
Burada CREATE TABLE ifadesini büyük harflerle yazdık. Ancak bu ifadeyi siz isterseniz küçük harflerle de yazabilirsiniz. Benim burada büyük harf kullanmaktaki amacım SQL komutlarının, “adres_defteri”, “isim” ve “soyisim” gibi değişkenlerden görsel olarak ayırt edilebilmesini sağlamak. Yani CREATE TABLE ifadesinin mesela “adres_defteri” değişkeninden kolayca ayırt edilebilmesini istediğim için burada CREATE TABLE ifadesini büyük harflerle yazdım.
Karakter dizisinin devamında (isim, soyisim) ifadesini görüyoruz. Tahmin edebileceğiniz gibi, bunlar tablodaki sütun başlıklarının adını gösteriyor. Buna göre, oluşturduğumuz tabloda “isim” ve “soyisim” adlı iki farklı sütun başlığı olacak.
Yukarıda execute() metodunu kullanarak, veritabanı içinde adres_defteri adlı bir tablo oluşturduk. Ardından da bu tablo içine isim ve soyisim adlı iki sütun başlığı yerleştirdik. Bu işlemin ne kadar kolay olduğunu görüyorsunuz. Şimdi yine buna benzer bir komut yardımıyla, yukarıda oluşturduğumuz sütun başlıklarının altını dolduracağız:
>>> im.execute("INSERT INTO adres_defteri VALUES ('Fırat', 'Özgül')")
Burada CREATE TABLE komutundan sonra INSERT INTO adlı yeni bir SQL komutu daha öğreniyoruz. CREATE TABLE ifadesi Türkçe’de “TABLO OLUŞTUR” anlamına geliyor. INSERT INTO ise ”... İÇİNE YERLEŞTİR” anlamına gelir. Yukarıdaki karakter dizisi içinde görünen VALUES ise “DEĞERLER” demektir. Yani aslında yukarıdaki karakter dizisi şu anlama gelir: “adres_defteri İÇİNE ‘Fırat’ ve ‘Özgül’ DEĞERLERİNİ YERLEŞTİR. Yani şöyle bir tablo oluştur”:
isim soyisim Fırat Özgül
Buraya kadar gayet güzel gidiyoruz. İsterseniz şimdi derin bir nefes alıp, şu ana kadar yaptığımız şeyleri bir gözden geçirelim:
- Öncelikle sqlite3 modülünü içe aktardık. Bu modülün nimetlerinden yararlanabilmek için bunu yapmamız gerekiyordu. “sqlite3” kelimesini her defasında yazmak bize angarya gibi gelebileceği için bu modülü farklı bir adla içe aktarmayı tercih edebiliriz. Mesela import sqlite3 as sql veya import sqlite3 as lite gibi...
- sqlite3 modülünü içe aktardıktan sonra bir veritabanına bağlanmamız veya elimizde bir veritabanı yoksa yeni bir veritabanı oluşturmamız gerekiyor. Bunun için connect() adlı bir fonksiyondan yararlanıyoruz. Bu fonksiyonu, sqlite3.connect("veritabanı_adı") şeklinde kullanıyoruz. Eğer içinde bulunduğumuz dizinde, “veritabanı_adı” adlı bir veritabanı varsa Sqlite bu veritabanına bağlanır. Eğer bu adda bir veritabanı yoksa, çalışma dizini altında bu ada sahip yeni bir veritabanı oluşturulur. Özellikle deneme amaçlı işlemler yapmamız gerektiğinde, sabit disk üzerinde bir veritabanı oluşturmak yerine RAM üstünde geçici bir veritabanı ile çalışmayı da tercih edebiliriz. Bunun için yukarıdaki komutu şöyle yazıyoruz: sqlite3.connect(":memory:"). Bu komutla RAM üzerinde oluşturduğumuz veritabanı, bağlantı kesildiği anda ortadan kalkacaktır.
- Veritabanımızı oluşturduktan veya varolan bir veritabanına bağlandıktan sonra yapmamız gereken şey bir imleç oluşturmak olacaktır. Daha sonra bu imlece ait metotlardan yararlanarak önemli işler yapabileceğiz... Sqlite’ta bir imleç oluşturabilmek için db.cursor() gibi bir komut kullanıyoruz. Tabii ben burada oluşturduğunuz veritabanına “db” adını verdiğinizi varsayıyorum...
- İmlecimizi de oluşturduktan sonra önümüz iyice açılmış oldu. Şimdi dir(im) gibi bir komut kullanarak imlecin metotlarının ne olduğunu inceleyebilirsiniz. Tabii ben burada imlece “im” adını verdiğinizi varsaydım... Gördüğünüz gibi, listede execute() adlı bir metot da var. Artık imlecin bu execute() metodunu kullanarak SQL komutlarını işletebiliriz.
- Yukarıda iki adet SQL komutu öğrendik. Bunlardan ilki CREATE TABLE. Bu komut veritabanı içinde bir tablo oluşturmamızı sağlıyor. İkincisi ise INSERT INTO ... VALUES .... Bu komut da, oluşturduğumuz tabloya içerik eklememizi sağlıyor. Bunları şu şekilde kullandığımızı hatırlıyorsunuz:
>>> im.execute("CREATE TABLE adres_defteri (isim, soyisim)")
>>> im.execute("INSERT INTO adres_defteri VALUES ('Fırat', 'Özgül')")
Burada bir şey dikkatinizi çekmiş olmalı. SQL komutlarını yazmaya başlarken çift tırnakla başladık. Dolayısıyla karakter dizisini yazarken iç taraftaki Fırat ve Özgül değerlerini yazmak için tek tırnak kullanmamız gerekti. Karakter dizileri içindeki manevra alanınızı genişletmek için, SQL komutlarını üç tırnak içinde yazmayı da tercih edebilirsiniz. Böylece karakter dizisi içindeki tek ve çift tırnakları daha rahat bir şekilde kullanabilirsiniz. Yani:
>>> im.execute("""CREATE TABLE adres_defteri (isim, soyisim)""")
>>> im.execute("""INSERT INTO adres_defteri VALUES ("Fırat", "Özgül")""")
Ayrıca üç tırnak kullanmanız sayesinde, uzun satırları gerektiğinde bölerek çok daha okunaklı kodlar da yazabileceğinizi biliyorsunuz.
Veri İşleme - commit() Metodu¶
Bir önceki bölümde bir Sqlite veritabanına nasıl veri gireceğimizi öğrendik. Ama aslında iş sadece veri girmeyle bitmiyor. Verileri veritabanına “işleyebilmek” için bir adım daha atmamız gerekiyor. Mesela şu örneğe bir bakalım:
# -*- coding: utf-8 -*-
import sqlite3
vt = sqlite3.connect(":memory:")
im = vt.cursor()
im.execute("""CREATE TABLE personel (isim, soyisim, şehir, eposta)""")
im.execute("""INSERT INTO personel VALUES
("Orçun", "Kunek", "Adana", "okunek@gmail.com")""")
Burada öncelikle RAM üzerinde geçici bir veritabanı oluşturduk. Zaten gerçek bir uygulama yazmadığımız, henüz test aşamasında olduğumuz için en iyi yaklaşım geçici bir veritabanı oluşturmak olacaktır.
Ardından, vt.cursor() komutuyla imlecimizi de oluşturduktan sonra, SQL komutlarımızı çalıştırıyoruz. Önce isim, soyisim, şehir ve eposta adlı sütunlardan oluşan, “personel” adlı bir tablo oluşturduk. Daha sonra “personel” tablosunun içine “Orçun”, “Kunek”, “Adana” ve “okunek@gmail.com” değerlerini yerleştirdik.
Ancak her ne kadar veritabanına veri işlemiş gibi görünsek de aslında henüz işlenmiş bir şey yoktur. Biz henüz sadece verileri girdik. Ama verileri veritabanına işlemedik. Bu girdiğimiz verileri veritabanına işleyebilmek için commit() adlı bir metottan yararlanacağız:
>>> vt.commit()
Gördüğünüz gibi, commit() imlecin değil, bağlantı nesnesinin (yani burada vt değişkeninin) bir metodudur. Şimdi bu satırı da betiğimize ekleyelim:
# -*- coding: utf-8 -*-
import sqlite3
vt = sqlite3.connect(":memory:")
im = vt.cursor()
im.execute("""CREATE TABLE personel (isim, soyisim, şehir, eposta)""")
im.execute("""INSERT INTO personel VALUES
("Orçun", "Kunek", "Adana", "okunek@gmail.com")""")
vt.commit()
Bu son satırı da ekledikten sonra Sqlite veritabanı içinde şöyle bir tablo oluşturmuş olduk:
isim soyisim şehir eposta Orçun Kunek Adana okunek@gmail.com
Veritabanından Veri Almak¶
Yukarıda, bir veritabanına nasıl veri gireceğimizi ve işleyeceğimizi gördük. İşin asıl önemli kısmı, bu verileri daha sonra veritabanından geri alabilmektir. Şimdi bu işlemi nasıl yapacağımıza bakacağız.
Veritabanından herhangi bir veri alabilmek için SELECT...FROM... adlı bir SQL komutundan yararlanmamız gerekiyor.
Dilerseniz önce bir tablo oluşturalım:
# -*- coding: utf-8 -*-
import sqlite3
vt = sqlite3.connect(":memory:")
im = vt.cursor()
im.execute("""CREATE TABLE faturalar
(fatura, miktar, ilk_odeme_tarihi, son_odeme_tarihi)""")
Şimdi bu tabloya bazı veriler ekleyelim:
im.execute("""INSERT INTO faturalar VALUES
("Elektrik", 45, "23 Ocak 2010", "30 Ocak 2010")""")
Verileri veritabanına işleyelim:
vt.commit()
Yukarıdaki kodlar bize şöyle bir tablo verdi:
fatura miktar ilk_odeme_tarihi son_odeme_tarihi Elektrik 45 23 Ocak 2010 30 Ocak 2010
Buraya kadar olan kısmı zaten biliyoruz. Bilmediğimiz ise bu veritabanından nasıl veri alacağımız. Onu da şöyle yapıyoruz:
im.execute("""SELECT * FROM faturalar""")
Burada özel bir SQL komutu olan SELECT...FROM...‘dan faydalandık. Burada joker karakterlerden biri olan “*” işaretini kullandığımıza dikkat edin. SELECT * FROM faturalar ifadesi şu anlama gelir: “faturalar adlı tablodaki bütün öğeleri seç!“
Burada “SELECT” kelimesi “SEÇMEK” demektir. “FROM” ise ”...DEN/...DAN” anlamı verir. Yani “SELECT FROM faturalar” dediğimizde “faturalardan seç” demiş oluyoruz... Burada kullandığımız “*” işareti de “her şey” anlamına geldiği için, “SELECT * FROM faturalar” ifadesi “faturalardan her şeyi seç” gibi bir anlama gelmiş oluyor.
Betiğimizi yazmaya devam edelim:
veriler = im.fetchall()
Burada da ilk defa gördüğümüz bir metot var: fetchall(). Gördüğünüz gibi, fetchall() imlecin bir metodudur. Yukarıda gördüğümüz SELECT...FROM... komutu bir tablodaki verileri seçiyordu. fetchall() metodu ise seçilen bütün verileri alma işlevi görür. Yukarıda biz fetchall() metoduyla aldığımız bütün verileri veriler adlı bir değişkene atadık.
Artık bu verileri rahatlıkla yazdırabiliriz:
print veriler
Dilerseniz betiğimizi topluca görelim:
# -*- coding: utf-8 -*-
import sqlite3
vt = sqlite3.connect(":memory:")
im = vt.cursor()
im.execute("""CREATE TABLE faturalar
(fatura, miktar, ilk_odeme_tarihi, son_odeme_tarihi)""")
im.execute("""INSERT INTO faturalar VALUES
("Elektrik", 45, "23 Ocak 2010", "30 Ocak 2010")""")
vt.commit()
im.execute("""SELECT * FROM faturalar""")
veriler = im.fetchall()
print veriler
Bu betiği çalıştırdığımızda şöyle bir çıktı alırız:
[(u'Elektrik', 45, u'23 Ocak 2010', u'30 Ocak 2010')]
Gördüğünüz gibi, veriler bir liste içinde demet halinde yer alıyor. Ama tabii siz bu verileri istediğiz gibi biçimlendirecek kadar Python bilgisine sahipsiniz.
Bu arada, tablo oluştururken sütun adlarında boşluk (ve Türkçe karakter) kullanmak iyi bir fikir değildir. Mesela ilk ödeme tarihi yerine ilk_odeme_tarihi ifadesini tercih edin. Eğer kelimeler arasında mutlaka boşluk bırakmak isterseniz bütün kelimeleri tırnak içine alın. Mesela: "ilk odeme tarihi" veya "ilk ödeme tarihi".
Veritabanı Güvenliği - SQL Injection¶
Python’da veritabanları ve Sqlite konusunda daha fazla ilerlemeden önce çok önemli bir konudan bahsetmemiz gerekiyor. Tahmin edebileceğiniz gibi, veritabanı denen şey oldukça hassas bir konudur. Bilgiyi bir araya toplayan bu sistem, içerdeki bilgilerin değerine ve önemine de bağlı olarak üçüncü şahısların ağzını sulandırabilir. Ancak depoladığınız verilerin ne kadar değerli ve önemli olduğundan bağımsız olarak veritabanı güvenliğini sağlamak, siz programcıların asli görevidir.
Peki veritabanı yönetim sistemleri acaba hangi tehditlerle karşı karşıya?
SQL komutlarını işleten bütün veritabanları için günümüzdeki en büyük tehditlerden birisi hiç kuşkusuz kötü niyetli kişilerin SQL’e sızma (SQL injection) girişimleridir.
Şimdi şöyle bir şey düşünün: Diyelim ki siz bir alışveriş karşılığı birine 100.000 TL’lik bir çek verdiniz. Ancak çeki verdiğiniz kişi bu çek üzerindeki miktarı tahrif ederek artırdı ve banka da tahrif edilerek artırılan bu miktarı çeki getiren kişiye (hamiline) ödedi. Böyle bir durumda epey başınız ağrıyacaktır.
İşte böyle tatsız bir durumla karşılaşmamak için, çek veren kişi çekin üzerindeki miktarı hem rakamla hem de yazıyla belirtmeye özen gösterir. Ayrıca rakam ve yazılara ekleme yapılmasını da engellemek için rakam ve yazıların sağına soluna “#” gibi işaretler de koyar. Böylece çeki alan kişinin, kendisine izin verilenden daha fazla bir miktarı elde etmesini engellemeye çalışır.
Yukarıdakine benzer bir şey veritabanı uygulamalarında da karşımıza çıkabilir. Şimdi şu örneğe bakalım:
# -*- coding: utf-8 -*-
import sqlite3
#Deneme amaçlı bir çalışma yaptığımız için veritabanımızı
#bellek üzerinde oluşturuyoruz.
db = sqlite3.connect(":memory:")
#Veritabanı üzerinde istediğimiz işlemleri yapabilmek
#için bir imleç oluşturmamız gerekiyor.
im = db.cursor()
#imlecin execute() metodunu kullanarak, veritabanı içinde
#"kullanicilar" adlı bir tablo oluşturuyoruz. Bu tabloda
#kullanıcı_adi ve parola olmak üzere iki farklı sütun var.
im.execute("""CREATE TABLE kullanicilar (kullanici_adi, parola)""")
#Yukarıda oluşturduğumuz tabloya yerleştireceğimiz verileri
#hazırlıyoruz. Verilerin liste içinde birer demet olarak
#nasıl gösterildiğine özellikle dikkat ediyoruz.
veriler = [
("ahmet123", "12345678"),
("mehmet321", "87654321"),
("selin456", "123123123")
]
#veriler adlı liste içindeki bütün verileri kullanicilar adlı
#tabloya yerleştiriyoruz. Burada tek öğeli bir demet
#tanımladığımıza dikkat edin (i,)
for i in veriler:
im.execute("""INSERT INTO kullanicilar VALUES %s""" %(i,))
#Yaptığımız değişikliklerin tabloya işlenebilmesi için
#commit() metodunu kullanıyoruz.
db.commit()
#Kullanıcıdan kullanıcı adı ve parola bilgilerini alıyoruz...
kull = raw_input("Kullanıcı adınız: ")
paro = raw_input("Parolanız: ")
#Burada yine bir SQL komutu işletiyoruz. Bu komut, kullanicilar
#adlı tabloda yer alan kullanici_adi ve parola adlı sütunlardaki
#bilgileri seçiyor.
im.execute("""SELECT * FROM kullanicilar WHERE
kullanici_adi = '%s' AND parola = '%s'"""%(kull, paro))
#Hatırlarsanız daha önce fetchall() adlı bir metottan
#söz etmiştik. İşte bu fetchone() metodu da ona benzer.
#fetchall() bütün verileri alıyordu, fetchone() ise
#verileri tek tek alır.
data = im.fetchone()
#Eğer data adlı değişken False değilse, yani bu
#değişkenin içinde bir değer varsa kullanıcı adı
#ve parola doğru demektir. Kullanıcıyı içeri alıyoruz.
if data:
print u"Programa hoşgeldin %s!" % data[0]
#Aksi halde kullanıcıya olumsuz bir mesaj veriyoruz.
else:
print u"Parola veya kullanıcı adı yanlış!"
Bu örnekte henüz bilmediğimiz bazı kısımlar var. Ama siz şimdilik bunları kafanıza takmayın. Nasıl olsa bu kodlarda görünen her şeyi biraz sonra tek tek öğreneceğiz. Siz şimdilik sadece işin özüne odaklanın.
Yukarıdaki kodları çalıştırdığınızda, eğer kullanıcı adı ve parolayı doğru girerseniz Programa hoşgeldin çıktısını göreceksiniz. Eğer kullanıcı adınız veya parolanız yanlışsa bununla ilgili bir uyarı alacaksınız.
Her şey iyi hoş, ama bu kodlarda çok ciddi bir problem var.
Dediğimiz gibi, bu kodlar çalışırken eğer kullanıcı, veritabanında varolan bir kullanıcı adı ve parola yazarsa sisteme kabul edilecektir. Eğer doğru kullanıcı adı ve parola girilmezse sistem kullanıcıya giriş izni vermeyecektir. Ama acaba gerçekten öyle mi?
Şimdi yukarıdaki programı tekrar çalıştırın. Kullanıcı adı ve parola sorulduğunda da her ikisi için şunu yazın:
x' OR '1' = '1
O da ne! Program sizi içeri aldı... Hem de kullanıcı adı ve parola doğru olmadığı halde... Hatta şu kodu sadece kullanıcı adı kısmına girip parola kısmını boş bırakmanız da sisteme giriş hakkı elde etmenize yetecektir.:
x' OR '1' = '1' --
İşte yukarıda gösterdiğimiz bu işleme “SQL’e sızma” (SQL injection) adı verilir. Kullanıcı, tıpkı en başta verdiğimiz tahrif edilmiş çek örneğinde olduğu gibi, sistemin zaaflarından yararlanarak, elde etmeye hakkı olandan daha fazlasına erişim hakkı elde ediyor.
Burada en basit şekliyle bool işleçlerinden biri olan or‘dan yararlanıyoruz. or‘un nasıl işlediğini gayet iyi biliyorsunuz, ama ben yine de birkaç örnekle or‘un ne olduğunu ve ne yaptığını size hatırlatayım. Şu örneklere bakın:
>>> a = 21
>>> a == 22
False
>>> b = 13
>>> b == 13
True
>>> if a == 22 and b == 13:
... print "Merhaba!"
...
>>> if a == 22 or b == 13:
... print "Merhaba!"
...
Merhaba!
Örneklerden de gördüğünüz gibi, and işlecinin True sonucunu verebilmesi için her iki önermenin de doğru olması gerekir. O yüzden a == 22 and b == 13 gibi bir ifade False değeri veriyor. Ancak or işlecinin True sonucu verebilmesi için iki önermeden sadece birinin doğru olması yeterlidir. Bu yüzden, sadece b == 13 kısmı True olduğu halde a == 22 or b == 13 ifadesi True sonucu veriyor... İşte biz de yukarıdaki SQL’e sızma girişiminde or‘un bu özelliğinden faydalanıyoruz.
Dilerseniz neler olup bittiğini daha iyi anlayabilmek için, sızdırılan kodu doğrudan ilgili satıra uygulayalım:
im.execute("""SELECT * FROM kullanicilar WHERE
kullanici_adi = 'x' OR '1' = '1' AND parola = 'x' OR '1' = '1'""")
Sanırım bu şekilde neler olup bittiği daha net görülüyor. Durumu biraz daha netleştirmek için Python’u yardıma çağırabiliriz:
>>> kullanici_adi = 'ahmet123'
>>> parola = '12345678'
>>> kullanici_adi == 'x'
False
>>> '1' == '1'
True
>>> kullanici_adi == 'x' or '1' == '1'
True
>>> parola == 'x'
False
>>> (kullanici_adi == 'x' or '1' == '1') and (parola == 'x' or '1' == '1')
True
'1' == '1' ifadesi her zaman True değeri verecektir. Dolayısıyla kullanıcı adının ve parolanın doğru olup olmaması hiçbir önem taşımaz. Yani her zaman True değerini vereceği kesin olan ifadeler yardımıyla yukarıdaki gibi bir sızma girişiminde bulunabilirsiniz.
Yukarıda yaptığımız şey, ‘%s’ ile gösterilen yerlere kötü niyetli bir SQL komutu sızdırmaktan ibarettir. Burada zaten başlangıç ve bitiş tırnakları olduğu için sızdırılan kodda başlangıç ve bitiş tırnaklarını yazmıyoruz. O yüzden sızdırılan kod şöyle görünüyor:
x' OR '1' = '1
Gördüğünüz gibi, x’in başındaki ve 1’in sonundaki tırnak işaretleri koymuyoruz.
Peki yukarıda verdiğimiz şu kod nasıl çalışıyor:
x' OR '1' = '1' --
Python’da yazdığımız kodlara yorum eklemek için “#” işaretinden yararlandığımızı biliyorsunuz. İşte SQL kodlarına yorum eklemek için de “–” işaretlerinden yararlanılır. Şimdi dilerseniz yukarıdaki kodu doğrudan ilgili satıra uygulayalım ve ne olduğunu görelim:
im.execute("""SELECT * FROM kullanicilar WHERE
kullanici_adi = 'x' OR '1'='1' --AND parola = '%s'""")
Burada yazdığımız “–” işareti AND parola = '%s' kısmının sistem tarafından yorum olarak algılanmasını sağlıyor. Bu yüzden kodların bu kısmı işletilmiyor. Dolayısıyla da sisteme giriş yapabilmek için sadece kullanıcı adını girmemiz yeterli oluyor!.. Burada ayrıca kodlarımızın çalışması için 1’in sonuna bir adet tırnak yerleştirerek kodu kapattığımıza dikkat edin. Çünkü normal bitiş tırnağı yorum tarafında kaldı...
Dikkat ederseniz SQL’e sızdığımızda “ahmet123” adlı kullanıcının hesabını ele geçirmiş olduk. Peki neden ötekiler değil de “ahmet123”? Bunun sebebi, “ahmet123” hesabının tablonun en başında yer alması. Eğer tablonun başında “admin” diye bir hesap olmuş olsaydı, veritabanına azami düzeyde zarar verme imkanına kavuşacaktınız...
Peki SQL’e sızma girişimlerini nasıl önleyeceğiz? Bu girişime karşı alabileceğiniz başlıca önlem “%s” işaretlerini kullanmaktan kaçınmak olacaktır. Bu işaret yerine ”?” işaretini kullanacaksınız. Yani yukarıdaki programı şöyle yazacağız:
# -*- coding: utf-8 -*-
import sqlite3
db = sqlite3.connect(":memory:")
im = db.cursor()
im.execute("""CREATE TABLE kullanicilar (kullanici_adi, parola)""")
veriler = [
("ahmet123", "12345678"),
("mehmet321", "87654321"),
("selin456", "123123123")
]
for i in veriler:
im.execute("""INSERT INTO kullanicilar VALUES (?, ?)""", i)
db.commit()
kull = raw_input("Kullanıcı adınız: ")
paro = raw_input("Parolanız: ")
im.execute("""SELECT * FROM kullanicilar WHERE
kullanici_adi = ? AND parola = ?""", (kull, paro))
data = im.fetchone()
if data:
print u"Programa hoşgeldin %s!" % data[0]
else:
print u"Parola veya kullanıcı adı yanlış!"
Dediğimiz gibi, SQL’e sızma girişimlerine karşı alabileceğiniz başlıca önlem “%s” işaretleri yerine ”?” işaretini kullanmak olmalıdır. Bunun dışında, SQL komutlarını işletmeden önce bazı süzgeçler uygulamak da güvenlik açısından işinize yarayabilir. Örneğin kullanıcıdan alınacak verileri alfanümerik karakterlerle [http://www.istihza.com/blog/alfanumerik-ne-demek.html/] sınırlayabilirsiniz:
if kull.isalnum() and paro.isalnum():
im.execute("""SELECT * FROM kullanicilar WHERE
kullanici_adi = '%s' AND parola = '%s'"""%(kull, paro))
Böylece kullanıcının bazı “tehlikeli” karakterleri girmesini engelleyebilir, onları sadece harf ve sayı girmeye zorlayabilirsiniz.
Her halükarda unutmamamız gereken şey, güvenliğin çok boyutlu bir kavram olduğudur. Birkaç önlemle pek çok güvenlik açığını engelleyebilirsiniz, ancak bütün güvenlik açıklarını bir çırpıda yamamak pek mümkün değildir. Bir programcı olarak sizin göreviniz, yazdığınız programları güvenlik açıklarına karşı sürekli taramak ve herhangi bir açık ortaya çıktığında da bunu derhal kapatmaya çalışmaktır.