Ocena użytkowników: 2 / 5

Gwiazdka aktywnaGwiazdka aktywnaGwiazdka nieaktywnaGwiazdka nieaktywnaGwiazdka nieaktywna
 


Różnicę między tymi formatami łatwo zrozumieć porównując zapis danych w postaci XML z tabelką w Excelu. Dla przykładu fakturę można przedstawić w postaci struktury:

<faktura numer="123">
<naglowek>
<nabywca>Jan Kowalski</nabywca>
</naglowek>
<wiersze>
<pozycja nr="1">
<nazwa>Ogórki kiszone</nazwa>
<jm>szt</jm>
<ilosc>1</ilosc>
<cena>2</cena>
<wartosc>2</wartosc>
</pozycja>
</wiersze>
</faktura>


W Excelu będziemy to widzieć w postaci prymitywnej tabeli:

numer=123
Jan Kowalski
Ogórki kiszone szt 1 2 2



Rzeczywiście w programie łatwiej operować obiektami, niż danymi tabelarycznymi. Pod warunkiem jednak, że dostaje się je już w postaci obiektu. I to właśnie miały zapewnić obiektowe bazy danych.
Jeśli wnikliwie przeanalizujemy powyższy przykład, to od razu widać, że pomysł był od początku kontrowersyjny. Bo jak byśmy się nie wysilali, to i tak nie przekonamy przysłowiowej pani Zosi z księgowości, że struktura obiektowa jest bardziej właściwa ;-). A więc konwersja obiektowo-relacyjna i tak wystąpi - tyle, że po stronie interfejsu z użytkownikiem.
Informatycy to ludzie zazwyczaj dość inteligentni. Czemu więc próbowano w miejsce tradycyjnych, sprawdzonych rozwiązań wprowadzić coś całkowicie odmiennego?
Pierwszą zaletą pomysłu było to, że był intelektualnie atrakcyjny i łatwo można było przekonać dysponentów kapitału, że warto w niego pakować miliony. A drugą - że można było wykazać ekonomiczną opłacalność takiej inwestycji.
Żeby to zrozumieć, musimy wiedzieć na co firmy wydają pieniądze w ramach informatyzacji.
Obrazuje to doskonale stary rysunek (od czasu, gdy go pierwszy raz widziałem przybyło kilka klatek):

 


Ilość pośredników między klientem a programistą sprawia, że klient dostaje produkt drogi i nie dostosowany do jego potrzeb. Dzięki obiektowym bazom danych pozbywamy się co najmniej jednego pośrednika: programisty baz danych. Na dodatek analityk, który potrafi opisać rzeczywistość w postaci obiektów uzyskuje wgląd i kontrolę nad całym procesem produkcji.
Opisany przez niego obiekt pojawia się w całym procesie tworzenia oprogramowania: od projektu po bazę danych.

Tabelki trzymają się mocno

Pomimo, że programowanie obiektowe zdominowało przemysł softwerowy, w bazach danych królują nadal tradycyjne rozwiązania. Dlaczego? Bo są dobre i sprawdzone w milionach wdrożeń. Firmie Microsoft udało się przekonać ludzi, że potrzebują co raz to nowego systemu na swoje PC-ty, bo to wbrew pozorom nie są to duże koszty. Zmiana bazy danych często powoduje konieczność głębokiej przebudowy systemu i generuje olbrzymie koszty. Jeśli już więc chcemy wdrażać model obiektowy, to zdecydowanie lepiej zacząć od nowych aplikacji.
A ponieważ te aplikacje muszą często czerpać dane z "przestarzałych" baz - pojawia się pole do popisu dla ORM. „Odpuszczono” więc same bazy danych, ale postanowiono je „opakować” obiektami, tak by nie straszyły swą „przestarzałą” strukturą.

I ty zostaniesz programistą

Największą zaletą ORM jest niebywała prostota użytkowania. Aby pokazać siłę tego rozwiązania, posłużę się przykładem prostej, ale kompletnej aplikacji Django. Poniżej przepis krok po kroku jak ją zbudować. (Jeśli ktoś nie zna Django, a chciałby zgłębiać szczegóły podanego opisu – polecam artykuł Twoja pierwsza aplikacja w Django).

1. Zakładamy projekt - książkę telefoniczną:

django-admin.py startproject telefony


2. Ustawiamy parametry bazy danych (SQLite) - w pliku settings.py:

DATABASES = {
    'default': {
	 'ENGINE': 'django.db.backends.sqlite3', 
	 'NAME': 'telefony.db',
	

3. Zdefiniujmy obiekt danych. W tym celu zakładamy moduł models.py z zawartością:

from django.db import models

class Telefony(models.Model):    
  telefon = models.CharField(max_length=20)
  opis = models.CharField(max_length=80)
  
  class Meta:
	verbose_name_plural = u'Telefony'        # nazwa w liczbie mnogiej

4. Dodajmy moduł administratora, który będzie mógł zmieniać zawartość bazy (admin.py):

from django.contrib import admin
from telefony.models import Telefony

class TelefonyAdmin(admin.ModelAdmin):
	list_display = ('telefon','opis')
	search_fields = ('opis',)

admin.site.register(Telefony,TelefonyAdmin)



5. Wykonujemy operację synchronizacji modelu z bazą danych:

python manage.py syncdb

w trakcie tej operacji dodatkowo zostanie założony administrator wraz z hasłem (oczywiście po pobraniu od nas odpowiednich danych).

6. Przygotujemy stronę prezentacji danych. W tym celu w pliku z adresami urls.py odkomentujemy wiersz:

url(r'^$', 'telefony.views.home', name='home'),

 

aby to zadziałało musimy zgodnie z powyższym opisem stworzyć moduł views zawierający funkcję home:

from django.http import HttpResponse
from telefony.models import Telefony

def home(request):
	tabelka='<table>'
	for tel in  Telefony.objects.all():
		tabelka=tabelka+'<tr><td>%s</td><td>%s</td></tr>' % (tel.telefon, tel.opis)
	return HttpResponse('<html><body>'+tabelka+'</table></body></html>',
     mimetype="text/html") 

 

7. Uaktywniamy panel administratora - odkomentując w pliku urls.py trzy linijki:

from django.contrib import admin
admin.autodiscover()
url(r'^admin/', include(admin.site.urls)),


Dodatkowo w pliku konfiguracji settings.py dodajemy dwie linijki w zmiennej INSTALLED_APPS (pierwsza tam jest, tylko w formie komentarza):

'django.contrib.admin',
'telefony',


8. Uruchamiamy gotową palikację do testów:

python manage.py runserver

Wpisując w przeglądarce: 127.0.0.1:8000/admin logujemy się do panelu administratora i uzupełniamy bazę danych. Na koniec otwieramy stronę 127.0.0.1:8000 i cieszymy się z gotowej aplikacji.


W ciągu 10-15 minut udało się stworzyć gotową stronę, która po doszlifowaniu prezentacji danych może być całkiem użyteczna.


Czy można inaczej?

Zdefiniujmy odrębny moduł dostępu do bazy danych o nazwie dblink.py:

import sqlite3 as lite
import sys
from django.conf import settings

class SqliteDB:
	def __init__(self):
		try:
			self.con = lite.connect(settings.DATABASES['default']['NAME'])
			self.cur = self.con.cursor()
		except lite.Error, e:
			print u"Baza danych: %s" % e.args[0]
			sys.exit(1)

db=SqliteDB()

Nasz moduł wyświetlania telefonów przybierze postać:

from django.http import HttpResponse
from telefony.models import Telefony
from dblink import db

def home(request):
	tabelka='<table>'
	for (telefon,opis) in  db.cur.execute('select telefon,opis from telefony_telefony'):
	  tabelka=tabelka+'<tr><td>%s</td><td>%s</td></tr>'
% (telefon, opis)
	return HttpResponse('<html><body>'+tabelka+'</table></body></html>',
mimetype="text/html") 



Co się zmieniło?
Nie korzystamy z ORM, ale wprost z modułu dostępu do bazy danych (tu sqlite3).
Jeśli chcemy korzystać ze standardowego panelu administratora - to model musi niestety pozostać zdefiniowany.

Co zyskujemy dzięki takiemu rozwiązaniu?
Jego zalety są szczególnie widoczne w dużych aplikacjach. Ale już w takim małym przykładzie możemy spostrzec, że:

  1. Program jest bardziej czytelny dla programisty.
    Niby to kwestia gustu. Jednak programista aplikacji operuje na co dzień bazami danych i zapis w formie SQL jest jasny. A tu mamy wszystko w jednym module. Nie musimy szukać modelu, by wiedzieć co i dlaczego dostaliśmy.
  2. Łatwiejsza diagnostyka.
    Pisząc powyższy moduł pomyliłem się w zapytaniu, pisząc 'select telefony,....'. Komunikat 'no such column: telefony' każe sprawdzić zgodność zapytania z bazą danych. W ORM musimy grzebać w modułach definicji.
  3. Łatwiejsze modyfikacje.
    Chcąc zmienić zakres danych, sposób ich uporządkowania etc... możemy po prostu zmienić zapytanie SQL.
    Najpierw można je przetestować w dostarczanych dla danej bazy programie klienta (SQLiteAdmin, PgAdmin etc...), a później wpisać do modułu. Zakres szukania kodu w którym musimy szukać ewentualnych błędów w każdym z tych kroków jest ograniczony.


Zasadnicze wady ORM


1. Powyższy przykład jest prosty, a na dodatek stworzony w Pythonie, w którym ORM jest dość naturalny. Jeśli ktoś próbował zrozumieć działanie na przykład otwartego framework'u PHP w którym zastosowano ORM, musi sobie zadać pytanie, czy nie napisałby w tym czasie aplikacji w 'czystym PHP'.

2. Często musimy tworzyć aplikacje korzystające z gotowych danych i żadne mechanizmy synchronizacji nie wchodzą w rachubę.
Po zapoznaniu się ze schematem bazy danych mamy do wyboru: albo mozolnie przepisywać go do modelu ORM, albo zająć się programowaniem, stosując wprost zapytania SQL.

3. Wiele aplikacji obecnych na rynku ma tak fatalnie zaprojektowane bezy danych, że zapytania bywają bardzo złożone. Napisanie ich w modelu obiektowym mija się po prostu z celem.

4. Testując aplikację, możemy śledzić kod programu i jego efekty w bazie danych (lub na ekranie). Programując w tradycyjny sposób, te dwa źródła informacji mamy tuż obok siebie. W modelu ORM pomiędzy nimi jest masa niezrozumiałego kodu, który 'coś' robi. Jeśli na dodatek jest to obce oprogramowanie, znalezienie źródła błędu jest utrudnione.

5. Przyłączenie do bazy jest zaimplementowane "gdzieś" w bibliotece....
Jeśli wszystko jest zgodne z tym, co programista ORM sobie umyślił, to nie ma problemu. Ale jeśli na przykład w bazie jest inne kodowanie polskich znaków?
To do niedawna była przecież raczej reguła niż wyjątek. Innym problemem może być utrzymywanie połączenia z bazą. W usługach hostingowych regułą jest ograniczony czas gwarantowanego połączenia. Jeśli program mamy uruchamiany przy każdym zapytaniu - nie ma to znaczenia.
Ale jeśli rezyduje na stałe?
Możemy to wszystko obsłużyć, programując moduł dostępu do bazy danych. ORM nam tego raczej nie zapewni.

Połączenie z bazą danych dla opisanego powyżej modułu dnlink.py z zastosowaniem bazy danych MySql, może wyglądać mniej więcej tak:

self.con = MySQLdb.connect (host = , user = , passwd = , db = ,
                            use_unicode=True, charset=settings.DB_CHARSET)
self.charset = self.con.character_set_name()			
self.cur = self.con.cursor()


Co nam to daje? Zobaczmy na przykładzie:

q=u"select telefon,opis from telefony_telefony where opis like '%s'"  %
nazwisko
for (telefon,opis) in db.cur.execute(q.encode(db.charset)): print "%s : %s" % (telefon,nazwisko.decode(db.charset))


Widzimy, że baza danych może pamiętać dane w innym kodowaniu, a w innym je zwracać (settings.DB_CHARSET).
Po połączeniu dostajemy informację o tym kodowaniu (self.con.character_set_name()) i możemy ją wykorzystać do transformacji między unicodem w jakim działa Python, a danymi (decode/encode).
Podobnie rozwiązanie ma problem podtrzymywania połączenia. Wystarczy zaimplementować w obiekcie połączenia bazy danych odpowiednią metodę:

def chk(self):
  if self.con :
    try:
	  self.con.ping()
    except MySQLdb.OperationalError, message:
	  self.con = None
    if self.con is None :	# dswieżenie połączenia



To można wykonywać przed każdym zapytaniem, lub stworzyć swoją funkcję (execute).
Tego typu problemy są obce wielbicielom ORM, ale są one realne i bywają bardzo uciążliwe.

6. Na koniec rzecz, która może sę wydawać szukaniem dziury w całym. Chodzi o projektowanie baz danych. Jest to sztuka ważna i poświęcono jej mnóstwo książek. Od lat jednak przemysł softwerowy dostarcza rozwiązań, które zdają się problem dobrego projektu bazy danych unieważniać. Wskutek tego większość baz nie jest efektywna, a czasem bywają one koszmarne. Gdy taka baza ma kilkaset tabel, a brak jest szczegółowej dokumentacji, wyszukiwanie informacji w niej jest bardzo trudne.
Jedną z kontrowersyjnych (dla mnie) kwestii związanych z ORM jest stosowanie ujednoliconych kluczy: zawsze jest pole autoincrement o nazwie 'id'.
Ale to jest szersza kwestia i wymaga odrębnego potraktowania.

 

Podsumowanie

Pomimo zawartej w artykule krytyki nie uważam, by ORM należało na stałe skreślić ze słownika ważnych pojęć związanych z programowaniem. Jednak umiejętność projektowania baz danych pozostaje jedną z kluczowych dla informatyka. ORM może być atrakcyjny jedynie w stosunkowo prostych projektach.