Chapter 6: Warstwa abstracji bazy danych
Warstwa abstrakcji bazy danych
Zależności
Platforma web2py dostarczana jest wraz warstwą abstrakcji bazy danych (ang. Database Abstraction Layer - DAL), API odwzorowującego obiekty Pythona na obiekty bazy danych, takie jak zapytania, tabele i rekordy. DAL dynamicznie generuje w czasie rzeczywistym kod SQL używając dialektu specyficznego dla wykorzystywanej bazy danych, tak że nie trzeba pisać samemu kodu SQL lub uczyć się różnych dialektów SQL (termin SQL ma tu znaczenie ogólne) a aplikacje będą przenośne pomiędzy różnymi typami baz danych. Częściowa lista obsługiwanych baz danych jest pokazana w tabeli poniżej. Proszę sprawdzić na stronie internetowej web2py i na liście dyskusyjnej czy nie zostały dodane nowe adaptery. Baza Google NoSQL jest omówiona jako szczególny przypadek w rozdziale 13.
W podrozdziale "Pułapki", znajdującym się na końcu tego rozdziału, można znaleźć trochę więcej informacji o określonych bazach danych.
Dystrybucja binarna na Windows działa od razu po zainstalowaniu z SQLite i MySQL, a dystrybucja binarna na Mac z SQLite. Jeżeli chce się użyć innej bazy danych, trzeba uruchomić web2py z dystrybucji źródłowej i zainstalować odpowiedni sterownik wymagany przez wybraną bazę danych.
Po zainstalowaniu właściwego sterownika należy uruchomić web2py ze źródła -- web2py go odnajdzie i użyje. Oto wykaz sterowników:
Baza danych | Sterowniki (źródło) |
SQLite | sqlite3 lub pysqlite2 lub zxJDBC [zxjdbc] (na Jython) |
PostgreSQL | psycopg2 [psycopg2] lub pg8000 [pg8000] lub zxJDBC [zxjdbc] (na Jython) |
MySQL | pymysql [pymysql] lub MySQLdb [mysqldb] |
Oracle | cx_Oracle [cxoracle] |
MSSQL | pyodbc [pyodbc] |
FireBird | kinterbasdb [kinterbasdb] lub fdb lub pyodbc |
DB2 | pyodbc [pyodbc] |
Informix | informixdb [informixdb] |
Ingres | ingresdbi [ingresdbi] |
Cubrid | cubriddb [cubridb] |
Sybase | Sybase [Sybase] |
Teradata | pyodbc [Teradata] |
SAPDB | sapdb [SAPDB] |
MongoDB | pymongo [pymongo] |
IMAP | imaplib [IMAP] |
Sterowniki sqlite3
, pymysql
, pg8000
i imaplib
dostarczane są wraz z web2py. Obsługa MongoDB jest eksperymentalna. Opcja IMAP umożliwia użycie DAL z dostępem IMAP.
DAL - krótki przegląd
W web2py zdefiniowane są następujące klasy, które składają się na DAL:
DAL reprezentuje połączenie z bazą danych. Na przykład:
db = DAL('sqlite://storage.db')
Table reprezentuje tabele bazy danych. Nie tworzy się bezpośrednio instancji z klasy Table. Zamiast tego wykorzystuje się metodę DAL.define_table
.
db.define_table('mytable', Field('myfield'))
Najważniejsze metody Table, to:
.insert
, .truncate
, .drop
i .import_from_csv_file
.
Field reprezentuje pole bazy danych. Można tworzyć jego instancje i przekazywać argument obiektu do metody DAL.define_table
.
select
. Może być traktowany jak lista obiektów Row:rows = db(db.mytable.myfield!=None).select()
Obiekt Row zawiera wartości pola.
for row in rows:
print row.myfield
Query jest obiektem reprezentującym klauzulę "where" SQL:
myquery = (db.mytable.myfield != None) | (db.mytable.myfield > 'A')
Set jest obiektem reprezentującym zestaw rekordów. Jego najważniejsze metody, to count
, select
, update
i delete
. Na przykład:
myset = db(myquery)
rows = myset.select()
myset.update(myfield='somevalue')
myset.delete()
Expression jest czymś w rodzaju wyrażenia orderby
lub groupby
. Klasa Field jest pochodną klasy Expression. Oto przykład.
myorder = db.mytable.myfield.upper() | db.mytable.id
db().select(db.table.ALL, orderby=myorder)
Stosowanie DAL poza web2py
stosowanie DAL poza web2py
DAL web2py może być stosowany poza środowiskiem web2py poprzez:
from gluon import DAL, Field
# można też rozważyć: from gluon.validators import *
Konstruktor DAL
Podstawowe użycie:
>>> db = DAL('sqlite://storage.db')
Baza danych jest teraz połączona a połączenie jest przechowywane w globalnej zmiennej db
.
W każdej chwili można pobrać łańcuch połączenia.
>>> print db._uri
sqlite://storage.db
i nazwę bazy danych
>>> print db._dbname
sqlite
Ciąg znakowy połączenia nosi nazwę _uri
ponieważ jest instancją Uniform Resource Identifier.
DAL umożliwia dokonywanie wielu połączeń z tą samą bazą danych lub z różnymi bazami danych, nawet z bazami różnych typów. Na razie zakładamy obecność tylko jednej bazy danych, ponieważ jest to najpowszechniejsza sytuacja.
Sygnatura DAL
DAL(
uri='sqlite://dummy.db',
pool_size=0,
folder=None,
db_codec='UTF-8',
check_reserved=None,
migrate=True,
fake_migrate=False,
migrate_enabled=True,
fake_migrate_all=False,
decode_credentials=False,
driver_args=None,
adapter_args=None,
attempts=5,
auto_import=False,
bigint_id=False,
debug=False,
lazy_tables=False,
db_uid=None,
do_connect=True,
after_connection=None,
tables=None,
ignore_field_case=True,
entity_quoting=False,
table_hash=None)
Łańcuch połączenia (parametr uri)
Połączenie z bazą danych jest ustanawiane przez utworzenie instancji klasy DAL:
>>> db = DAL('sqlite://storage.db', pool_size=0)
db
nie jest słowem kluczowym, ale lokalną zmienną przechowująca obiekt połączenia DAL
. Ma się wolny wybór w jej nazwaniu, nie musi to być db
. Konstruktor DAL
wymaga jednego argumentu -- ciągu znakowego połączenia. Ciąg znakowy połączenia jest jedynym wyrażeniem kodu web2py zależnym od typu używanej bazy danych:
ndb
SQLite | sqlite://storage.db |
MySQL | mysql://username:password@localhost/mydb |
PostgreSQL | postgres://username:password@localhost/mydb |
MSSQL (legacy) | mssql://username:password@localhost/mydb |
MSSQL (>=2005) | mssql3://username:password@localhost/mydb |
MSSQL (>=2012) | mssql4://username:password@localhost/mydb |
FireBird | firebird://username:password@localhost/mydb |
Oracle | oracle://username/password@mydb |
DB2 | db2://username:password@mydb |
Ingres | ingres://username:password@localhost/mydb |
Sybase | sybase://username:password@localhost/mydb |
Informix | informix://username:password@mydb |
Teradata | teradata://DSN=dsn;UID=user;PWD=pass;DATABASE=mydb |
Cubrid | cubrid://username:password@localhost/mydb |
SAPDB | sapdb://username:password@localhost/mydb |
IMAP | imap://user:password@server:port |
MongoDB | mongodb://username:password@localhost/mydb |
Google/SQL | google:sql://project:instance/database |
Google/NoSQL | google:datastore |
Google/NoSQL/NDB | google:datastore+ndb |
Trzeba mieć na uwadze, że baza danych SQLite składa się z pojedynczego pliku. Jeśli on nie istnieje, to zostanie utworzony. Plik ten jest blokowany w czasie wykonywania operacji dostępu do bazy danych. W przypadku baz danych MySQL, PostgreSQL, MSSQL, FireBird, Oracle, DB2, Ingres i Informix baza danych "test" musi zostać utworzona poza web2py. Po nawiązaniu połączenia web2py będzie mógł odpowiednio tworzyć, zmieniać i usuwać tabele.
W przypadku Google/NoSQL opcja +ndb
włącza NDB. NDB używa buforu Memcache do odczytu danych, które są często wykorzystywane. Jest to całkowicie zautomatyzowane i jest realizowane na poziomie magazynu danych, a nie na poziomie web2py.
Możliwe jest również ustawienie ciągu połączenia na wartość None
. W tym przypadku DAL nie będzie się łączył z jakąkolwiek bazą danych, ale API może być ciągle dostępne dla testowania. Przykład tego omówiono w rozdziale 7.
Czasem zachodzi potrzeba wygenerowania zapytania SQL tak jakby było ustanowione połączenie z bazą danych, ale bez rzeczywistego łączenia się z bazą danych. Można to zrobić tak:
db = DAL('...', do_connect=False)
W tym przypadku będzie można wywołać _select
, _insert
, _update
i _delete
do wygenerowania zapytania SQL ale nie select
, insert
, update
i delete
. W większości przypadków można użyć do_connect=False
nawet bez posiadania wymaganych sterowników bazy danych.
Proszę zwrócić uwagę, że domyślnie web2py używa kodowania znaków utf8 dla bazy danych. Jeśli pracuje się z istniejącą bazą danych, która zachowuje się inaczej, to można zmienić sposób kodowania znaków przez web2py wykorzystując opcjonalny parametr db_codec
, podobnie do tego:
db = DAL('...', db_codec='latin1')
w przeciwnym razie otrzyma się bilety UnicodeDecodeError.
Buforowanie połączeń
Drugi argument konstruktora DAL, to pool_size
, domyślnie zero.
Jako że ustanawianie połączenia z bazą danych za każdym razem jest raczej powolne, web2py implementuje mechanizm buforowania połączeń (ang. connection pooling). Po tym jak połączenie zostaje ustanowione, strona obsłużona i transakcja zakończona, połączenie nie jest zamykane ale trafia do puli. Gdy zostaje odebrane następne żądanie, web2py próbuje odzyskać połączenie z puli i wykorzystać go do nowej transakcji. Jeżeli połączenie nie jest dostępne w puli, to zostaje ustanowione nowe połączenie.
Przy uruchomieniu web2py pula jest zawsze pusta. Pula rośnie od minimalnej wartości określonej w pool_size
do maksymalnej liczby współbieżnych żądań. Oznacza to, że jeśli pool_size=10
ale nasz serwer nie otrzyma nigdy więcej niż 5 współbieżnych żądań, to rzeczywista wielkość puli wynosi 5. Jeśli pool_size=0
, to buforowanie połączeń nie jest dokonywane.
Połączenia w pulach są współdzielone pomiędzy kolejnymi wątkami, w tym sensie, że mogą być użyte przez dwa różne ale nie jednoczesne wątki. Istnieje tylko jedna pula dla każdego procesu web2py.
Parametr pool_size
jest ignorowany dla połączeń z SQLite i Google App Engine. Buforowanie połączeń jest ignorowane dla SQLite, ponieważ nie przynosi żadnych korzyści.
Niepowodzenie w połączeniu
Jeśli web2py nie może połączyć się z baza danych i oczekuje 1 sekundę, to podejmuje 5 takich prób, zanim zadeklaruje awarię. W przypadku buforowania połączeń możliwe jest, że buforowane połączenie, które pozostaje otwarte, ale nie używane przez pewien czas zostaje zamknięte przez zakończenie działania bazy danych. Dzięki tej możliwości web2py próbuje przywrócić uszkodzone połączenia.
Leniwe tabele
Ustawienie opcji lazy_tables = True
zapewni znaczny wzrost wydajności. Zobacz niżej: leniwe tabele
Replikacja baz danych
Pierwszym argumentem DAL(...) może być lista adresów URI. W tym przypadku web2py próbuje połączyć się z każdym z nich. Głównym celem jest obsłużenie wielu serwerów baz danych i wyrównywanie obciążenia między nimi. Oto typowy przykład użycia:
db = DAL(['mysql://...1','mysql://...2','mysql://...3'])
W tym przypadku DAL próbuje połączyć się z pierwszym serwerem i w przypadku niepowodzenia będzie próbował połączyć się z kolejnym. Można to również użyć do rozdzielania ładowania w bazie danych z konfiguracją master-slave. Jest to szczegółowo omówione w rozdziale 13 w kontekście skalowalności.
Zarezerwowane słowa kluczowe
Argument check_reserved
, to kolejny argument, który może być przekazany do konstruktora DAL. Instruuje on aby sprawdzić nazwy tabel, czy nie zawierają zarezerwowanych słów kluczowych SQL. Argument ten domyślnie ustawiany jest na None.
Jest to lista ciągów znakowych, będących nazwami adapterów motoru bazy danych.
Nazwa adaptera jest taka sama, jak w ciągu połączenia DAL. Tak więc, jeśli chce się sprawdzić słowa kluczowe wobec PostgreSQL i MSSQL, to ciąg połączenia będzie wyglądać następująco:
db = DAL('sqlite://storage.db',
check_reserved=['postgres', 'mssql'])
DAL skanuje słowa kluczowe w tej samej kolejności jak na liście.
Istnieją jeszcze dwie dodatkowe opcje "all" i "common". Jeśli określi się opcję all
, to zostaną sprawdzone wszystkie znane słowa kluczowe SQL (dla wszystkich znanych dialektów SQL). W przypadku określenia opcji common
, zostaną sprawdzone tylko popularne słowa kluczowe SQL, takie jak SELECT
, INSERT
, UPDATE
itd.
W celu obsługi motorów baz danych można również określić, czy chce się również sprawdzić niezarezerwowane słowa kluczowe SQL. W tym przypadku należy dołączyć do nazwy przyrostek _nonreserved
. Na przykład:
check_reserved=['postgres', 'postgres_nonreserved']
Następujące motory baz danych obsługują zarezerwowane słowa kluczowe.
PostgreSQL | postgres(_nonreserved) |
MySQL | mysql |
FireBird | firebird(_nonreserved) |
MSSQL | mssql |
Oracle | oracle |
Ustawienie cytowania encji (entity_quoting, ignore_field)
Na poziomie DAL można również używać jawnego cytowania encji SQL. Działa to przejrzyście, tak więc można stosować te same nazwy w Python i w schemacie DB.
ignore_field_case = True
entity_quoting = True
Oto przykład:
db = DAL('postgres://...', ...,ignore_field_case=False, entity_quoting=True)
db.define_table('table1', Field('column'), Field('COLUMN'))
print db(db.table1.COLUMN != db.table1.column).select()
Inne parametry konstruktora DAL
Lokalizacja folderu bazy danych
folder
– folder, w którym mają być utworzone pliki .table
. W web2py jest ustawiane automatycznie. Poza web2py trzeba ścieżkę określić jawnie.
Ustawienie domyślnej migracji
Migracje są opisane dalej, w rozdziale Migracje. Ustawienia migracji konstruktora DAL są wartościami logicznymi wpływającymi na na domyślne i globalne zachowanie:
migrate = True
ustawia domyślne zachowanie migracji dla wszystkich tabelfake_migrate = False
ustawia domyślne zachowanie fake_migrate dla wszystkich tabelmigrate_enabled = True
, jeśli ustawione na False, wyłącza wszystkie migracjefake_migrate_all = False
, jeśli ustawione na True finguje migrowanie wszystkich tabel
Wykorzystywanie powłoki web2py
Można eksperymentować z API DAL wykorzystując powłoką web2 (z opcją -S linii poleceń).
Rozpocznij od utworzenie połączenia. W dalszej częsci tego rozdziału używać będziemy SQLite. Nic w tych przykładach się nie zmieni po zmianie silnika bazy danych.
Konstruktor Table
Sygnatura define_table
Tabele definiowane są w DAL poprzez define_table
:
>>> db.define_table('person', Field('name'),
id=id,
rname=None,
redefine=True
common_filter,
fake_migrate,
fields,
format,
migrate,
on_define,
plural,
polymodel,
primarykey,
redefine,
sequence_name,
singular,
table_class,
trigger_name)
Wyrażenie to definiuje, przechowuje i zwraca obiekt Table
o nazwie "person" zawierające pole (kolumnę) "name". Obiekt ten może być również dostępny poprzez db.person
, tak więc nie trzeba pozyskiwać zwracanej wartości.
id
: Uwagi na temat klucza głównego
Nie deklaruje się pola o nazwie "id", ponieważ jest ono zawsze tworzone przez web2py automatycznie. Każda tabela zawiera domyślnie pole o nazwie "id". Jest to pole samoprzyrostowe typu integer (startujace od 1) wykorzystywane do odwoływania się do rekordu i mające unikalne wartości, tak więc "id" jest kluczem głównym (ang. primary key).
Uwaga: to, czy pole 'id" startuje od wartości 1 zależy od specyfiki motoru bazy danych. Na przykład, nie jest tak w bazie NoSQL Google App Engine.
Opcjonalnie można zdefiniować pole type='id'
i web2py będzie wykorzystywał to pole jako samoprzyrostowe pole id. Nie jest to zalecane, z wyjątkiem używania tabel baz danych starszych wersji. Z pewnym ograniczeniem można również korzystać z innych kluczy głównych, co jest omówione w rozdziale :ref:`"Starsze bazy danych a indeksowanie tabel" <dal_primary_key>`.
plural
i singular
Obiekty smartgrid mogą wymagać informacji o nazwach tabel w liczbie pojedyńczej i mnogiej. Z założenia obiekty te są inteligentne, ale parametry plural
i singular
umożliwiają jawne określenie takich nazw. Więcej informacji znajduje się w opisie obiektu smartgrid.
redefine
Tabele wystarczy zdefiniować tylko raz, ale można wymusić, aby web2py przedefiniowywał istniejące tabele:
db.define_table('person', Field('name'))
db.define_table('person', Field('name'), redefine=True)
Redefinicja może spowodować migrację, jeśli zawartość pola jest inna.
format
: reprezentacja rekordu
Chociaż nie jest to obowiązkowe, zaleca się określenie reprezentacji formatu dla rekordu za pomocą parametru format
.
>>> db.define_table('person', Field('name'), format='%(name)s')
lub
>>> db.define_table('person', Field('name'), format='%(name)s %(id)s')
albo nawet coś bardziej skomplikowanego przy użyciu funkcji:
>>> db.define_table('person', Field('name'),
format=lambda r: r.name or 'anonymous')
Atrybut format
może być wykorzystywany w dwóch celach:
- Do reprezentowania przywoływanych rekordów w polach rozwijanych select/option.
- Do ustawiania atrybutu
db.othertable.person.represent
dla wszystkich pól odwołujących się do tej tabeli. Oznacza to, że SQLTABLE nie będzie pokazywać odwołań przez id, ale zamiast tego będzie wykorzystywać format preferowanej reprezentacji.
rname
: reprezentacja rekordu
Atrybut rname
ustawia nazwę tabeli dla bazy danych. Umożłiwia to aliasowanie nazwy tabeli web2py i to, że rname
można wykorzystac jako rzeczywistą nazwą tabeli używaną podczas konstruowania zapytania dla bazy danych. W celu zilustrowania chociaż jednego zastosowania, rname
można użyć do dostarczenia pełnej kwalifikowanej nazwy tabeli do MSSQL, która znajduje się w innej bazie danych na serwerze:
rname = 'db1.dbo.table1'
primarykey
: obsługa starszych tabel
Atrybut primarykey
pomaga obsłużyć starsze tabele z istniejącym kluczem głównym, nawet wieloczęściowym. Zobacz rozdział Starsze bazy danych w dalszej części książki.
migrate
, fake_migrate
Opcja migrate
ustawia opcje migracji dla tabeli. Zobacz rozdział Migracja tabeli.
table_class
Jeśli definiuje się własna klasę Table jako podklasę gluon.dal.Table
, można dostarczyć ją w tym atrybucie. Pozwala on na rozszerzanie i nadpisywanie metod. Przykład:
table_class=MyTable
sequence_name
(Opcjonalnie) Nazwa sekwencji niestandardowej tabeli (jeśli jest obsługiwana przez bazę danych). Można utworzyć SEQUENCE (rozpoczynając od 1, z przyrosten o 1) lub stosować to dla starszych (legacy) tabel z własną sekwencją. Prosze mieć na uwadze, że gdy jest to konieczne, to web2py będzie domyślnie tworzył sekwencje w sposób automatyczny (rozpoczynając od 1).
trigger_name
(optional) Dotyczy sequence_name
. Istostne dla niektórych motorów baz danych, które nie obsługują auto-przyrostowych pól numerycznych.
polymodel
Dla Google App Engine
on_define
on_define
jest wywołaniem zwrotnym wyzwalanym gdy tworzona jest lazy_table, ale jest wywoływane zawsza, jeśli tabela nie jest leniwa. Pozwala na dynamiczne zmiany w tabeli bez utraty korzyści płynących z opóźnienia instancji.
Przykład:
db = DAL(lazy_tables=True)
db.define_table('person',Field('name'),Field('age','integer'),
on_define=lambda table: [
table.name.set_attributes(requires=IS_NOT_EMPTY(),default=''),
table.age.set_attributes(requires=IS_INT_IN_RANGE(0,120),default=30),
Proszę mieć na uwadze, że ten przykład pokazuje jak używać on_define
, ale nie jest to w rzeczywistości konieczne. Po prostu, wartość requires
może zostać dodana do definicji Field
i tabela będzie ciągle leniwa. Jednak requires
, która pobiera obiekt Set
jako pierwszy argument, taki jak IS_IN_DB, będzie wykonywać zapytanie w stylu
db.sometable.somefield == some_value
sometable
. Jest to sytuacja opisana w on_define
.Leniwe tabele - główny sposób zwiększenia wydajności
Modele web2py są wykonywane przed kontrolerami, więc wszystkie tabele są definiowane przy każdym żądaniu. Nie wszystkie tabele są potrzebne do obsługi każdego żądania, więc możliwe jest, że czasami zdefiniowane tabele nie są wykorzystane. Mogą się tu przydać modele warunkowe (modele warunkowe, rozdział 4), ale web2py oferuje duży wzrost wydajności poprzez mechanizm lazy_tables
. Funkcjonalność ta oznacza, że tworzenie tabeli zostaje odroczone do momentu, w którym tabela jest rzeczywiście przywoływana. Aby włączyć mechanizm leniwych tabel należy ustawić parametr DAL(...,lazy_tables=True)
. Jest to jeden z najbardziej istotnych mechanizmów zwiększających wydajność web2py.
Dodawanie atrybutów do pól i tabel
Jeśli chce się dodać własne atrybuty do pól, można to zrobić tak:
db.table.field.extra = {}
"extra" nie jest słowem kluczowym, to indywidualny atrybut, teraz dołączony do obiektu field. Można go zastosować również również do tabel, ale musi być poprzedzony znakiem podkreślenia, aby uniknąć konfliktu nazewniczego z polami:
db.table._extra = {}
Konstruktor Field
Oto domyślne wartości konstruktora Field:
Field(name, 'string', length=None, default=None,
required=False, requires='<default>',
ondelete='CASCADE', notnull=False, unique=False,
uploadfield=True, widget=None, label=None, comment=None,
writable=True, readable=True, update=None, authorize=None,
autodelete=False, represent=None, compute=None,
uploadfolder=None,
uploadseparate=None,uploadfs=None)
Nie wszystkie z tych parametrów są istotne dla każdego pola. Parametr "length" ma znaczenie tylko dla dla pól typu "string". Parametr "uploadfield" i "authorize" są istotne tylko dla pól typu "upload". Parametr "ondelete" ma znaczenie dla pól typu "reference" i "upload".
length
ustala maksymalna długość pól typu "string", "password" lub "upload". Jeślilength
nie jest określony, zostaje zastosowana wartość domyślna, ale nie zapewnia ona kompatybilności wstecznej. Aby uniknąć niechcianych migracji w czasie aktualizacji , zalecamy określanie zawsze długości pól typu string, password i upload.default
ustawia domyślną wartość pola. Domyślna wartość jest używana gdy nie jest jawnie określona. Jest ona również wykorzystywana do wstępnego ustawiania wartości pól formularzy budowanych z tabel przy użyciu SQLFORM. Proszę mieć na uwadze, że zamiast stałej wartości, parametrdefault
może być funkcja (w tym funkcja lambda), która zwraca wartość odpowiedniego typu dla pola. W takim przypadku funkcja jest wywoływana raz dla każdego wstawianego rekordu, nawet gdy w pojedynczej transakcji wstawianych jest wiele rekordów.required
instruuje DAL, że nie jest jest dozwolone wstawianie rekordów do tej tabeli, jeśli wartość pola nie jest jawnie określona.requires
jest walidatorem lub listą walidatorów. Nie jest to używany przez DAL, ale jest wykorzystywane przez SQLFORM. Domyślne wartości walidatorów dla danych typów są pokazane w niżej prezentowanej tabeli.
Proszę pamiętać, że
requires=...
jest egzekwowane na poziomie fomularza,required=True
jest egzekwowane na poziomie DAL (wewnętrznie), podczas gdynotnull
,unique
iondelete
są egzekwowane na poziomie bazy danych. Jest ważne aby rozróżniać to podczas programowania w DAL, choć czasami może wydawać się to nadmiarowe.
uploadfolder
chociaż ma domyślną wartośćNone
, to większość adapterów DB wgrywa pliki do os.path.join(request.folder, 'uploads'). MongoAdapter obecnie nie wykonuje tego.rname
, powoduje dostarczanie pola z "rzeczywistą nazwą" - nazwą zrozumiała dla adaptera bazy danych. Nazwa web2py tego pola jest wtedy skutecznym aliasem.ondeleteondelete
tłumaczone jest na wyrażenia "ON DELETE" SQL. Domyślnie jest ustawione na "CASCADE". Informuje to bazę danych, że podczas usuwania rekordu należy również usunąć wszystkie rekody odniesione do usuwanego rekordu. W celu wyłączenia tej możliwości, trzeba ustawićondelete
na "NO ACTION" lub "SET NULL".notnull=True
tłumaczone jest na wyrażenia "NOT NULL" SQL. Zapobiega to możliwości wstawienia w polu wartości null.unique=True
tłumaczone jest na wyrażenie "UNIQUE" i determinuje unikalność wartości pola w danej tabeli. Jest egzekwowane na poziomie bazy danych.uploadfield
na zastosowanie tylko do pól typu "upload". Pole typu "upload" przechowuje nazwę pliku przechowywanego gdzieś indziej, domyślnie w systemie plików aplikacji w folderze "uploads/". Jeśli parametruploadfield
jest ustawiony naTrue
, to plik jest przechowywany w polu blob w tej samej tabeli i wartośćuploadfield
jest nazwą tego pola blob. Jest to omówione szczegółowo dalej w kontekście SQLFORM.uploadfolder
domyślnie jest ustawiony na folder "uploads/" aplikacji. Jeśli ustawi się to na inna ścieżkę, pliki będą pobierane do wskazanego folderu.
Na przykład,
Field(...,uploadfolder=os.path.join(request.folder,'static/temp'))będzie ładował pliki do folderu "web2py/applications/myapp/static/temp".
uploadseparate
jeśli jest ustawione naTrue
, będzie powodować przesyłanie plików do różnych podfolderóœ folderu uploadfolder. Jest to zoptymalizowane tak, aby unikać zbyt dużej ilośći plików w tym samym folderze/podfolderze. UWAGA: Nie można zmienić wartościuploadseparate
z True na False bez zerwania odnośników do istniejących juz plików. W web2py albo używa się oddzielnych podfolderów albo nie. Zmiana tego zachowania po tym jak pliki zostały już przesłane uniemożliwi, aby web2py mógł odzyskać te plików. Jest możliwe przeniesienie plików i naprawienie problemu, ale nie jest to tutaj opisane.uploadfs
umożliwia określenie innefo systemu plików, gdzie przechowywane są przesłane pliki, w tym magazynu danych Amazon S3 lub zdalnej przestrzeni SFTP. Opcja ta wymaga zainstalowania PyFileSystem.uploadfs
musi wskazywać naPyFileSystem
.PyFileSystemuploadfs
widget
musi być jednym z dostęþnych obiektów widget, w tym indywidualnych widżetów, na przykład:SQLFORM.widgets.string.widget
. Lista dostęþnych widżetów zostanie omówiona później. Każdy typ pola ma domyślny widżet.label
jest ciągiem znakowym (lub helperem albo czymś, co może być serializowane do ciągu znakowego), który zawiera etykitę do zastosowania dla tego pola w automatycznie generowanych formularzach.comment
jest ciągiem znakowym (lub helperem albo czymś co może być serializowane do ciągu znakowego), który zawiera komentarz dolączany do pola i jest wyświetlany na prawo od pola input w automatycznie generowanych formularzach.writable
określa, czy w formularzach pole jest możliwe do zapisu.readable
określa, czy w formularzach pole jest możliwe do odczytu. Jeśli pole nie jest możliwe do odczytu ani do zapisu, to nie będzie wyświetlane w tworzonych lub aktualizowanych formularzach.update
zawiera domyślną wartość pola podczas aktualizacji rekordu.compute
jest opcjonalną funkcją. Jeśli rekord jest wstawiany lub aktualizowany, zostanie wykonana funkcja obliczeniowa podana w tym parametrze a pole wypełnione wynikiem tej funkcji. Rekord jest przekazywany do funkcji obliczeniowej jakodict
ale ten słownik nie może zawierać bieżącej wartości tego lub innego pola obliczeniowego.authorize
można wykorzystać do wymagania kontroli dostępu do odpowiedniego pola. Tylko dla pól "upload". Jest to szczegółowo omówione w kontekście uwierzytelniania i autoryzacji.autodelete
określa, czy wartość koresponujacego poluploaded
powinna zostać usunięta podczas usuwania rekordu wskazujacego ten plik. Tylko dla pól "upload". Jednakże, rekordy usuwane przez samą bazę danych w operacji CASCADE nie wywołają automatycznego usuwania wartości pól przez web2py. Na grupe web2py Google omówione jest rozwiązanie tego problemu.represent
może mieć wartość None lub może wskazywać funkcję, która pobiera wartość pola i zwraca alternatywną reprezentację dla wartości tego pola.
Przykłady:
db.mytable.name.represent = lambda name,row: name.capitalize() db.mytable.other_id.represent = lambda id,row: row.myfield db.mytable.some_uploadfield.represent = lambda value,row: A('get it', _href=URL('download', args=value))
Typy pól
Typ pola | Domyślne walidatory pola |
string | IS_LENGTH(length) domyślna długość, to 512 |
text | IS_LENGTH(65536) |
blob | None |
boolean | None |
integer | IS_INT_IN_RANGE(-1e100, 1e100) |
double | IS_FLOAT_IN_RANGE(-1e100, 1e100) |
decimal(n,m) | IS_DECIMAL_IN_RANGE(-1e100, 1e100) |
date | IS_DATE() |
time | IS_TIME() |
datetime | IS_DATETIME() |
password | None |
upload | None |
reference <table> | IS_IN_DB(db,table.field,format) |
list:string | None |
list:integer | None |
list:reference <table> | IS_IN_DB(db,table.field,format,multiple=True) |
json | IS_JSON() |
bigint | None |
big-id | None |
big-reference | None |
Typ decimal
wymaga i zwraca wartości jako obiekty Decimal
, tak jak zdefiniowano to w module decimal
Pythona. SQLite nie obsługuje typu decimal
, więc wewnętrznie traktujemy go jako double
. Argumenty (n,m) są odpowiednio liczbą ogólną cyfr i liczbą cyfr po przecinku.
Typy big-id
i big-reference
są tylko obsługiwane przez niektóre motory bazy danych i są typami eksperymentalnymi. Zwykle nie są używane jako typy pól, chyba że chodzi o starsze tabele, jednak konstruktor DAL posiada argument bigint_id
, który przy ustawieniu na True
sprawia, ze pola id
i reference
stają się odpowiednio typu big-id
i big-reference
.
Pola list formatu list:<type>
są specjalne, ponieważ zostały zaprojektowane tak, aby korzystać z niektórych denormalizacyjnych możliwości NoSQL (w przypadku Google App Engine NoSQL: typy pól ListProperty
i StringListProperty
) i być wszystkie przenoszonymi z powrotem na obsługiwane przez web2py relacyjne bazy danych. Listy na relacyjnych bazach danych są przechowywane jako pola tekstowe (text
). Poszczególne elementy są rozdzielane znakiem pionowej kreski |
i w związku z tym każdy znak kreski |
w łańcuchu tekstowym musi być zabezpieczony przez dodanie drugiej kreski (||
). Jest to omówione w oddzielnym podrozdziale.
Typ pola json
jest prawie wyjaśniony. Można w nim przechowywać wszystkie serializowalne obiekty json. Został zaprojektowany specjalnie do pracy z MongoDB i przenoszenia zwrotnego wartości na inne adaptery baz danych, w celu zachowania przenośności.
Pola "blob" są również specjalne. Domyślnie, dane binarne są kodowane w base64 będąc zapisywanymi do rzeczywistych pól bazy danych i rozkodowywane podczas ekstrakcji. Ma to niekorzystny skutek zużywania o 25% więcej przestrzeni dyskowej niż potrzeba dla pól blob, ale ma też dwie zalety. Średnio zmniejsza ilość danych przesyłanych pomiędzy web2py a serwerem bazy danych i sprawia, że komunikacja uniezależnia się od konwencji sekwencji unikowych stosowanych dla konkretnych motorów baz danych.
Modyfikowanie pól i tabel w czasie wykonania
Większość atrybutów pól i tabel może być modyfikowana po ich zdefiniowaniu:
db.define_table('person',Field('name',default=''),format='%(name)s')
db.person._format = '%(name)s/%(id)s'
db.person.name.default = 'anonymous'
Proszę zauważyć, że atrybuty tabel są zwykle poprzedzane znakiem podkreślenia, aby uniknąć konfliktu z nazwami pól.
Można uzyskać listę tabel, które zostały zdefiniowane dla danego połączenia z bazą danych:
>>> print db.tables
['person']
Można również uzyskać listę pól zdefiniowanych dla określonej tabeli:
>>> print db.person.fields
['id', 'name']
Można zapytać o typ tabeli:
>>> print type(db.person)
<class 'pydal.objects.Table'>
i można uzyskać dostęp do tabeli z połączenia DAL używając:
>>> print type(db['person'])
<class 'pydal.objects.Table'>
Podobnie, można uzyskać dostęp do pól wykorzystując ich nazwę na wiele równoważnych sposobów:
>>> print type(db.person.name)
<class 'pydal.objects.Field'>
>>> print type(db.person['name'])
<class 'pydal.objects.Field'>
>>> print type(db['person']['name'])
<class 'pydal.objects.Field'>
Można uzyskać dostęp do atrybutów określonego pola:
>>> print db.person.name.type
string
>>> print db.person.name.unique
False
>>> print db.person.name.notnull
False
>>> print db.person.name.length
32
w tym jego tabeli, nazwy tabeli i połączenia:
>>> db.person.name._table == db.person
True
>>> db.person.name._tablename == 'person'
True
>>> db.person.name._db == db
True
Obiekt pola ma również metody. Niektóre z nich są wykorzystywane do budowy zapytań, co zobaczymy to później. Specjalną metoda obiektu pola jest validate
i wykorzystywana jest do wywoływania walidatorów dla danego pola.
print db.person.name.validate('John')
zwraca krotkę (value, error)
. Element error
przybiera wartość None
, jeśli dane wejściowe przechodzą walidację.
Migracje
Metoda define_table
sprawdza czy istnieje odpowiednia tabela. Jeśli nie, to jest generowane i wykonywane zapytanie SQL tworzące tą tabelę. Jeżeli tabela istnieje, ale różni się od teraz definiowanej, to generowane i wykonywane jest zapytanie SQL zmieniające istniejącą tabelę. Jeśli ulega zmianie typ pola, ale nie jego nazwa, to zostanie podjęta próba konwersji danych (gdy się tego nie chce, to trzeba ponownie zdefiniować tabelę, pozwalając aby web2py usunął takie pole i jeszcze raz go dodał). Jeśli tabela istnieje i pasuje do obecnej definicji, zostanie pozostawiona bez zmian. W każdym przypadku metoda ta tworzy obiekt db.person
reprezentujący tabelę.
Zachowanie takie nazywamy "migracją". Wszystkie próby migracji rejestrowane są w pliku "databases/sql.log".
Pierwszym argumentem define_table
jest zawsze nazwa tabeli. Inne nienazwane argumenty są polami (obiektami Field). Funkcja ta pobiera również opcjonalnie kluczowy argument o nazwie "migrate":
>>> db.define_table('person', Field('name'), migrate='person.table')
Wartością argumentu migrate
jest nazwa pliku (w folderze "databases" aplikacji) w którym web2py przechowuje wewnętrzne informacje o migracji dla tej tabeli. Pliki te są bardzo ważne i nigdy nie powinny być usuwane, gdy istnieją odpowiadające im tabele. W przypadku, gdy tabela zostanie usunięta a odpowiadający jej plik migracji istnieje, to można go ręcznie usunąć. Domyślnie argument migrate
jest ustawiony na True. Powoduje to, że web2py wygeneruje nazwę pliku z hasha ciągu połączenia. Jeśli migrate
jest ustawione na False, to migracja nie jest wykonywana a web2py zakłada, że dana tabela istnieje w magazynie danych i zawiera (co najmniej) pole wyszczególnione w define_table
. Najlepiej jest nadać plikowi migracyjnemu jednoznaczną nazwę.
W tej samej aplikacji nie może być dwóch tabel odnoszonych do jednego pliku migracyjnego.
Argument migrate
jest zawarty również w klasie DAL. Określa on domyślną wartość argumentu migrate
dla define_table
. Na przykład,
>>> db = DAL('sqlite://storage.db', migrate=False)
ustawi domyślną wartość migrate
na False dla db.define_table
podczas wywoływania tej metody bez argumentu migrate
.
Trzeba mieć na uwadze, że web2py migracją obejmuje tylko nowe kolumny, usunięte kolumny i kolumny ze zmienionym typem (z wyjątkiem sqlite). web2py nie migruje zmian dokonanych w wartościach atrybutów takich jak
default
,unique
,notnull
iondelete
.
Migracje można wyłączyć na raz dla wszystkich tabel:
db = DAL(...,migrate_enabled=False)
Jest to zalecane w przypadku, gdy dwie aplikacje współdzielą tą samą bazę danych. Tylko jedna z tych dwóch aplikacji powinna wykonać migrację, pozostałe należy wyłączyć.
Naprawa zepsutych migracji
Istnieją dwa ogólnie znane problemy związane z migracją i są sposoby na ich rozwiązanie.
Pierwszy problem jest specyficzny dla SQLite. SQLite nie wymusza typu kolumn i nie może usuwać kolumn. Oznacza to, że jeśli ma się kolumnę typu string i się ją usunie, to w rzeczywistości nie zostanie ona usunięta. Jeśli następnie doda się tą kolumnę ponownie z innym typem (na przykład datetime), uzyska się w efekcie kolumnę datetime, która zawiera ciągi znakowe (praktycznie śmieci). W web2py nie monituje tego faktu, bo nie wie co jest w bazie danych, aż do czasu próby pobrania rekordu.
Jeśli web2py zwraca błąd w funkcji gluon.sql.parse podczas wyboru rekordów, to jest problem: uszkodzone dane w kolumnie, wyżej opisanych z powodów.
Rozwiązaniem jest zaktualizowanie wszystkich rekordów tabeli i zaktualizowaniu wartości w kolumnie w zapytaniu z None
.
Inny problem jest bardziej ogólny, ale typowy dla MySQL. MySQL nie pozwala na więcej niż jedną operację ALTER TABLE w transakcji. Oznacza to, że web2py musi dzielić złożone transakcje na mniejsze (jedno polecenie ALTER TABLE w jednym czasie) i zatwierdzać każdą porcje oddzielnie. Jest więc możliwe, że część transakcji zostanie zatwierdzona a część nie, pozostawiając web2py w stanie uszkodzonym. Dlaczego część transakcji się nie powiedzie? Ponieważ, przykładowo, gdy dotyczy to zmian w tabeli i przekształcenia kolumny typu string do kolumny typu datetime, web2py próbuje przekształcić dane, ale nie mogą być one zmienione. Co dzieje się z web2py? Zostaje on wprowadzony w błąd co do struktury tabeli przechowywanej w bazie danych.
Rozwiązaniem polega na włączeniu fingowanych migracji:
db.define_table(....,migrate=True,fake_migrate=True)
Przebuduje to metadane web2py dotyczące danej tabeli, zgodnie z jej definicją. Wypróbuj wiele definicji, aby zobaczyć które z nich działają (jedną przed błędną migracją a drugą po niej). Po pomyślnym rozwiązaniu problemu trzeba usunąć parametr fake_migrate=True
.
Przed przystąpieniem do rozwiązywania problemów z migracją, rozsądnie jest wykonać kopię bezpieczeństwa plików "applications/yourapp/databases/*.table".
Problemy migracyjne mogą zostać naprawione naraz dla wszystkich tabel:
db = DAL(...,fake_migrate_all=True)
To również oznacza, że model opisuje tabele, które nie istnieją w bazie danych, ale może pomóc w zawężeniu problemu.
Streszczenie sterowania migracją
Znaczenie różnych argumentów migracji pokazuje ten pseudo kod:
if DAL.migrate_enabled and table.migrate:
if DAL.fake_migrate_all or table.fake_migrate:
perform fake migration
else:
perform migration
Metoda insert
Można wstawić rekordy w określonej tabeli:
>>> db.person.insert(name="Alex")
1
>>> db.person.insert(name="Bob")
2
Metoda insert zwraca unikalną wartość "id" każdego wstawianego rekordu.
Można skrócić tabelę, czyli skasować wszystkie rekordy i zresetować licznik id.
>>> db.person.truncate()
Teraz, gdy ponownie wstawi się rekord, licznik rozpocznie numeracje identyfikatora od 1 (to jednak zależy od specyfiki zaplecza bazy danych i nie ma zastosowania w przypadku Google NoSQL):
>>> db.person.insert(name="Alex")
1
Proszę zwrócić uwagę, że do truncate
można przekazać parametry, na przykład można powiadomić SQLITE aby zrestartował licznik id.
db.person.truncate('RESTART IDENTITY CASCADE')
Argument ten jest surowym kodem SQL i dlatego zależy od motoru bazy danych.
web2py udostępnia też metodę bulk_insert
>>> db.person.bulk_insert([{'name':'Alex'}, {'name':'John'}, {'name':'Tim'}])
[3,4,5]
Kod ten pobiera listę słowników pól, które mają być wstawione i wykonuje naraz wiele wstawień. Zwraca identyfikatory wstawionych rekordów. Na obsługiwanych relacyjnych bazach dach nie przynosi to korzyści w porównaniu z pojedynczym wstawianiem rekordów w pętli, ale na Google App Engine NoSQL, powoduje znaczny wzrost prędkości.
Metody commit
i rollback
Żadna opreracja tworzenia, usuwania, wstawiania, skracania, kasowania lub aktualizji nie zostanie wykonania zanim nie zostanie wydane polecenie zatwierdzenia:
>>> db.commit()
Aby to sprawdzić, wstaw nowy rekord:
>>> db.person.insert(name="Bob")
2
i cofnąć transakcję, czyli zignorować wszystkie operacje od ostatniego zatwierdzenia:
>>> db.rollback()
Jeśli teraz dokona się ponownie wstawienia, licznik znowu zostanie ustawiony na 2, ponieważ poprzednia operacja wstawienia została wycofana.
>>> db.person.insert(name="Bob")
2
Kod w modelach, widokach i kontrolerach jest zamykany w kodzie web2py, co wygląda tak:
try:
execute models, controller function and view
except:
rollback all connections
log the traceback
send a ticket to the visitor
else:
commit all connections
save cookies, sessions and return the page
W web2py nigdy nie ma potrzeby jawnego wywoływania commit
lub rollback
, chyba że trzeba zastosować bardziej szczegółową kontrolę.
Surowy SQL
Synchronizacja zapytań
Wszystkie zapytania są automatycznie synchronizowane przez web2py. Zmienna db._timings
jest listą krotek. Każda krotka zawiera surowe zapytanie SQL, tak jak jest przekazywana sterownikowi bazy danych i czas po jakim ma być ona wykonana w sekundach. Zmienna ta może być wyświetlana w widoku przy użyciu paska narzędziowego:
{{=response.toolbar()}}
executesql
DAL pozwala jawne wystawiać wyrażenia SQL.
>>> print db.executesql('SELECT * FROM person;')
[(1, u'Massimo'), (2, u'Massimo')]
W tym przypadku zwracane wartości nie są parsowane lub przekształcane przez a format zależy od konkretnego sterownika bazy danych. Takie wykorzystanie z klauzulami wyboru (SELECT) jest zwykle niepotrzebne, ale z indeksami jest dość powszechne. Metoda executesql
pobiera cztery opcjonalne argumenty: placeholders
, as_dict
, fields
i colnames
. Argument placeholders
jest opcjonalną sekwencją wartości, które zostały w nim podstawione lub słownikiem z kluczami dopasowującymi nazwane symbole zastępcze w zapytaniu SQL, jeśli jest to obsługiwane przez sterownik DB.
Jeśli argument as_dict
jest ustawiony na True, wynikowy kursor zwracany przez sterownik DB zostanie przekształcony na sekwencję słowników driver z kluczami będącymi nazwami pól DB. Wyniki zwracane przy as_dict = True
są takie same jak te, zwracane podczas wykorzystania .as_list() do zwykłego wyboru.
[{field1: value1, field2: value2}, {field1: value1b, field2: value2b}]
Argument fields
jest listą obiektów Field DAL dopasowujących pola zwracane przez DB. Obiekty Field powinny być częścią jednego lub więcej obiektów Table zdefiniowanych w obiekcie DAL. Lista fields
może zawierać jeden lub więcej obiektów Table DAL, oprócz lub zamiast obiektów Field, lub może być po prostu prosta tabelą (nie w liście). W takim przypadku obiekty Field będą wyodrębniane z tabeli.
Zamiast określania argumentu fields
można określić argument colnames
jako listę nazw pól w formacie tablename.fieldname. Również powinny one reprezentować tabele i pola zdefiniowane w obiekcie DAL.
Możliwe jest też określenie zarówno argumentu fields
jak i związanego argumentu colnames
. W takim przypadku fields
może również zawierać obiekt Expression DAL oprócz obiektów Field. Powiązane z obiektami "fields" obiekty colnames
muszą nadal być w formacie tablename.fieldname. Dla obiektów Expression zawartych w fields
związane obiekty colnames
mogą być dowolną prawidłową etykietą.
Trzeba podkreślić, że obiekty Table DAL przywoływane w argumentach fields
lub colnames
mogą być tabelami atrapowymi, nie mającymi swojej reprezentacji w jakiejkolwiek rzeczywistej tabeli bazy danych. Warto też pamiętać, że argumenty fields
i colnames
muszą specyfikować pola w tej samej kolejności, w jakiej zwracane są one z DB.
_lastsql
Niezależnie od tego, czy SQL był wykonany ręcznie przy użyciu executesql
czy SQL został wygenerowany przez DAL, zawsze można znaleźć kod SQL w db._lastsql
. Jest to szczególnie przydatne do celów debugowania:
>>> rows = db().select(db.person.ALL)
>>> print db._lastsql
SELECT person.id, person.name FROM person;
web2py nigdy nie generuje zapytań z wykorzystaniem operatora "*". web2py zawsze jawnie dokonuje wyboru pól.
drop
Wreszcie, można usunąć tabele i wszystkie dane zostaną utracone:
>>> db.person.drop()
Uwaga dotycząca sqlite: web2py nie będzie odtwarzał usuniętej tabeli, dopóki nie przejdziesz do katalogu bazy danych w systemie plików aplikacji i nie usuniesz pliku związanego z usuniętą tabelą.
Indeksy
Obecnie API DAL nie dostarcza polecenia do tworzenia indeksów tabel, ale można to zrobić używając polecenia executesql
. Jest tak dlatego, że istnienie indeksów może spowodować kompleksowe migrowanie i lepiej radzić sobie z tym w sposób jawny. Indeksy mogą być potrzebne dla tych pól, które są używane w zapytaniach rekurencyjnych.
Oto przykład, jak utworzyć indeks przy pomocy SQL w SQLite:
>>> db = DAL('sqlite://storage.db')
>>> db.define_table('person', Field('name'))
>>> db.executesql('CREATE INDEX IF NOT EXISTS myidx ON person (name);')
Inne dialekty bazy danych mają bardzo podobną składnię, ale mogą nie obsługiwać opcjonalnej dyrektywy "IF NOT EXISTS".
Starsze bazy danych a tabele z kluczem
W pewnych warunkach można połączyć web2py ze starszymi bazami danych.
Najłatwiej jest to osiągnąć przy spełnieniu następujacych warunków:
- Każda tabela musi mieć unikalne, automatycznie przyrostowe pole typu integer o nazwie "id".
- Do rekordów trzeba się odwoływać wyłącznie za pomocą pola "id".
Podczas uzyskiwania dostępu do istniejącej tabeli, tj. tabeli nie utworzonej przez web2py w bieżącej aplikacji, zawsze trzeba ustawić migrate=False
.
Jeśli starsza tabela ma samoprzyrostowe pole integer o nazwie "id", web2py może nadal uzyskiwać do niej dostęp, ale definicja tabeli musi jawnie zawierać Field('....','id')
, gdzie ...
jest nazwą samoprzyrostowego pola integer.
Wreszcie, jeśli w starszych tabelach stosuje się klucz główny, który nie jest samoprzyrostowym polem id, można użyć "tabelę z kluczem", na przykład:
db.define_table('account',
Field('accnum','integer'),
Field('acctype'),
Field('accdesc'),
primarykey=['accnum','acctype'],
migrate=False)
primarykey
jest listą nazw pól, które tworzą klucz główny.- Wszystkie pola klucza głównego maja ustawienie
NOT NULL
, nawet jeśli tego nie określono set. - Tabele z kluczem mogą odwoływać się tylko do tabel z kluczem.
- W polach referencyjnych musi się stosować format
reference tablename.fieldname
. - Funkcja
update_record
nie jest dostępna dla rekordów tabel z kluczem.
Obecnie obsługiwane są tylko tabele dla DB2, MS-SQL, Ingres i Informix, lecz dodane zostaną inne motory baz danych.
W chwili obecnej nie możemy zagwarantować, że atrybut primarykey
działa z każdą istniejącą starszą tabelą i w każdym obsługiwanym systemie bazodanowym. Dla uproszczenia zalecamy, jeśli to możliwe, utworzenie widoku bazy danych, który ma pole samoprzyrostowe id.
Transakcje rozproszone
Obecnie funkcjonalność ta jest obsługiwana tylko dla PostgreSQL, MySQL i Firebird, ponieważ tylko te systemy udostępniają API dla dwufazowych zatwierdzeń.
Załóżmy dla przykładu, że mamy dwa (lub więcej) połączeń do odrębnych baz danych PostgreSQL:
db_a = DAL('postgres://...')
db_b = DAL('postgres://...')
W modelach i kontrolerach można zatwierdzać zmiany do nich jednocześnie:
DAL.distributed_transaction_commit(db_a, db_b)
W razie niepowodzenia funkcja ta wycofuje zmiany i zgłasza wyjątek.
Gdy do zatwierdzania zmian wykorzystywana jest jedna akcja kontrolera, jeśli ma się dwa różne połączenia i nie wywołuje się powyższej funkcji, web2py zatwierdza zmiany oddzielnie. Oznacza to, że istnieje możliwość, że jedno zatwierdzenie się powiedzie a drugie nie. Takiej sytuacji zapobiegają transakcje rozproszone.
Więcej o przesyłaniu plików
Rozważmy następujący model
>>> db.define_table('myfile',
Field('image', 'upload', default='path/'))
W tym przypadku, w polu 'upload', domyślna wartość może być opcjonalnie ustawiona na ścieżkę (bezwzględną lub względem folderu app bieżącej aplikacji) a domyślny obraz będzie ustawiony na kopię tego pliku na ścieżce. Dla każdego nowego rekordu, który nie określa obrazu, wykonywana jest nowa kopia.
Zwykle wstawianie jest obsługiwane automatycznie poprzez formularz SQLFORM lub CRUD (który jest też formularzem SQLFORM), ale czasami ma się plik w systemie plików i chce się go programowo przesłać. Można to zrealizować w ten sposób:
>>> stream = open(filename, 'rb')
>>> db.myfile.insert(image=db.myfile.image.store(stream, filename))
Możliwe jest również wstawienie pliku w prostszy sposób i posiadanie metody wstawiającej, wywołującej automatycznie magazynowanie:
>>> stream = open(filename, 'rb')
>>> db.myfile.insert(image=stream)
W tym przypadki nazwę pliku uzyskuje się z obiektu strumienia, jeśli jest dostępny.
Metoda store
przesyła obiekt pola pobierając strumień pliku i nazwę pliku. Używa ona nazwy pliku do określenia rozszerzenia (typu) pliku, tworzy nową nazwę tymczasową dla pliku (zgodnie z mechanizmem przesyłania w web2py) i ładuje zawartość pliku do nowego pliku tymczasowego (w folderze przesyłania, jeśli nie określono inaczej). Zwraca ona nową nazwę pliku tymczasowego, która następnie zostaje przechowana w polu image
tabeli db.myfile
.
Uwaga, jeśli plik ma być przechowywany w powiązanym polu blob, a nie w systemie plików, metoda store
nie będzie wstawiać pliku w pole blob (ponieważ store
jest wywoływana przed wstawianiem), tak więc plik musi być jawnie wstawiony w pole blob:
>>> db.define_table('myfile',
Field('image', 'upload', uploadfield='image_file'),
Field('image_file', 'blob'))
>>> stream = open(filename, 'rb')
>>> db.myfile.insert(image=db.myfile.image.store(stream, filename),
image_file=stream.read())
Przeciwieństwem metody store
jest metoda retrieve
:
>>> row = db(db.myfile).select().first()
>>> (filename, stream) = db.myfile.image.retrieve(row.image)
>>> import shutil
>>> shutil.copyfileobj(stream,open(filename,'wb'))
Query
, Set
, Rows
Rozważmy ponownie tabele zdefiniowaną wcześniej (i usuniętą) i wstawmy do niej trzy rekordy:
>>> db.define_table('person', Field('name'))
>>> db.person.insert(name="Alex")
1
>>> db.person.insert(name="Bob")
2
>>> db.person.insert(name="Carl")
3
Można przechowywać tabelę w zmiennej. Na przykład, w zmiennej person
:
>>> person = db.person
Można również w zmiennej przechowywać pole, takie jak name
. Przykładowo, można to zrobić tak:
>>> name = person.name
Można nawet zbudować zapytanie (używając operatorów takich jak ==, !=, <, >, <=, >=, like, belongs) i przechować ją w zmiennej q
:
>>> q = name=='Alex'
Gdy w db
wywoła się zapytanie, można zdefiniować zestaw rekordów. Ten zestaw rekordów można przechować w zmiennej s
:
>>> s = db(q)
Proszę zauważyć, że żadne zapytanie do bazy danych do tej pory nie została wywołana. DAL + Query po prostu definiują zestaw rekordów w tej zmiennej db, taki który pasuje do zapytania. web2py ustala skąd zapytanie angażuje tabelę (lub tabele) i w rzeczywistości nie ma potrzeby określania która to tabela.
Metoda select
Mając zestaw rekordów s
, można pobrać rekordy poleceniem select
:
>>> rows = s.select()
Zwraca to iterowalny obiekt klasy pydal.objects.Rows
, którego elementami są obiekty Row. Obiekty pydal.objects.Row
działają podobnie do słowników, ale ich elementy mogą również być dostępne jako atrybuty, takie jak gluon.storage.Storage
. Obiekty te różnią się od słowników tym, że ich wartości są tylko do odczytu.
Obiekt Rows umożliwia wykonywanie pętli na wyniku polecenia select i drukuje wybrane pola dla każdego wiersza:
>>> for row in rows:
print row.id, row.name
1 Alex
Można wykonać te wszystkie czynności w jednym poleceniu:
>>> for row in db(db.person.name=='Alex').select():
print row.name
Alex
Polecenie select może pobierać argumenty. Wszystkie nienazwane argumenty są interpretowane jako nazwy pól, które chce się pobrać. Na przykład, można jawnie pobrać pola "id" i "name":
>>> for row in db().select(db.person.id, db.person.name):
print row.name
Alex
Bob
Carl
Atrybut ALL tabeli wskazuje na wybór wszystkich pól:
>>> for row in db().select(db.person.ALL):
print row.name
Alex
Bob
Carl
Proszę zwrócić uwagę, że żaden ciag zapytania nie jest przekazywany do db. Platforma web2py rozumie, że jeśli nie przekazało się zapytania informująceego o tym jakie pola chce się uzyskać z tabeli person i jakie rekordy tej tabeli, to chce się uzyskać wszystkie pola tej tabeli i wszystkie jej rekordy.
Równoważna, alternatywna składnia jest następująca:
>>> for row in db(db.person.id > 0).select():
print row.name
Alex
Bob
Carl
i tu web2py rozumie, że jeśli zapytuje się o wszystkie rekordy tabeli person (id > 0) bez dodatkowej informacji, to chce się otrzymać wszystkie pola tabeli person.
Weźmy po uwagę jeden obiekt wiersz:
row = rows[0]
Można wyodrębnić jego wartości używając wiele równoważnych wyrażeń:
>>> row.name
Alex
>>> row['name']
Alex
>>> row('person.name')
Alex
Ta ostatnia składnia jest szczególnie przydatna przy wyborze wyrażenia, zamiast kolumny. Pokażemy to później.
Można też zrobić
rows.compact = False
aby wyłączyć pełną notację
row[i].name
albi zamiast tego włączyć notację kompaktową:
row[i].person.name
Ta, to jest niezwykłe i rzadko potrzebne.
Renderowanie wierszy z wykorzystanie atrybutu represent
Można przepisać wiersze zwracane przez polecenie select
, aby skorzystać z informacji formatowania, zawartej w ustawieniu argumentu represent
pola.
rows = db(query).select()
repr_row = rows.render(0)
Jeśli nie zostanie określony indeks, ma się do dyspozycji generator do iterowania po wszystkich wierszach:
for row in rows.render():
print row.myfield
Ma to również zastosowanie do wycinka:
for row in rows[0:10].render():
print row.myfield
Jeśli chce się tylko przekształcić wybrane pola poprzez ich atrybut represent
, można je wykazać w argumencie fields
:
repr_row = row.render(0, fields=[db.mytable.myfield])
Proszę mieć na uwadze, że zwraca to przekształconą kopię oryginalnego obiektu Row, więc nie istnieje metoda update_record
(którą i tak się nie potrzebuje) lub metoda delete_record
.
Skróty DAL
DAL obsługuje różne skróty upraszczające kod. W szczególności:
myrecord = db.mytable[id]
zwraca rekord z określonym id
, jeśli istnieje. Jeśli id
nie istnieje, zwracane jest None
. Powyższe wyrażenie jest ekwiwalentem dla
myrecord = db(db.mytable.id==id).select().first()
Można usuwać rekord odwołując się do id:
del db.mytable[id]
co jest równoważne z
db(db.mytable.id==id).delete()
i usuwa rekord z określonym id
, jeśli istnieje.
Uwaga: ta składnia skrótu delete
nie działa obecnie, jeśli aktywowane jest wersjonowanie .
Można wstawiać rekordy:
db.mytable[0] = dict(myfield='somevalue')
Jest to ekwiwalenetem wyrażenia
db.mytable.insert(myfield='somevalue')
i tworzy nowy rekord z wartościami pól określonymi w słowniku po prawej stronie.
Można aktualizować rekordy:
db.mytable[id] = dict(myfield='somevalue')
co jest ekwiwalentem dla
db(db.mytable.id==id).update(myfield='somevalue')
i aktualizuje istniejący rekord z wartościami pól określonymi w słowniku po prawej stronie.
Pobieranie wiersza
Dogodna jest jeszcze inna składnia:
record = db.mytable(id)
record = db.mytable(db.mytable.id==id)
record = db.mytable(id,myfield='somevalue')
Składnia ta, pozornie podobna do poprzedniej składni db.mytable[id]
, jest bardziej elastyczna i bezpieczniejsza. Przede wszystkim sprawdza, czy id
jest typu int (lub czy str(id)
jest typu int) i gdy nie, zwraca None
(nigdy nie zgłasza wyjątku). Pozwala również określić wiele warunków, które rekord musi spełniać. Jeśli nie są one spełnione, zwraca None
.
Rekursywane polecenia select
Rozważmy poprzednia tabelę "person" i nowa tabelę "thing" odwołująca się do "person":
>>> db.define_table('thing',
Field('name'),
Field('owner_id','reference person'))
Wybierzmy z tej tabeli zestaw rekordów, w ten sposób:
>>> things = db(db.thing).select()
Jest to równoważne poleceniu
>>> things = db(db.thing._id>0).select()
gdzie ._id
odwołuje się do klucza podstawowego tej tabeli. Zwykle db.thing._id
jest takie same jak db.thing.id
i zakładamy to prawie we wszystkich przykładach tej książki.
Dla każdego obiektu Row w zestawie "things" możliwe jest pobranie nie tylko pól z wybranej tabeli ("thing"), ale również z tabel połączonych (rekursywnie):
>>> for thing in things: print thing.name, thing.owner_id.name
Tutaj thing.owner_id.name
wymaga wyboru jednej bazy danych dla każdej rzeczy ("thing") w zestawie rzeczy ("things") i dlatego jest nieefektywne. Sugerujemy zastosowanie złączenia (ang. joins), w miarę możliwości, zamiast rekursywnego wybierania. Jednak jest to wygodne i praktyczne przy dostępie do pojedynczych rekordów.
Można to też zrobić od tyłu, wybierając rzeczy ("thing") do których odwołują się osoby ("person"):
person = db.person(id)
for thing in person.thing.select(orderby=db.thing.name):
print person.name, 'owns', thing.name
W ostatnich wyrażeniach person.thing
jest skrótem dla
db(db.thing.owner_id==person.id)
czyli zestawem rzeczy do którego odwołuje się bieżaca odoba. Ta składnia załamie się, jeśli tabela z odniesieniami ma wiele odniesień. W takim przypadku trzeba być bardziej precyzyjnym i użyć pełnego zapytania.
Serializowanie Rows
w widokach
Weźmy po uwagę następującą akcję zawierająca zapytanie:
def index()
return dict(rows = db(query).select())
Wynik polecenia select
może być wyświetlany w widoku przy zastosowaniu następującej składni:
{{extend 'layout.html'}}
<h1>Records</h1>
{{=rows}}
Co jest równoważne z:
{{extend 'layout.html'}}
<h1>Records</h1>
{{=SQLTABLE(rows)}}
SQLTABLE
konwertuje rows
do tabeli HTML z nagłówkiem zawierającym nazwy kolumn i z jednym wierszem na rekord. Wiersze tabeli są oznaczane przemiennie klasą even
i odd
. Obiekt Rows
jest w tle najpierw konwertowany do obiektu SQLTABLE
(nie mylić z obiektem Table
) i następnie serializowany. Wartości ekstrahowane z bazy danej są też formatowane przez walidatory związane z polem i następnie zabezpieczane znakami unikowymi.
Możliwe jest (a czasem dogodne) bezpośrednie wywołanie SQLTABLE
.
Konstruktor SQLTABLE
pobiera następujące opcjonalne argumenty:
linkto
: funkcja lambda lub akcja, która ma być użyta do zlinkowania pól referencyjnych (domyślnieNone
);
Jeśli argumentowi temu przypisze się łańcuch z nazwą akcji, to zostanie wygenerowany odnośnik do funkcji, przekazujący jako argumenty jej nazwę tabeli i id każdego rekordu (w tej kolejności). Na przykład:
linkto = 'pointed_function' # wygeneruje coś takiego: <a href="pointed_function/table_name/id_value">Jeśli chce się wygenerować inny odnośnik, można określić funkcję lambda, która otrzyma jako parametry wartość id, typ obiektu (np. table) i nazwę obiektu. Na przykład, jeśli chce się otrzymać argumenty w odwrotnej kolejności:
linkto = lambda id, type, name: URL(f='pointed_function', args=[id, name])
upload
: adres URL lub akcja pobierania umożliwiająca pobieranie przesyłanych plików (domyślnie None);headers
: słownik odwzorowujący nazwy pól na ich etykiety, do zastosowania jako nagłówki (domyślnie{}
). Może to być również instrukcja. Obecnie obsługujemyheaders='fieldname:capitalize'
;truncate
: liczba znaków po której nastąpi obcięcie długich wartości w tabeli (domyślnie 16);columns
: lista nazw pól, które mają być wyświetlane jako kolumny (w formacie nazwatabeli.nazwapola). Pola nie wymienione na liście nie są wyświetlane (domyślnie all);**attributes
: ogólne atrybuty helpera do przekazania do najbardziej zewnętrznego obiektu TABLE.
Oto przykład:
{{extend 'layout.html'}}
<h1>Records</h1>
{{=SQLTABLE(rows,
headers='fieldname:capitalize',
truncate=100,
upload=URL('download'))
}}
SQLTABLE
jest przydatny, ale czasami nie wystarcza.SQLFORM.grid
jest rozszerzeniem SQLTABLE, które tworzy tabelę z funkcjonalnością wyszukiwania i stronicowaniem, a także możliwością otwierania szczegółów rekordów, tworzeniem, edycją i usuwaniem rekordów.SQLFORM.smartgrid
jest dalszym uogólnieniem, które pozwala na to wszystko, ale również tworzy przyciski dla dostępu do przywoływanych rekordów.
Oto przykład użycia SQLFORM.grid
:
def index():
return dict(grid=SQLFORM.grid(query))
i odpowiedni widok:
{{extend 'layout.html'}}
{{=grid}}
Przy pracy z wieloma wierszami preferowane są SQLFORM.grid
i SQLFORM.smartgrid
bardziej niż SQLTABLE
, ponieważ są bardziej wydajne. Więcej na ten temat w rozdziale 7.
Argumenty metody select
Metoda select
pobiera dużą ilość opcjonalnych argumentów.
orderby
Można pobierać rekordy sortując je, na przykład, wg name:
>>> for row in db().select(
db.person.ALL, orderby=db.person.name):
print row.name
Alex
Bob
Carl
Można pobrać rekordy sortując je w odwrotnej kolejności (proszę zwrócić uwagę na tyldę):
>>> for row in db().select(
db.person.ALL, orderby=~db.person.name):
print row.name
Carl
Bob
Alex
Można też pobrać rekordy i przesortować je w losowej kolejności:
>>> for row in db().select(
db.person.ALL, orderby='<random>'):
print row.name
Carl
Alex
Bob
Stosowanie
orderby='<random>'
nie jest obsługiwane w Google NoSQL. W takiej sytuacji (a także w wielu innych), gdy wbudowane funkcje są niewystarczające, można wykorzystać instrukcjęimport
:import random rows=db(...).select().sort(lambda row: random.random())
Można sortować rekordy z wykorzystaniem wielu pól, posługując się znakiem "|":
>>> for row in db().select(
db.person.ALL, orderby=db.person.name|db.person.id):
print row.name
Carl
Bob
Alex
groupby, having
Używając groupby
razem z orderby
można grupować rekordy z tymi samymi wartościami w określonych polach (jest to specyficzne dla motoru bazy danych i nie jest obsługiwane przez Google NoSQL):
>>> for row in db().select(
db.person.ALL,
orderby=db.person.name, groupby=db.person.name):
print row.name
Alex
Bob
Carl
Można użyć having
w połączeniu z groupby
do grupowania warunkowego.
>>> print db(query1).select(db.person.ALL, groupby=db.person.name, having=query2)
Proszę zwrócić uwagę, że query1
filtruje rekordy w celu wyświetlenia a query2
filtruje rekordy w celu grupowania.
distinct
Z argumentem distinct=True
można określić, że chce się tylko wybrać odmienne rekordy (grupowanie bez powtórzeń). Ma to ten sam efekt, co grupowanie z użyciem wszystkich wskazanych pól, poza tym, że nie wymaga sortowania. Podczas używania distinct
ważne jest, aby nie wybierać wszystkich pól (ALL
) a w szczególności nie wybierać pola "id", gdyż wówczas wszystkie rekordy będą odmienne (ang. distinct).
Oto przykład:
>>> for row in db().select(db.person.name, distinct=True):
print row.name
Alex
Bob
Carl
Wartością distinct
może być też wyrażenie, na przykład:
>>> for row in db().select(db.person.name,distinct=db.person.name):
print row.name
Alex
Bob
Carl
limitby
Z limitby=(min, max)
, można wybrać podzestaw rekordów z przesunięciem równym min
ale nie przekraczającym max
(w tym przypadku pierwsze dwa rozpoczynając od zera):
>>> for row in db().select(db.person.ALL, limitby=(0, 2)):
print row.name
Alex
Bob
orderby_on_limitby
Należy pamiętać, że DAL domyślnie, niejawnie dodaje orderby
podczas stosowania limitby
. Zapewnia to, że zapytanie zwróci ten sam wynik za każdym razem, co jest ważne przy stronicowaniu. Może to jednak powodować problem z wydajnością. Ten stan rzeczy można poprawić, używając orderby_on_limitby = False
(co jest domyślnie ustawione na True).
left
Zostało to omówione w rozdziale dotyczącym złączeń.
cache, cacheable
Przykładem zastosowania, który daje o wiele szybsze wybieranie jest:
rows = db(query).select(cache=(cache.ram,3600),cacheable=True)
Proszę przeczytać poniżej omówienie 'buforowania poleceń wyboru', aby zrozumieć jakie są tego zalety i wady.
Operatory logiczne
Zapytania mogą być łączone przy użyciu bitowego operatora AND "&":
>>> rows = db((db.person.name=='Alex') & (db.person.id>3)).select()
>>> for row in rows: print row.id, row.name
4 Alex
i bitowego operatora OR "|
":
>>> rows = db((db.person.name=='Alex') | (db.person.id>3)).select()
>>> for row in rows: print row.id, row.name
1 Alex
Można negować zapytanie (lub podzapytanie) stosując bitowy operator "!=":
>>> rows = db((db.person.name!='Alex') | (db.person.id>3)).select()
>>> for row in rows: print row.id, row.name
2 Bob
3 Carl
lub jawnie negować używając operator jednoargumentowy "~":
>>> rows = db(~(db.person.name=='Alex') | (db.person.id>3)).select()
>>> for row in rows: print row.id, row.name
2 Bob
3 Carl
Ze względu na ograniczenia Pythona w przeciążaniu operatorów "and" i "or", nie mogą być one wykorzystywane w tworzeniu zapytań. Zamiast tego trzeba używać bitowych operatorów "&" i "|". Proszę zwrócić uwagę, ze operatory te (w przeciwieństwie do "and" i "or") mają wyższy priorytet niż operatory porównania, tak więc "ekstra" nawiasy w powyższym przykładzie są obowiązkowe. Podobnie operator jednoargumentowy "~" ma wyższy priorytet od operatorów porównania, więc
~
-negacja porównania musi być w nawiasach.
Jest też możliwe budowanie zapytań wykorzystując wraz z operatorami logicznymi skróty przypisania:
>>> query = db.person.name!='Alex'
>>> query &= db.person.id>3
>>> query |= db.person.name=='John'
Metody count
, isempty
, delete
, update
Można zliczać rekordy w zestawie:
>>> print db(db.person.id > 0).count()
3
Proszę zwrócić uwagę, że count
pobiera opcjonalny argument distinct
z domyślną wartością False, który działa bardzo podobnie jak taki sam argument w poleceniu select
. Metoda count
ma też argument cache
, który działa bardzo podobnie do równoważnego argumentu w metodzie select
.
Czasami zachodzi potrzeba sprawdzenia, czy tabela jest pusta. Bardziej efektywnym sposobem jest wykorzystanie metody isempty
:
>>> print db(db.person.id > 0).isempty()
False
lub równoważnie:
>>> print db(db.person).isempty()
False
Można usuwać rekordy z zestawu:
>>> db(db.person.id > 3).delete()
Można również aktualizować wszystkie rekordy w zestawie przekazując nazwane argumenty odpowiadające aktualizowanym polom:
>>> db(db.person.id > 3).update(name='Ken')
Wyrażenia
Wartość przypisana do instrukcji update
może być wyrażeniem. Dla przykładu rozważmy ten model:
>>> db.define_table('person',
Field('name'),
Field('visits', 'integer', default=0))
>>> db(db.person.name == 'Massimo').update(
visits = db.person.visits + 1)
Wyrażeniami mogą też być wartości używane w zapytaniach:
>>> db.define_table('person',
Field('name'),
Field('visits', 'integer', default=0),
Field('clicks', 'integer', default=0))
>>> db(db.person.visits == db.person.clicks + 1).delete()
Klauzula case
case
Wyrażenie może zawierać klauzulę case, na przykład:
>>> db.define_table('person',Field('name'))
>>> condition = db.person.name.startswith('M')
>>> yes_or_no = condition.case('Yes','No')
>>> for row in db().select(db.person.name, yes_or_no):
... print row.person.name, row(yes_or_no)
Max Yes
John No
Metoda update_record
web2py umożliwia również aktualizowanie pojedynczego rekordu, który jest już w pamięci, używając polecenia update_record
>>> row = db(db.person.id==2).select().first()
>>> row.update_record(name='Curt')
Polecenie update_record
nie powinno być mylone z
>>> row.update(name='Curt')
ponieważ dla pojedynczego wiersza, metoda update
aktualizuje obiekt Row a nie rekord bazy danych, jak ma to miejsce w przypadku metody update_record
.
Możliwe jest również zmienienie atrybutów wiersza (za jednym razem) i następnie wywołanie update_record()
bez argumentów, aby zapisać zmiany:
>>> row = db(db.person.id > 2).select().first()
>>> row.name = 'Curt'
>>> row.update_record() # saves above change
Metoda update_record
dostępna jest tylko, jeśli pole id
tabeli jest dołączone do polecenia select a argument cacheable
nie jest ustawiony na True
.
Wstawianie i aktualizowanie ze słownika
Częsty problem jest konieczność wstawiania lub aktualizowania rekordów w tabeli, której nazwa tabeli, pola do aktualizacji i wartość dla tego pola są przechowywane w zmiennych. Na przykład: tablename
, fieldname
i value
.
Wstawianie można zrealizować przy zastosowaniu następującej składni:
db[tablename].insert(**{fieldname:value})
Aktualizację rekordu o określonym id można wykonać z:
db(db[tablename]._id==id).update(**{fieldname:value})
Proszę zauważyć, że użyliśmy table._id
zamiast table.id
. W ten sposób zapytanie działa nawet dla tabel z polem typu "id" o innej nazwie niż "id".
first
i last
Rozpatrzmy obiekt Rows zawierający rekordy:
>>> rows = db(query).select()
>>> first_row = rows.first()
>>> last_row = rows.last()
co jest równoważne z
>>> first_row = rows[0] if len(rows)>0 else None
>>> last_row = rows[-1] if len(rows)>0 else None
as_dict
i as_list
Obiekt Row może być serializowany do zwykłego słownika przy użyciu metody as_dict()
a obiekt Rows może być serializowany do listy słowników przy użyciu metody as_list()
. Oto kilka przykładów:
>>> rows = db(query).select()
>>> rows_list = rows.as_list()
>>> first_row_dict = rows.first().as_dict()
Metody te są wygodne do przekazywania Rows do ogólnych widoków oraz do przechowywania Rows w sesjach (ponieważ same obiekty Rows nie mogą być serializowane , bo zawierają odniesienia do otwartego połączenia DB):
>>> rows = db(query).select()
>>> session.rows = rows # not allowed!
>>> session.rows = rows.as_list() # allowed!
Złączenie wierszy
Obiekty Row mogą być łączone na poziomie Pythona. Przyjmijmy tutaj:
>>> print rows1
person.name
Max
Tim
>>> print rows2
person.name
John
Tim
Można dokonać iloczynu tych rekordów z dwóch zestawów wierszy:
>>> rows3 = rows1 & rows2
>>> print rows3
name
Tim
Można wykonać złączenie union rekordów usuwając powtórzenia:
>>> rows3 = rows1 | rows2
>>> print rows3
name
Max
Tim
John
find
, exclude
, sort
Czasami trzeba wykonać dwa wybory i jedno złączenie podzestawów poprzedniego wyboru. W tym przypadku bezcelowe jest ponowne uzyskiwanie dostępu do bazy danych. Obiekty find
, exclude
i sort
pozwalają manipulować obiektami Rows i wygenerować inny zestaw bez dostępu do bazy danych. Bardziej szczegółowo:
find
zwraca nowy zestaw Rows przefiltrowany zgodnie z warunkiem i zostawia oryginał niezmieniony.exclude
zwraca nowy zestaw Rows przefiltrowany zgodnie z warunkiem i usuwa je z oryginalnego zestawu Rows.sort
zwraca nowy zestaw Rows przesortowany zgodnie z warunkiem i pozostawia oryginał bez zmian.
Wszystkie te metody pobierają pojedynczy argument, funkcje, która działa na każdym poszczególnym wierszu.
Oto przykład użycia:
>>> db.define_table('person',Field('name'))
>>> db.person.insert(name='John')
>>> db.person.insert(name='Max')
>>> db.person.insert(name='Alex')
>>> rows = db(db.person).select()
>>> for row in rows.find(lambda row: row.name[0]=='M'):
print row.name
Max
>>> print len(rows)
3
>>> for row in rows.exclude(lambda row: row.name[0]=='M'):
print row.name
Max
>>> print len(rows)
2
>>> for row in rows.sort(lambda row: row.name):
print row.name
Alex
John
Mogą być one złączone:
>>> rows = db(db.person).select()
>>> rows = rows.find(
lambda row: 'x' in row.name).sort(
lambda row: row.name)
>>> for row in rows:
print row.name
Alex
Max
Metoda sort
pobiera opcjonalny argument reverse=True
o oczywistym znaczeniu.
Metoda find
ma opcjonalny argument limitby.
Inne metody
update_or_insert
Czasem trzeba wykonać wstawienie, tylko gdy w bazie danych nie ma rekordu z tymi samymi wartościami jak te, które chce się wstawić. Można to zrobić tak:
db.define_table('person',Field('name'),Field('birthplace'))
db.person.update_or_insert(name='John',birthplace='Chicago')
Rekord zostanie wstawiony tylko gdy nie ma użytkownika o imieniu John urodzonego w Chicago.
Można określić, które wartości zostaną użyte jako klucz do ustalenia, czy rekord istnieje. Na przykład:
db.person.update_or_insert(db.person.name=='John',
name='John',birthplace='Chicago')
i jeśli istnieje rekord z wartością John, to zostanie zaktualizowane jego miejsce urodzenia, w przeciwnym razie będzie utworzony nowy rekord.
Kryterium wyboru w powyższym przykładzie było pojedyncze pole. Można również użyć zapytanie, takie jak to:
db.person.update_or_insert((db.person.name=='John') & (db.person.birthplace=='Chicago'),
name='John',birthplace='Chicago',pet='Rover')
validate_and_insert
, validate_and_update
Funkcja
ret = db.mytable.validate_and_insert(field='value')
działa badzo podobnie jak:
id = db.mytable.insert(field='value')
oprócz tego, że wywołuje to walidatory dla pól przed wykonaniem wstawienia i kończy działanie, jeśli walidacja się nie powiedzie. Ewentualne błędy walidacji są umieszczane w ret.error
. Jeśli walidacja jest pozytywna, identyfikator nowego wiersza umieszczany jest w ret.id
. Trzeba pamiętać, że walidacja jest wykonywana przez logikę przetwarzania formularza, tak więc ta funkcja jest rzadko potrzebna.
Podobnie
ret = db(query).validate_and_update(field='value')
działa tak samo jak
num = db(query).update(field='value')
oprócz tego, że wywołuje walidatory dla pól przed wykonaniem aktualizacji. Proszę pamiętać, że działa to tylko gdy zapytanie dotyczy pojedynczej tabeli. Liczbę zaktualizowanych rekordów można znaleźć w res.updated
a błędy w ret.errors
.
smart_query
(eksperymentalna)
Zdarza się, że trzeba parsować kwerendę przy użyciu języka naturalnego, taką jak to:
name contain m and age greater than 18
DAL zapewnia metodę na parsowanie tego typu zapytań:
search = 'name contain m and age greater than 18'
rows = db.smart_query([db.person],search).select()
Pierwszy argument musi być listą tabel lub pól, które powinny być dozwolone w wyszukiwaniu. Zgłasza on RuntimeError
, jeśli ciąg wyszukiwania jest nieprawidłowy. Funkcjonalność ta może zostać wykorzystana do budowy interfejsów RESTful (patrz rozdział 10) i jest używany wewnętrznie przez SQLFORM.grid
i SQLFORM.smartgrid
.
W ciągu wyszukiwania smartquery pole może być identyfikowane tylko przez nazwę pola oraz przez nazwa_tabeli.nazwa_pola. Ciągi mogą być ograniczane cudzysłowami, jeśli zawierają spacje.
Pola obliczane
Pola DAL mogą mieć atrybut compute
. Musi on być funkcją (lub lambdą), która pobiera obiekt Row i zwraca wartość pola. Gdy modyfikowany jest nowy rekord, w tym wstawianie lub aktualizacja, gdy nie jest dostarczona wartość dla tego pola, web2py próbuje obliczyć tą wartość na podstawie wartości innych pól, wykorzystując funkcję compute
. Oto przykład:
>>> db.define_table('item',
Field('unit_price','double'),
Field('quantity','integer'),
Field('total_price',
compute=lambda r: r['unit_price']*r['quantity']))
>>> r = db.item.insert(unit_price=1.99, quantity=5)
>>> print r.total_price
9.95
Proszę zauważyć, że obliczana wartość jest przechowywana w db i nie jest obliczana przy pobieraniu, jak w przypadku pól wirtualnych, opisanych dalej. Dwa typowe zastosowania pól wyliczeniowych, to:
- w aplikacji wiki, do przechowywania przetworzonego tekstu wejściowego wiki jako HTML, aby uniknąć ponownego przetworzania go przy każdym żądaniu;
- w wyszukiwaniu, do obliczenia znormalizowanych wartości dla pola, aby mogła być ona wykorzystana w wyszukiwaniu.
Pola obliczane są ewaluowane w kolejności, w jakiej są zdefiniowane w tabeli. Pole pbliczane może odnosić się do poprzednio zdefiniowanych pól obliczanych (nowość w wersji 2.5.1)
Pola wirtualne
Pola wirtualne są też polami obliczanymi (omówionych w poprzednim rozdziale), ale różnią się od nich, ponieważ są wirtualne w tym sensie, że nie są przechowywane w bazie danych i są obliczane za każdym razem, gdy rekordy są ekstrahowane z bazy danych. Mogą być stosowane do uproszczenia kodu użytkownika bez wykorzystywania przechowywania danych w bazie danych, ale nie można ich używać do wyszukiwania.
Pola wirtualne nowego stylu
W web2py zapewniono nowy i łatwy sposób definiowania pól wirtualnych i leniwych pól wirtualnych. Rozdział niniejszy jest oznaczony jako eksperymentalny, ponieważ API tych pól jest ciągle zmieniane i może odbiegać trochę od niniejszego opisu.
Tutaj rozważymy przykład, jak w poprzednim podrozdziale. W szczególności rozpatrzmy następujący model:
>>> db.define_table('item',
Field('unit_price','double'),
Field('quantity','integer'),
Zdefiniujmy wirtualne pole total_price
jako
>>> db.item.total_price = Field.Virtual(
'total_price',
lambda row: row.item.unit_price*row.item.quantity)
czyli po prostu definiujemy pole total_price
aby było Field.Virtual
. Jedynym argumentem konstruktora jest funkcja pobierająca obiekt row i zwracająca wyliczone wartości.
Pole wirtualne zdefiniowane tak jak wyżej jest obliczane automatycznie dla wszystkich rekordów podczas wybierania rekordów:
>>> for row in db(db.item).select(): print row.total_price
Możliwe jest również zdefiniowanie pól Method
, które są obliczane na żądanie, podczas wywoływania. Na przykład:
>>> db.item.discounted_total = Field.Method(lambda row, discount=0.0: row.item.unit_price*row.item.quantity*(1.0-discount/100))
W tym przypadku row.discounted_total
nie jest wartością ale funkcją. Funkcja ta pobiera te same argumenty, jak funkcja przekazywana do konstruktora Method
, z tym wyjątkiem, że row
jest niejawne (trzeba o tym myśleć jak o self
dla obiektów rows).
Leniwe pole w powyższym przykładzie pozwala obliczyć cenę ogólną dla każdego item
:
>>> for row in db(db.item).select(): print row.discounted_total()
Pozwala także na przekazanie opcjonalnego procentowego discount
(15%):
>>> for row in db(db.item).select(): print row.discounted_total(15)
Pola Virtual i Method mogą być zdefiniowane w miejscu, gdzie zdefiniowana jest tabela:
>>> db.define_table('item',
Field('unit_price','double'),
Field('quantity','integer'),
Field.Virtual('total_price', lambda row: ...),
Field.Method('discounted_total', lambda row, discount=0.0: ...))
Należy pamietać, że pola wirtualne nie mają tych samych atrybutów jak inne pola (default, readable, requires itd.). W starszych wersjach web2py nie pojawiają się one na liście
db.table.fields
i wymagają specjalnego podejścia dla wyświetlania w SQLFORM.grid i SQLFORM.smartgrid. Przeglądnij omówienie siatek i wirtualnych pól w rozdziale poświęconym formularzom.
Pola wirtualne starego stylu
W celu zdefiniowania jednego lub więcej wirtualnych pól, można również zdefiniować klasę kontenera, jej instancję i odnieść to do tabeli lub zestawu rekordów. Rozważmy następująca tabelę:
>>> db.define_table('item',
Field('unit_price','double'),
Field('quantity','integer'),
Teraz zdefiniujemy pole wirtualne total_price
:
>>> class MyVirtualFields(object):
def total_price(self):
return self.item.unit_price*self.item.quantity
>>> db.item.virtualfields.append(MyVirtualFields())
Proszę zauważyć, że każda metoda klasy, która pobiera pojedynczy argument self
jest nowym polem wirtualnym. Argument self
odnosi się do każdego wiersza select. Wartości pola określane są przez pełna ścieżkę, tak jak w self.item.unit_price
. Tabela jest powiązana z polami wirtualnymi przez dołączenia instancji klasy do atrybutu virtualfields
tabeli.
Pola wirtualne mogą również uzyskać rekursywny dostęp do pól, tak jak tu:
>>> db.define_table('item',
Field('unit_price','double'))
>>> db.define_table('order_item',
Field('item','reference item'),
Field('quantity','integer'))
>>> class MyVirtualFields(object):
def total_price(self):
return self.order_item.item.unit_price * self.order_item.quantity
>>> db.order_item.virtualfields.append(MyVirtualFields())
Proszę zwrócić uwagę na rekursywny dostęp do pola self.order_item.item.unit_price
gdzie self
jest rekordem pętli.
Działa to też na wyniku złączenia JOIN:
>>> db.define_table('item',
Field('unit_price','double'))
>>> db.define_table('order_item',
Field('item','reference item'),
Field('quantity','integer'))
>>> rows = db(db.order_item.item==db.item.id).select()
>>> class MyVirtualFields(object):
def total_price(self):
return self.item.unit_price * self.order_item.quantity
>>> rows.setvirtualfields(order_item=MyVirtualFields())
>>> for row in rows: print row.order_item.total_price
Składnia w tym przypadku jest inna. Wirtualne pole uzyskuje dostęp zarówno do self.item.unit_price
jak i self.order_item.quantity
które należą do złączenia instrukcji select. Wirtualne pole jest przypisane do wierszy tabeli przy użyciu metody setvirtualfields
obiektu rows. Metoda ta pobiera dowolną liczbę nazwanych argumentów i mogą zostać użyte do ustawienia wielu wirtualnych pól zdefiniowanych w wielu klasach i i dołączonych do wielu tabel:
>>> class MyVirtualFields1(object):
def discounted_unit_price(self):
return self.item.unit_price*0.90
>>> class MyVirtualFields2(object):
def total_price(self):
return self.item.unit_price * self.order_item.quantity
def discounted_total_price(self):
return self.item.discounted_unit_price * self.order_item.quantity
>>> rows.setvirtualfields(
item=MyVirtualFields1(),
order_item=MyVirtualFields2())
>>> for row in rows:
print row.order_item.discounted_total_price
Pola wirtualne mogą być leniwe – wszystko co trzeba zrobić, to zwrócić funkcję i uzyskać do nie dostęp ją wywołując:
>>> db.define_table('item',
Field('unit_price','double'),
Field('quantity','integer'),
>>> class MyVirtualFields(object):
def lazy_total_price(self):
def lazy(self=self):
return self.item.unit_price * self.item.quantity
return lazy
>>> db.item.virtualfields.append(MyVirtualFields())
>>> for item in db(db.item).select():
print item.lazy_total_price()
krócej jest stosując funkcję lambda:
>>> class MyVirtualFields(object):
def lazy_total_price(self):
return lambda self=self: self.item.unit_price * self.item.quantity
Relacje jeden do wielu
W celu zilustrowania tego, jak zaimplementować relacje jeden do wielu w DAL web2py, zdefiniujemy inną tabelę "thing", która odnosi się do tabeli "person", którą tutaj przedefiniujemy:
>>> db.define_table('person',
Field('name'),
format='%(name)s')
>>> db.define_table('thing',
Field('name'),
Field('owner_id', 'reference person'),
format='%(name)s')
Tabela "thing" ma dwa pola, nazwę rzeczy i właściciela rzeczy. Pole "owner_id" jest identyfikatorem odnoszonego pola. Typ referencji (odniesienia) można określić dwoma sposobami:
Field('owner_id', 'reference person')
Field('owner_id', db.person)
Ten ostatni sposób jest zawsze przekształcany do pierwszego. Są one równoważne, z wyjątkiem leniwych tabel, samo odniesień lub innych rodzajów cyklicznych referencji, w których dozwolona jest tylko pierwsza notacja.
Gdy typem pola jest inna tabela, zakłada się, że odniesienie pola do innej tabeli jest realizowane przez identyfikator. Można wydrukować rzeczywista wartość typu i otrzymać:
>>> print db.thing.owner_id.type
reference person
Teraz wstawimy trzy rzeczy, dwie własności Alexa i jedna własności Boba:
>>> db.thing.insert(name='Boat', owner_id=1)
1
>>> db.thing.insert(name='Chair', owner_id=1)
2
>>> db.thing.insert(name='Shoes', owner_id=2)
3
W ten sposób można dokonać wybierania dla innej tabeli:
>>> for row in db(db.thing.owner_id==1).select():
print row.name
Boat
Chair
Ponieważ rzecz jest odniesiona do osoby, osoba może mieć wiele rzeczy, więc rekord z tabeli person teraz nabywa nowy atrybut "thing", który jest zestawem rekordów, który definiuje rzeczy określonej osoby. Pozwala to na wykonanie pętli po wszystkich osobach i pobranie ich rzeczy:
>>> for person in db().select(db.person.ALL):
print person.name
for thing in person.thing.select():
print ' ', thing.name
Alex
Boat
Chair
Bob
Shoes
Carl
Złączenia wewnętrzne
Innym sposobem na osiągnięcie podobnego rezultatu jest wykorzystanie złączenia, w szczególności INNER JOIN
. web2py wykonuje złączenia automatycznie i przejrzyście, gdy łączone są dwie lub więcej tabel, w następujący sposób:
>>> rows = db(db.person.id==db.thing.owner_id).select()
>>> for row in rows:
print row.person.name, 'has', row.thing.name
Alex has Boat
Alex has Chair
Bob has Shoes
Proszę zauważyć, że web2py dokonał złączenia, tak więc rows
zawiera teraz dwa rekordy, jeden dla każdej tabeli, połączonych ze sobą. Ponieważ te dwa rekordy mogą mieć pola z nazwami konfliktującymi, więc należy określić tabelę, z której ma być ekstrahowana wartość pola dla wiersza. Wydaje się, że powyższy kod można by wykonać inaczej:
row.name
lecz jest to niejasne i oczywiście nazwa osoby lub rzeczy w wyniku złączenia powinna być bardziej konkretna, bo teraz może oznaczać:
row.person.name
lub:
row.thing.name
Oto alternatywna składnia INNER JOINS:
>>> rows = db(db.person).select(join=db.thing.on(db.person.id==db.thing.owner_id))
>>> for row in rows:
print row.person.name, 'has', row.thing.name
Alex has Boat
Alex has Chair
Bob has Shoes
Chociaż wyjście jest takie samo, generowany kod SQL jest w obu przypadkach inny. Ostatnia składnia usuwa ewentualne niejasności, gdy ta sama tabela jest złączana podwójnie i aliasowana:
>>> db.define_table('thing',
Field('name'),
Field('owner_id1','reference person'),
Field('owner_id2','reference person'))
>>> rows = db(db.person).select(
join=[db.person.with_alias('owner_id1').on(db.person.id==db.thing.owner_id1).
db.person.with_alias('owner_id2').on(db.person.id==db.thing.owner_id2)])
Wartością join
może być lista db.table.on(...)
do złączenia.
Złączenia zewnętrzne lewe
Proszę zauważyć, że na powyższej liście nie pojawia się Carl, bo nic nie ma. Jeśli zamierza się wybrać wszystkie osoby (bez względu na to, czy coś posiadają, czy nie) wraz z ewentualnymi rzeczami posiadanymi przez te osoby, to trzeba wykonać złączenie zewnetrzne lewe (LEFT OUTER JOIN). Realizuje się to wykorzystując argument left
polecenie select
. Oto przykład:
>>> rows=db().select(
db.person.ALL, db.thing.ALL,
left=db.thing.on(db.person.id==db.thing.owner_id))
>>> for row in rows:
print row.person.name, 'has', row.thing.name
Alex has Boat
Alex has Chair
Bob has Shoes
Carl has None
gdzie:
left = db.thing.on(...)
realizuje zapytanie z lewym złączeniem. Tutaj argument db.thing.on
jest wyrażeniem wymaganym do wykonania złączenia (tak jak wyżej omawianym złączeniu wewnętrznym). W przypadku lewego złączenia, trzeba jawnie określić które pola mają zostać wybrane.
Można łączyć ze soba wiele lewych złączeń przekazując do atrybutu left
listę lub krotkę w wyrażeniu db.mytable.on(...)
.
Grupowanie i zliczanie
Podczas wykonywania złączenia, chce się czasem pogrupować wiersze zgodnie z jakimiś kryteriami i dokonać obliczeń. Na przykład, policzyć liczbę rzeczy posiadanych przez każdą osobę. W web2py umożliwia to bardzo dobrze. Po pierwsze, potrzebujemy operatora zliczającego. Po drugie, chcemy aby złączenie tabeli person
z tabela thing
zostało wykonane wg. owner
. Po trzecie chcemy wybrać wszystkie wiersze (person
+ thing
), grupując je wg. person
i policzyć rzeczy podczas grupowania:
>>> count = db.person.id.count()
>>> for row in db(db.person.id==db.thing.owner_id).select(
db.person.name, count, groupby=db.person.name):
print row.person.name, row[count]
Alex 2
Bob 1
Proszę zauważyć, że operator count
(który jest operatorem wbudowanym) jest stosowany jak pole. Jedynym problemem tutaj jest to, jak uzyskać informacje. Każdy wiersz wyraźnie zawiera osobę (pole person
i liczbę (pole count
), ale ta liczba nie jest polem tabeli person
ani nie jest tabelą. Więc co robić? Chodzi o obiekt w pamięci reprezentujący rekord z kluczem równym wyrażeniu zapytania. Metoda count
obiektu Field
ma opcjonalny argument distinct
. Gdy jest on ustawiony na True
, określa, że należy zliczać tylko różne wartości pola.
Relacje wiele do wielu
W poprzednich przykładach przyjeliśmy, że rzecz może mieć tylko jednego właściciela, ale co zrobić, gdy jest więcej włascicieli jednej rzeczy.Co jeśli łódź należy do Alexa i Curta? Wymaga to relacji wiele-do-wielu i jest realizowane za poprzez pośrednią tabelę, która łączy osobe z rzeczą poprzez realcje ownership
.
Oto jak to zrobić:
>>> db.define_table('person',
Field('name'))
>>> db.define_table('thing',
Field('name'))
>>> db.define_table('ownership',
Field('person', 'reference person'),
Field('thing', 'reference thing'))
Istniejące relacja własnościowe można teraz ująć tak:
>>> db.ownership.insert(person=1, thing=1) # Alex posiada łódź
>>> db.ownership.insert(person=1, thing=2) # Alex posiada krzesło
>>> db.ownership.insert(person=2, thing=3) # Bob posiada buty
Dodajmy relację dla Curta, współwłaściciela łodzi:
>>> db.ownership.insert(person=3, thing=1) # Curt także właścielem łodzi
W ten sposób uzyskaliśmy potrójną realację między tabelami, co może być dogodne dla zdefiniowania nowego zestawu, na którym wykonamy operacje:
>>> persons_and_things = db(
(db.person.id==db.ownership.person) & (db.thing.id==db.ownership.thing))
Możemy teraz ławo pobrać wszystkie osoby i ich rzeczy i zestawić to wszystko w nowym obiekcie Set
:
>>> for row in persons_and_things.select():
print row.person.name, row.thing.name
Alex Boat
Alex Chair
Bob Shoes
Curt Boat
Podobnie, można wyszukać wszystkie rzeczy należące do Alexa:
>>> for row in persons_and_things(db.person.name=='Alex').select():
print row.thing.name
Boat
Chair
oraz wszystkich właścicieli łodzi:
>>> for row in persons_and_things(db.thing.name=='Boat').select():
print row.person.name
Alex
Curt
Lżejszą alternatywą relacji wiele-do-wielu jest tagowanie. Jest ono omówione w kontekście walidatora IS_IN_DB
. Tagowania działa nawet na bazach danych nie obsługujacych złaczeń, takich jak Google App Engine NoSQL.
Pola typu list:<type>
i contains
Platforma web2py udostępnia następne specjalne typy pól:
list:string
list:integer
list:reference <table>
Mogą one zawierać opdpowiednio listy: ciągów znakowych, liczb całkowitych i odniesień.
Na Google App Engine NoSQL typ list:string
jest odwzorowywany na typ StringListProperty
, a dwa pozostałe typy na ListProperty(int)
. Na relacyjnych bazach danych typy te są odwzorowywane na pola tekstowe, które zawierają elementy listy rozdzielane znakiem |
. Na przykład [1,2,3]
jest odwzorowywane na |1|2|3|
.
Na listach ciągów znakowych elementy list są zabezpieczane znakiem uunikowymi, tak więc znak |
w elemencie jest zamieniany na ||
. W każdym razie jest to wewnętrzna interpretacja i jest ona przejrzysta dla użytkownika.
Można użyć list:string
, na przykład, w następujący sposób:
>>> db.define_table('product',
Field('name'),
Field('colors','list:string'))
>>> db.product.colors.requires=IS_IN_SET(('red','blue','green'))
>>> db.product.insert(name='Toy Car',colors=['red','green'])
>>> products = db(db.product.colors.contains('red')).select()
>>> for item in products:
print item.name, item.colors
Toy Car ['red', 'green']
Typ list:integer
działa w ten sam sposób, ale elementami muszą być liczby.
Jak zwykle, wymagania są egzekwowane na poziomie formularza, a nie na poziomie metody insert
.
Dla typu
list:<type>
pola operatoracontains(value)
odwzorowywane są na zapytanie, które sprawdza listy zawierającevalue
. Operatorcontains
działa również dla zwykłych pól typustring
itext
oraz jest odwzorowywane na wyrażenieLIKE '%value%'
.
Typ list:reference
i operator contains(value)
są szczególnie przydatne do de-normalizacji relacji wiele-do-wielu. Oto przykład:
>>> db.define_table('tag',Field('name'),format='%(name)s')
>>> db.define_table('product',
Field('name'),
Field('tags','list:reference tag'))
>>> a = db.tag.insert(name='red')
>>> b = db.tag.insert(name='green')
>>> c = db.tag.insert(name='blue')
>>> db.product.insert(name='Toy Car',tags=[a, b, c])
>>> products = db(db.product.tags.contains(b)).select()
>>> for item in products:
print item.name, item.tags
Toy Car [1, 2, 3]
>>> for item in products:
print item.name, db.product.tags.represent(item.tags)
Toy Car red, green, blue
Proszę zauważyć, że pole typu list:reference tag
pobiera domyślnie ograniczenie
requires = IS_IN_DB(db,'tag.id',db.tag._format,multiple=True)
które wytwarza w formularzu złożone rozwijane pole SELECT/OPTION
.
Warto również pamiętać, że to pole pobiera domyślnie atrybut represent
, który reprezentuje listę odniesień jako rozdzielaną przecinkami listę sformatowanych odniesień. Jest to wykorzystywane w formularzach tylko do odczytu oraz w formularzach SQLTABLE
.
Podczas gdy
list:reference
ma domyślny walidator i domyślną reprezentację,list:integer
ilist:string
nie. Tak więc te dwa typy pól potrzebują walidatoraIS_IN_SET
lubIS_IN_DB
, jeśli chce się je zastosować w formularzach.
Inne operatory
Platforma web2py ma też inne operatory dostarczające API dla dostępu do równoważnych operatorów SQL.
Zdefiniujmy inną tabelę "log" do przechowywania zdarzeń bezpieczeństwa, ich atrybutów event_time
i severity
, gdzie severity
jest liczbą całkowitą.
>>> db.define_table('log', Field('event'),
Field('event_time', 'datetime'),
Field('severity', 'integer'))
Tak jak poprzednio, wstawimy kilka zdarzeń: "skanowanie portu", "iniekcja xss" i "nieautoryzowane logowanie". Ze względu na przykładowy charakter kodu, będziemy rejestrować zdarzenia z tym samym parametrem event_time
ale z różnym statusem (parametr severity
: odpowiednio 1, 2 i 3).
>>> import datetime
>>> now = datetime.datetime.now()
>>> print db.log.insert(
event='skanowanie portu', event_time=now, severity=1)
1
>>> print db.log.insert(
event='iniekcja xss', event_time=now, severity=2)
2
>>> print db.log.insert(
event='nieautoryzowane logowanie', event_time=now, severity=3)
3
like
, ilike
, regexp
, startswith
, endswith
, contains
, upper
, lower
Pola mają operator like
, który można wykorzystać do dopasowania ciągów znakowych:
>>> for row in db(db.log.event.like('port%')).select():
print row.event
port scan
Tutaj "port%
wskazuje ciąg o treści "port". Znak procentu, "%", jest znakiem wieloznacznika, oznaczającym "dowolną sekwencję znaków".
Operator like
odwzorowuje słowo LIKE
z ANSI-SQL. LIKE (pisane dużymi literami) jest obsługiwane przez większość baz danych i zależy od opcji collation
ustawionej w danej bazie danych. Metoda like
pisana jet małymi literami, ale również będzie wykonana, gdy zapiana będzie dużymi literami:
db.mytable.myfield.like('value',case_sensitive=False)
Platforma web2py dostarcza również kilka skrótów:
db.mytable.myfield.startswith('value')
db.mytable.myfield.endswith('value')
db.mytable.myfield.contains('value')
co w przybliżeniu odpowiedno odpowiada:
db.mytable.myfield.like('value%')
db.mytable.myfield.like('%value')
db.mytable.myfield.like('%value%')
Proszę zauważyć, że contains
ma specjalne znaczenie dla pól list:<type>
, co zostało omówione w poprzednim rozdziale.
Metoda contains
może również przekazywać listę wartości i opcjonalnie logiczny argument all
do wyszukiwania rekordów zawierajacych dowolne wartości:
db.mytable.myfield.contains(['value1','value2'], all=True)
lub dowolną wartość z listy
db.mytable.myfield.contains(['value1','value2'], all=False)
Istnieje również metoda regexp
, która działa podobnie jak like
, ale pozwala stosowanie wyrażeń regularnych dla wyrażenia wyszukujacego. Jest to obsługiwane tylko przez bazy PostgreSQL, MySQL, Oracle i SQLite (w różnym stopniu).
Metody upper
i lower
umożliwiają konwersję wartości pola na duże lub małe litery oraz również można łączyć jes w operatorze like
:
>>> for row in db(db.log.event.upper().like('PORT%')).select():
print row.event
port scan
year
, month
, day
, hour
, minutes
, seconds
Pola typu date i datetime posiadają metody day
, month
i year
. Pola typu datetime i time posiadają metody hour
, minutes
i seconds
. oto przykłady:
>>> for row in db(db.log.event_time.year()==2013).select():
print row.event
port scan
xss injection
unauthorized login
belongs
Operator SQL IN
jest realizowany poprzez metodę belongs
, która zwraca true
gdy wartość pola należy do określonego zbioru (listy lub krotki):
>>> for row in db(db.log.severity.belongs((1, 2))).select():
print row.event
port scan
xss injection
DAL pozwala również zagnieżdżać polecenie select
jako argument operatora belongs
. Jedynym ograniczeniem jest to. że zagnieżdżone polecenie wybierające ma mieć nazwę _select
, a nie select
i może być wybrane jawnie tylko jedno pole to, które określa zestaw rekordów.
>>> bad_days = db(db.log.severity==3)._select(db.log.event_time)
>>> for row in db(db.log.event_time.belongs(bad_days)).select():
print row.event
port scan
xss injection
unauthorized login
W tych przypadkach. w których wymagany jest zagnieżdżony wybór a poszukiwane pole jest odniesieniem, można użyć zapytanie jako argument. Na przykład:
db.define_table('person', Field('name'))
db.define_table('thing', Field('name'), Field('owner_id', 'reference thing'))
db(db.thing.owner_id.belongs(db.person.name=='Jonathan')).select()
W tym przypadku jest oczywiste, że następne polecenie select
potrzebuje tylko pola odwołującego się do pola db.thing.owner_id
, więc nie potrzeba już stosować notację _select
.
Zagnieżdżone polecenie select
może być używane jako wartość poleceń insert
lub update
, w takim przypadku składnia jest inna:
lazy = db(db.person.name=='Jonathan').nested_select(db.person.id)
db(db.thing.id==1).update(owner_id = lazy)
Tutaj lazy
jest wyrażeniem obliczającym id
osoby "Jonathan". Dwie linie wyniku w jednym pojedynczym zapytaniu SQL.
sum
, avg
, min
, max
and len
count
do zliczania rekordów. Podobnie, można użyć sum
do dodawania (sumowania) wartości określonego pola z grupy rekordów. Podobnie jak w przypadku count
, wynik sum
jest wywoływany poprzez obiekt store
:>>> sum = db.log.severity.sum()
>>> print db().select(sum).first()[sum]
6
Można również stosować operatory avg
, min
i max
do obliczania odpowiednio wartości średniej, minimalnej i maksymalnej dla wybranych rekordów. Na przykład:
>>> max = db.log.severity.max()
>>> print db().select(max).first()[max]
3
.len()
oblicza długość wartości pól typu string, text lub boolean.
Wyrażenia można łączyć, aby otrzymać bardziej złożone wyrażenia. Na przykład można obliczyć sumę długości wszystkich poważniejszych łańcuchów tekstowych w dziennikach zdarzeń, powiększonych o jeden:
>>> sum = (db.log.severity.len()+1).sum()
>>> print db().select(sum).first()[sum]
Podłańcuchy tekstowe
Można budować wyrażenia odnoszone do podłańcuchów tekstowych. Na przykład można pogrupować rzeczy, których nazwa zaczyna się od tych samych trzech liter i wybrać tylko jedna z każdej takiej grupy:
db(db.thing).select(distinct = db.thing.name[:3])
Domyślne wartości z coalesce
i coalesce_zero
Czasem zachodzi potrzeba wyciagniecia wartości z bazy danych, ale równocześnie potrzeba jakiejś wartości domyślnej, gdy wartość dla rekordu jest ustawiona na NULL
. W SQL do tego służy słowo kluczowe COALESCE
. W web2py istnieje ekwiwalentna metoda coalesce
:
>>> db.define_table('sysuser',Field('username'),Field('fullname'))
>>> db.sysuser.insert(username='max',fullname='Max Power')
>>> db.sysuser.insert(username='tim',fullname=None)
print db(db.sysuser).select(db.sysuser.fullname.coalesce(db.sysuser.username))
"COALESCE(sysuser.fullname,sysuser.username)"
Max Power
tim
Kiedy indziej potrzeba obliczyć jakieś wyrażenie matematyczne , ale pola posiadają wartość ustawiona na None
, podczas gdy powinno być zero. Na ratunek przychodzi coalesce_zero
zamieniając w zapytaniu None na zero:
>>> db.define_table('sysuser',Field('username'),Field('points'))
>>> db.sysuser.insert(username='max',points=10)
>>> db.sysuser.insert(username='tim',points=None)
>>> print db(db.sysuser).select(db.sysuser.points.coalesce_zero().sum())
"SUM(COALESCE(sysuser.points,0))"
10
Generowanie surowego kodu SQL
Może zachodzić potrzeba wygenerowania kodu SQL ale bez jego wykonywania. W web2py jest to łatwe do zrobienia, ponieważ każde polecenie wykonywane na I/O bazy danych ma w web2py odpowiednie metody, które po prostu tylko zwracają kod SQL, nie wykonując go. Metody te mają takie same nazwy i składnię jak ich bazodanowe odpowiedniki, ale ich nazwy rozpoczynają się znakiem podkreślenia.
Przykład dla _insert
>>> print db.person._insert(name='Alex')
INSERT INTO person(name) VALUES ('Alex');
Przykład dla _count
>>> print db(db.person.name=='Alex')._count()
SELECT count(*) FROM person WHERE person.name='Alex';
Przykład dla _select
>>> print db(db.person.name=='Alex')._select()
SELECT person.id, person.name FROM person WHERE person.name='Alex';
Przykład dla _delete
>>> print db(db.person.name=='Alex')._delete()
DELETE FROM person WHERE person.name='Alex';
Przykład dla _update
>>> print db(db.person.name=='Alex')._update()
UPDATE person SET WHERE person.name='Alex';
Ponadto zawsze można użyć
db._lastsql
do otrzymania ostatnio wykonywanego kodu SQL, niezaleznie od tego, czy był on wykonany ręcznie czy też poprzez wygenerowanie go w DAL.
Eksportowanie i importowanie danych
CSV (jedna tabela za jednym razem)
Podczas konwertowania obiektu *Rows* do ciągu znakowego jest on automatycznie serializowany w CSV:
>>> rows = db(db.person.id==db.thing.owner_id).select()
>>> print rows
person.id,person.name,thing.id,thing.name,thing.owner_id
1,Alex,1,Boat,1
1,Alex,2,Chair,1
2,Bob,3,Shoes,2
W CSV można serializować pojedynczą tabelę i przechowywać ją, np. w pliku "test.csv":
>>> open('test.csv', 'wb').write(str(db(db.person.id).select()))
Jest to równoważne z:
>>> rows = db(db.person.id).select()
>>> rows.export_to_csv_file(open('test.csv', 'wb'))
Można z powrotem odczytać plik CSV stosując:
>>> db.person.import_from_csv_file(open('test.csv', 'r'))
Podczas importowania web2py wyszukuje nazw pól w nagłówku CS. W naszym przykładzie, odnajdywane są dwie kolumny: "person.id" i "person.name". Przedrosrk "person." jest ignorowany oraz ignorowane są pola "id". Następnie są dołączane wszystkie rekordy i otrzymuja one nowe identyfikatory. Obie te operacje moga zostać wykonane z poziomu interfejsu administrayjnego aplikacji.
CSV (wszystkie tabele na raz)
W web2py można wykonać kopie zapasową (lub ją przywrócić) z całej bazy danych, przy użyciu dwóch poleceń.
Export wszystkich tabel:
>>> db.export_to_csv_file(open('somefile.csv', 'wb'))
Import wszystkich tabel:
>>> db.import_from_csv_file(open('somefile.csv', 'rb'))
Mechanizm ten może zostać użyty nawet jeśli typ importowanej bazy danych jest inny niż eksportowanej. Dane są przechowywane w pliku "somefile.csv" (nazwa przykładowa) w formacie CSV, gdzie każda tabela rozpoczyna się linią zawierajacą nazwę tabeli i inne linie z nazwami pól:
TABLE tablename
field1, field2, field3, ...
Dwie tabele są rozdzielane sekwencją \r\n\r\n
. Plik kończy się linią
END
Plik nie zawiera plików przesłanych na serwer, jeśli nie są przechowywane w bazie danych. W każdym bądź razie pliki te są łatwe do spakowania w postaci oddzielnego folderu "uploads".
Podczas importowania beda dodawane nowe rekordy, jeśli nie są puste. Ogólnie, nowo importowane rekordy nie mają tych samych identyfikatorów, co rekordy oryginalne (zapisane), ale web2py odtworzy wszystkie odwołania bezawaryjnie, nawet jeśli wartość id może się w międzyczasie zmienić.
Jeśli tabela zawiera pole o nazwie "uuid", pole to zostanie użyte do identyfikacji duplikatów. Ponadto, jeśli importowany rekord ma ta sama wartość "uuid" co rekord istniejący, to poprzedni rekord zostanie zaktualizowany.
CSV a synchronizacja zdalnej bazy danych
Przyjmijmy następujący model:
db = DAL('sqlite:memory:')
db.define_table('person',
Field('name'),
format='%(name)s')
db.define_table('thing',
Field('owner_id', 'reference person'),
Field('name'),
format='%(name)s')
if not db(db.person).count():
id = db.person.insert(name="Massimo")
db.thing.insert(owner_id=id, name="Chair")
Każdy rekord jest identyfikowany przez *ID* i odwołuje się przez *ID*. Jeśli ma się dwie kopie bazy danych używanych w różnych instalacjach web2py, to *ID* jest unikalne tylko dla poszczególnych baz danych a nie w zakresie wszystkich baz. Jest to problem podczas łączenia rekordów z różnych baz danych.
W celu unikatowej identyfikacji rekordów w zakresie wielu baz danych trzeba:
- mieć unikalny identyfikator (UUID),
- mieć pole event_time (aby było wiadomo. który rekord jest najbardziej aktualny),
- odwoływać się poprzez *UUID* zamiast *ID*.
Można to osiągnąć w poniższy sposób.
Zmień nasz model na:
db.define_table('person',
Field('uuid', length=64, default=lambda:str(uuid.uuid4())),
Field('modified_on', 'datetime', default=request.now),
Field('name'),
format='%(name)s')
db.define_table('thing',
Field('uuid', length=64, default=lambda:str(uuid.uuid4())),
Field('modified_on', 'datetime', default=request.now),
Field('owner_id', length=64),
Field('name'),
format='%(name)s')
db.thing.owner_id.requires = IS_IN_DB(db,'person.uuid','%(name)s')
if not db(db.person.id).count():
id = uuid.uuid4()
db.person.insert(name="Massimo", uuid=id)
db.thing.insert(owner_id=id, name="Chair")
Proszę zwrócić uwagę, że w powyższej definicji tabeli domyślna wartością dla obydwu pól
uuid
jest funkcja lambda zwracajaca UUID (przekształcony do ciagu znakowego). Funkcja lambda jest wywoływana tylko raz dla każdego wstawianego rekordu, dzięki czemu każdy rekord otrzymuje unikalny identyfikator UUID, nawet jeśli wstawianych jest wiele rekordów w pojedynczej transakcji.
Utwórz akcje kontrolera eksportującą bazę danych:
def export():
s = StringIO.StringIO()
db.export_to_csv_file(s)
response.headers['Content-Type'] = 'text/csv'
return s.getvalue()
Utwórz kontroler akcji, aby importować zapisaną kopię innej bazy danych i zsynchronizować rekordy:
def import_and_sync():
form = FORM(INPUT(_type='file', _name='data'), INPUT(_type='submit'))
if form.process().accepted:
db.import_from_csv_file(form.vars.data.file,unique=False)
# for every table
for table in db.tables:
# for every uuid, delete all but the latest
items = db(db[table]).select(db[table].id,
db[table].uuid,
orderby=db[table].modified_on,
groupby=db[table].uuid)
for item in items:
db((db[table].uuid==item.uuid)& (db[table].id!=item.id)).delete()
return dict(form=form)
Ewentualnie należy utworzyć indeks ręcznie, aby szybciej wyszukiwać identyfikator UUID.
Alternatywanie można użyć XML-RPC do wyksportowania lub zaimportowania pliku.
Jeśli rekordy odwołują się do plików przesłanych na serwer, trzeba też wyksportować (zaimportować) zawartość folderu *uploads*. Pliki w tym katalogu są już poetykietowane przez identyfikatory UUID, tak więc nie trzeba się martwić o konflikty nazewnicze i odniesienia.
HTML i XML (jedna tabela za jednym razem)
Obiekty Rows (podobnie jak helpery) posiadaja również metodę xml
, która serializuje je do formatu XML/HTML:
>>> rows = db(db.person.id > 0).select()
>>> print rows.xml()
<table>
<thead>
<tr>
<th>person.id</th>
<th>person.name</th>
<th>thing.id</th>
<th>thing.name</th>
<th>thing.owner_id</th>
</tr>
</thead>
<tbody>
<tr class="even">
<td>1</td>
<td>Alex</td>
<td>1</td>
<td>Boat</td>
<td>1</td>
</tr>
...
</tbody>
</table>
Jeśli musi się serializować Rows w innym formacie niż z własnymi znacznikami XML, można to łatwo zrobić używając uniwersalnego helpera TAG i notacji gwiazdkowej (*):
>>> rows = db(db.person.id > 0).select()
>>> print TAG.result(*[TAG.row(*[TAG.field(r[f], _name=f) for f in db.person.fields]) for r in rows])
<result>
<row>
<field name="id">1</field>
<field name="name">Alex</field>
</row>
...
</result>
Reprezentacja danych
Funkcja export_to_csv_file
akceptuje argument kluczowy o nazwie represent
. Gdy ma on wartość True
, to podczas ekspotowania danych zostaną użyta kolumny funkcji represent
zamiast surowych danych.
Funkcja ta akceptuje też argument kluczowy o nazwie colnames
, który powinien zawierać listę nazw kolumn do wyeksportowania. Domyślnie ekspotowane są wszystkie kolumny.
Zarówno export_to_csv_file
jak i import_from_csv_file
akceptują argumenty z kluczami, które ustawiają parser csv na określony format:
delimiter
: seperator rozdzielający wartości (domyślnie ',')quotechar
: znak cytatu używany dla wartości tekstowych (domyślnie podwójny cudzysłów)quoting
: system cytatowy (domyślniecsv.QUOTE_MINIMAL
)
Oto przykład:
>>> import csv
>>> rows = db(query).select()
>>> rows.export_to_csv_file(open('/tmp/test.txt', 'w'),
delimiter='|',
quotechar='"',
quoting=csv.QUOTE_NONNUMERIC)
Co wyrenderuje coś takiego:
"hello"|35|"this is the text description"|"2013-03-03"
Więcej informacji proszę zaczerpnąć z oficjalnej dokumentacji Pythona dla [quoteall]
Wybór pamięci podręcznej
Metod select
pobiera również argument cache
, którego wartością domyślną jest None
. Jeśli chce się włączyć buforowanie, trzeba argument ten ustawić na krotkę, w której pierwszy element jest modelem pamięci podręcznej (cache.ram, cache.disk itd.) a drugi czasem wygasania w sekundach.
W poniższym przykładzie widzimy kontroler, który buforuje wynik select
na poprzednio zdefiniowanej tabeli *db.log table*. W rzeczywistości select
pobiera dane z bazy danych nie częściej niż co 60 sekund i przechowuje wynik w cache.ram. Jeśli następnie wywołamy ten kontroler w czasie krótszym niż 60 sekund od ostatniej operacji I/O na bazie danych, to po prostu dane zostaną pobrane z cache.ram.
def cache_db_select():
logs = db().select(db.log.ALL, cache=(cache.ram, 60))
return dict(logs=logs)
Metoda select
ma też opcjonalny argument cacheable
, zwykle ustawiony na False
. Gdy ustawi się cacheable=True
, spowoduje to, że wynikowy Rows
będzie możliwy do serializacji, ale w obiektach Rows
brak będzie metod update_record
i delete_record
.
Jeśli nie potrzeba tych metod, można przyśpieszyć wybieranie ustawiając atrybut cacheable
:
rows = db(query).select(cacheable=True)
Gdy argument cache
jest ustawiony ale cacheable=False
(domyślnie), to buforowany będzie tylko wynik z bazy danych, nie rzeczywisty obiekt Rows. Gdy natomiast użyty zostanie argument cache
w połączeniu z cacheable=True
, to buforowany będzie cały obiekt Rows, co skutkuje to szybszym buforowaniem:
rows = db(query).select(cache=(cache.ram,3600),cacheable=True)
Samoodnoszenie tabel i aliasy
Możliwe jest zdefiniowanie tabel z polami, które odnoszą się do siebie, oto przykład:
db.define_table('person',
Field('name'),
Field('father_id', 'reference person'),
Field('mother_id', 'reference person'))
Proszę zauważyć, że alternatywna notacja używajaca obiektu table
jako typu pola będzie błędna w tym przypadku, ponieważ używa zmiennej db.person
przed jej zdefiniowaniem:
db.define_table('person',
Field('name'),
Field('father_id', db.person), # wrong!
Field('mother_id', db.person)) # wrong!
Ogólnie, db.tablename
i "reference tablename"
są równoważnymi typami pól, ale ten drugi jest jedynym właściwym dla self.references.
Jeśli tabela odwołuje się do siebie, to nie jest możliwe wykonanie polecenia JOIN
do wyboru osoby i jej rodziców bez użycia słowa kluczowego "AS" SQL. Uzyskuje się to w web2py stosując with_alias
. Oto przykład:
>>> Father = db.person.with_alias('father')
>>> Mother = db.person.with_alias('mother')
>>> db.person.insert(name='Massimo')
1
>>> db.person.insert(name='Claudia')
2
>>> db.person.insert(name='Marco', father_id=1, mother_id=2)
3
>>> rows = db().select(db.person.name, Father.name, Mother.name,
left=(Father.on(Father.id==db.person.father_id),
Mother.on(Mother.id==db.person.mother_id)))
>>> for row in rows:
print row.person.name, row.father.name, row.mother.name
Massimo None None
Claudia None None
Marco Massimo Claudia
Proszę zauważyć, że zdecydowaliśmy się na rozróżnienie między:
- "father_id": nazwa pola używana w tabeli "person";
- "father": alias, który chcemy stosować dla tabeli odwołującej się przez powyższe pole; jest on przekazywany do bazy danych;
- "Father": zmienna używana przez web2py do odwołania się do tego aliasu.
Różnica jest subtelna lecz nie ma nic złego w korzystaniu z tej samej nazwy dla nich trzech:
db.define_table('person',
Field('name'),
Field('father', 'reference person'),
Field('mother', 'reference person'))
>>> father = db.person.with_alias('father')
>>> mother = db.person.with_alias('mother')
>>> db.person.insert(name='Massimo')
1
>>> db.person.insert(name='Claudia')
2
>>> db.person.insert(name='Marco', father=1, mother=2)
3
>>> rows = db().select(db.person.name, father.name, mother.name,
left=(father.on(father.id==db.person.father),
mother.on(mother.id==db.person.mother)))
>>> for row in rows:
print row.person.name, row.father.name, row.mother.name
Massimo None None
Claudia None None
Marco Massimo Claudia
Przy budowaniu poprawnych zapytań ważne jest, aby jasno rozróżniać te elementy.
Zaawansowanie możliwości
Dziedziczenie tabel
Możliwe jest utworzenie tabeli zawierającej wszystkie pola z innej tabeli. Wystarczy przekazać do define_table
inną tabelę zamiast pola. Na przykład:
db.define_table('person', Field('name'))
db.define_table('doctor', db.person, Field('specialization'))
Możliwe jest również zdefiniowanie imitacji tabeli, która nie jest przechowywana w bazie danych, w celu wielokrotnego jej wykorzystania w różnych miejscach. Na przykład:
signature = db.Table(db, 'signature',
Field('created_on', 'datetime', default=request.now),
Field('created_by', db.auth_user, default=auth.user_id),
Field('updated_on', 'datetime', update=request.now),
Field('updated_by', db.auth_user, update=auth.user_id))
db.define_table('payment', Field('amount', 'double'), signature)
W tym przykładzie założono, że w web2py włączone jest standardowe uwierzytelnianie.
Teraz jeśli użyje się Auth
web2py utworzy jedną taką tabelę:
auth = Auth(db)
db.define_table('payment', Field('amount', 'double'), auth.signature)
Oczywiście, tabela nadrzędna musi mieć zdefiniowane walidatory.
Filtry filter_in
i filter_out
Możliwe jest zdefiniowanie filtra dla każdego pola po to, aby wywoływać ten filtr przed wstawianiem wartości do pola, czy po pobraniu wartości z określonego pola.
Proszę sobie wyobrazić, że chcemy przechowywać możliwą do serializacji strukturę danych Pythona w polu o formacie json. Oto jak to można zrobić:
>>> from simplejson import loads, dumps
>>> db.define_table('anyobj',Field('name'),Field('data','text'))
>>> db.anyobj.data.filter_in = lambda obj, dumps=dumps: dumps(obj)
>>> db.anyobj.data.filter_out = lambda txt, loads=loads: loads(txt)
>>> myobj = ['hello', 'world', 1, {2: 3}]
>>> id = db.anyobj.insert(name='myobjname', data=myobj)
>>> row = db.anyobj(id)
>>> row.data
['hello', 'world', 1, {2: 3}]
Osiągnąć to można też w inny sposób stosując pole typu SQLCustomType
, co zostanie omówione dalej.
Wywołania zwrotne przy wstawianiu, usuwaniu i aktualizowaniu rekordów
W web2py dostarczany jest mechanizm do rejestrowania wywołań zwrotnych przed albo po wstawieniu, aktualizacji i usunięciu rekordów.
Każda tabela przechowuje sześć list wywołań zwrotnych:
db.mytable._before_insert
db.mytable._after_insert
db.mytable._before_update
db.mytable._after_update
db.mytable._before_delete
db.mytable._after_delete
Można zarejestrować funkcje wywołania zwrotnego przez dodanie jej do jednej z tych list. Wywołanie zwrotne ma sygnaturę uzależnioną od funkcjonalności.
Najlepiej wyjaśnić to na przykładach.
>>> db.define_table('person',Field('name'))
>>> def pprint(*args): print args
>>> db.person._before_insert.append(lambda f: pprint(f))
>>> db.person._after_insert.append(lambda f,id: pprint(f,id))
>>> db.person._before_update.append(lambda s,f: pprint(s,f))
>>> db.person._after_update.append(lambda s,f: pprint(s,f))
>>> db.person._before_delete.append(lambda s: pprint(s))
>>> db.person._after_delete.append(lambda s: pprint(s))
Tutaj f
jest słownikiem pól przekazywanych do metod wstawiających lub aktualizujących, id
jest identyfikatorem nowo wstawianego rekordu, s
jest obiektem *Set* wykorzystywanym dla aktualizacji lub usuwania.
>>> db.person.insert(name='John')
({'name': 'John'},)
({'name': 'John'}, 1)
>>> db(db.person.id==1).update(name='Tim')
(<Set (person.id = 1)>, {'name': 'Tim'})
(<Set (person.id = 1)>, {'name': 'Tim'})
>>> db(db.person.id==1).delete()
(<Set (person.id = 1)>,)
(<Set (person.id = 1)>,)
Zwracanymi wartościami tego wywołania zwrotnego mogą być None
albo False
. Jeżeli jakiekolwiek wywołanie zwotne _before_*
zwraca wartość True
będzie przerywać operacje wstawiania, aktualizacji i usuwania.
Czasami wywołanie zwrotne może wykonywać aktualizację w na tej samej lub innej tabeli i chce się uniknąć wywołania zwrotnego uruchamiającego samego siebie rekurencyjnie. W tym celu obiekty *Set* mają metodę update_naive
, która działa podobnie do update
, ale ignoruje wywołania zwrotne _after_* i _before_*.
Usuwanie kaskadowe rekordów
W schemacie bazy danych można zdefiniować ralacje, które wyzwalają usuwanie powiązanych rekordów, zwane usuwaniem kaskadowym. DAL nie jest informowany o usunięciu rekordów w kaskadzie. Więc wyzwalacz on_delete
nie będzie wywoływał spodziewanego usuwania kasadowego.
Wersjonowanie rekordów
Jest możliwe, aby web2py pytał, czy ma zapisać każdą kopię rekordu, gdy rekord ten jest zmieniany indywidualnie. Jest na to kilka sposobów, lecz można to wykonać też dla wszystkicj tabej jednocześnie, używając taką składnię:
auth.enable_record_versioning(db)
Wymaga to uwierzytelnienia i jest omówione w rozdziale o uwierzytelnianiu. Można to również zrealizowć indywidualnie dla każdej tabeli, tak jak opisano poniżej.
Przyjmijmy, że may następująca tabelę:
db.define_table('stored_item',
Field('name'),
Field('quantity','integer'),
Field('is_active','boolean',
writable=False,readable=False,default=True))
Proszę zwrócić uwagę na ukryte pole logiczne o nazwie is_active
i na to, że jego domyślna wartość, to True
.
Możemy poinstruować web2py, aby utworzył nową tabelę (w tej samej lub innej bazie danych) i przechowywał tam wszystkie poprzednie wersje każdego rekordu w tabeli, podczas modyfikacji.
Robi się to tak:
db.stored_item._enable_record_versioning()
lub z wykorzystaniem bardziej szczegółowej składni:
db.stored_item._enable_record_versioning(
archive_db = db,
archive_name = 'stored_item_archive',
current_record = 'current_record',
is_active = 'is_active')
Klucz archive_db=db
powiadamia web2py, aby przechowywał tabelę archiwalną w tej samej bazie danych w tabeli o nazwie stored_item
. Klucz archive_name
ustawia nazwę dla tabeli archiwalnej. Tabela archiwalna ma te same pola co oryginalna tabela stored_item
z tym, że unikalne pola nie są już unikalne (ponieważ zachodzi potrzeba przechowywania wielu wersji) i ma dodatkowe pole, którego nazwa jest określona przez current_record
i które jest odniesione do ciezącego rekordu w tabeli stored_item
.
Gdy rekordy są usuwane, to tak naprawdę nie są usuwane. Usuwany rekord jest kopiowany do tabeli stored_item_archive
(podobnie jak przy modyfikacji) a pole is_active
zostaje ustawione na False. Poprzez włączenie wersjonowania rekordów web2py ustawia na tej tabeli filtr custom_filter
, który ukrywa wszystkie rekordy w tabeli stored_item
, których pole is_active
jest ustawione na False. Parametr is_active
w metodzie _enable_record_versioning
pozwala określić nazwę pola używanego przez filtr custom_filter
do określenia, czy pole zostało usunięte czy nie.
Filtry custom_filter
są ignorowane przez interfejs administracyjny.
Wspólne pola i obsługa wielu podmiotów
Obsługa wielu podmiotów (ang. multi-tenancy) to architektura, w której pojedyncza instancja aplikacji obsługuje wielu klientów. Każdy klient jest nazywany najemcą (*ang. tenant*). Najemcy mogą mieć możliwość dostosowania niektórych części aplikacji, takich jak kolor interfejsu użytkownika lub reguł biznesowych, ale nie mogą dostosować kodu aplikacji. Mogą oni też równocześnie używać tej samej instancji bazy danych. Jest to architektura wykorzystywana w szczególności w chmurach obliczeniowych. (*przyp. tłumacza*).
db._common_fields
jest listą pól, które powinny należeć do wszystkich tabel. Lista ta może również zawierać tabele i jest rozumiana jako wszystkie pola tabeli. Na przykład, czasem zachodzi potrzeba dodania sygnatury do wszystkich tabel, ale tabel auth
. W takim przypadku, po db.define_tables()
, ale przed definicją każdej innej tabeli, wstaw:
db._common_fields.append(auth.signature)
Jedno pole jest wyjątkowe: "request_tenant". To pole nie istnieje, ale można utworzyć i dodać do jakiejś tabeli (lub wsztstkich):
db._common_fields.append(Field('request_tenant',
default=request.env.http_host,writable=False))
Dla każdej tabeli z polem db._request_tenant
, wszystkie rekordy, dla wszystkich zapytań są zawsze automatycznie filtrowane przez:
db.table.request_tenant == db.table.request_tenant.default
a dla każdego wstawianego rekordu, pole to jest ustawiane na wartość domyślną. W powyższym przykładzie wybraliśmy
default = request.env.http_host
czyli wybraliśmy pytanie do naszej aplikacji, aby flitrowała wszystkie tabele we wszystkich tabelach we wszystkich zapytaniach przez
db.table.request_tenant == request.env.http_host
Ten prosty trik umożliwia włączyć każdą aplikację w architekturę obsługi wielu podmiotów (*ang. multi-tenant application*), czyli umożliwić używanie pojedynczej bazy danych, w tej samej instacji aplikacji przez dwie lub więcej domen (w naszym przykładzie nazwę domeny pobieramy z request.env.http_host
), tak aby odwiedzający widzieli inne dane. Pomyśli o uruchamianiu wielu sklepów internetowych z jedenej aplikacji i jednej bazie danych.
Filtr obsługi wielu podmiotów można wyłączyć stosując:
rows = db(query, ignore_common_filters=True).select()
Wspólne filtry
Wspólny filtr (*ang. common filter*) jest uogólnieniem wyżej omawianej idei obsługi wielu podmiotów. Pozwala to w łatwy sposób zapobiegać powtarzaniu tego samego zapytania. Rozważmy dla przykładu poniższą tabelę:
db.define_table('blog_post',
Field('subject'),
Field('post_text', 'text'),
Field('is_public', 'boolean'),
common_filter = lambda query: db.blog_post.is_public==True
)
Każdy wybór, usunięcie lub aktualizacja tej tabeli będzie dołaczać tylko publiczny wpis bloga. Ten atrybut można również zmienić w kontrolerach:
db.blog_post._common_filter = lambda query: db.blog_post.is_public == True
Zapobiega to przed zapominaniem ustawiania zakazu przeglądania nie publicznych wpisów i jest zarówno sposobem na unikanie powtarzania wyrażenia db.blog_post.is_public==True
w każdym przeszukiwaniu wpisów bloga, jaki i zwiększa bezpieczeństwo.
W przypadku, gdy chce się w jakiejś systuacji wyłączyć wspólny filtr (na przykład, aby umożliwić administratorowi zobaczenie wpisów nie publicznych), można usunąć ten filtr:
db.blog_post._common_filter = None
lub go zignorować:
db(query, ignore_common_filters=True).select(...)
Własne typy Field
(eksperymentalne)
Oprócz stosowania filter_in
i filter_out
, możliwe jest zdefiniowanie nowych (własnych) typów pól. Rozważmy tutaj pole, które zawiera dane binarne w kompresowanej formie:
from gluon.dal import SQLCustomType
import zlib
compressed = SQLCustomType(
type ='text',
native='text',
encoder =(lambda x: zlib.compress(x or '')),
decoder = (lambda x: zlib.decompress(x))
)
db.define_table('example', Field('data',type=compressed))
SQLCustomType
wytwarza niestandadowy typ pola. Argument type
musi być jednym ze standardowych typów pól web2py. Instruuje to web2py, jak ma traktować definiowany typ pola na poziomie web2py. Argument native
jest typem pola warunkowanym przez bazę danych - dozwolone nazwy zależą od silnika bazy danych. Argument encoder
jest opcjonalna funkcją stosowaną podczas zapisywania danych a decoder
jest opcjonalnym odwróceniem funkcji transformującej.
Funkcjonalność ta jest eksperymentalna. Jest ona praktycznie stosowana w web2py przez dłuższy czas z wynikiem pozytywnym, ale może spowodować nieprzenośność kodu, na przykład, gdy natywny typ jest specyficzny dla bazy danych. Nie działa w Google App Engine NoSQL.
Używanie DAL bez definiowania tabel
DAL można używać z dowolnym programie Python po prostu w ten sposób:
from gluon import DAL, Field
db = DAL('sqlite://storage.sqlite',folder='path/to/app/databases')
czyli przez zaimportowanie DAL, Field oraz określenie połączenia i folderu zawierającego pliki .table (folder app/databases).
Dla uzyskania dostępu do danych i ich atrybutów, nadal mamy możliwość definiowania wszystkich tabel dostępnych w db.define_tables(...)
.
Jeśli potrzebujemy tylko uzyskać dostęp do danych, ale nie do atrybutów tabel web2py, możemy obyć się bez ponownego definiowania tabel, ale po prostu pytając web2py, aby odczytał niezbędne informacje z metadanych w plikach .table
:
from gluon import DAL, Field
db = DAL('sqlite://storage.sqlite',folder='path/to/app/databases',
auto_import=True))
Pozwala to na dostęp do każdego obiektu db.table
bez ponownego definiowania.
PostGIS, SpatiaLite i MS Geo (eksperymentalnie)
DAL obsługuje geograficzne interfejsy API, przy użyciu PostGIS (dla PostgreSQL), spatialite (dla SQLite) i MSSQL oraz Spatial Extensions. Jest to funkcjonalność sponsorowana przez projekt Sahana i została zaimpelementowana przez Denesa Lengyela.
DAL zapewnia geometryczne i geograficzne typy pól oraz następujące funkcje:
st_asgeojson (tylko PostGIS)
st_astext
st_contains
st_distance
st_equals
st_intersects
st_overlaps
st_simplify (tylko PostGIS)
st_touches
st_within
st_x
st_y
Oto kilka przykładów:
from gluon.dal import DAL, Field, geoPoint, geoLine, geoPolygon
db = DAL("mssql://user:pass@host:db")
sp = db.define_table('spatial', Field('loc','geometry()'))
Poniżej wstawiamy punkt, linię i wielokat:
sp.insert(loc=geoPoint(1,1))
sp.insert(loc=geoLine((100,100),(20,180),(180,180)))
sp.insert(loc=geoPolygon((0,0),(150,0),(150,150),(0,150),(0,0)))
Proszę zwrócić uwagę na:
rows = db(sp.id>0).select()
Zawsze zwraca dane geometryczne serializowane jako tekst. Można zrobić to samo bardziej czytelnie, używając st_astext()
:
print db(sp.id>0).select(sp.id, sp.loc.st_astext())
spatial.id,spatial.loc.STAsText()
1, "POINT (1 2)"
2, "LINESTRING (100 100, 20 180, 180 180)"
3, "POLYGON ((0 0, 150 0, 150 150, 0 150, 0 0))"
można poprosić o natywną reprezentację przez użycie st_asgeojson()
(tylko w PostGIS):
print db(sp.id>0).select(sp.id, sp.loc.st_asgeojson().with_alias('loc'))
spatial.id,loc
1, [1, 2]
2, [[100, 100], [20 180], [180, 180]]
3, [[[0, 0], [150, 0], [150, 150], [0, 150], [0, 0]]]
Proszę zauważyć, że tablica jest punktem, tablica tablic jest linią a tablica z tablicami tablic jest wielokątem.
Oto przykład użycia funkcji geograficznych:
query = sp.loc.st_intersects(geoLine((20,120),(60,160)))
query = sp.loc.st_overlaps(geoPolygon((1,1),(11,1),(11,11),(11,1),(1,1)))
query = sp.loc.st_contains(geoPoint(1,1))
print db(query).select(sp.id,sp.loc)
spatial.id,spatial.loc
3,"POLYGON ((0 0, 150 0, 150 150, 0 150, 0 0))"
Obliczone odległości można również pobrać w postaci liczb zmiennoprzecinkowych:
dist = sp.loc.st_distance(geoPoint(-1,2)).with_alias('dist')
print db(sp.id>0).select(sp.id, dist)
spatial.id, dist
1 2.0
2 140.714249456
3 1.0
Kopiowanie danych z jeden bazy do drugiej
Rozważmy sytuację, w której używamy następującą bazę danych:
db = DAL('sqlite://storage.sqlite')
i potrzebujemy przenieść dane do innej bazy, dla której stosowany jest ciąg połączenia:
db = DAL('postgres://username:password@localhost/mydb')
Przed przełączeniem musimy przenieść dane i przebudować wszystkie metadane dla nowej bazy danych. Przyjmujemy, że nowa baza danych istnieje i że jest pusta.
Web2py zawiera skrypt, który realizuje to zadanie:
cd web2py
python scripts/cpdb.py \
-f applications/app/databases \
-y 'sqlite://storage.sqlite' \
-Y 'postgres://username:password@localhost/mydb' \
-d ../gluon
Po uruchomieniu skryptu można przełączyć ciąg połączenia w modelu i wszystko powinno działać i powinny tam też być nowe dane.
Skrypt ten dostarcza szereg opcji linii poleceń umożliwiających przenieść dane z jedenej aplikacji do drugiej, przenieść wszystkie tabele lub tylko niektóre, wyczyścić dane w tabelach. Więcej informacji można uzyskać uruchamiając:
python scripts/cpdb.py -h
Uwagi o nowym DAL i adapterach
Kod źródłowy warstwy abstrcji bazy danych (Database Abstraction Layer - DAL) został całkowicie przepisany w 2010 roku. Mimo to pozostaje kompatybilny wstecz. Jest bardziej modularny i ławtwiejszy do rozszerzania. Wyjaśnimy tutaj główną logikę DAL.
Plik "gluon/dal.py" definiuje, między innymi, następujace klasy:
ConnectionPool
BaseAdapter rozszerzającą ConnectionPool
Row
DAL
Reference
Table
Expression
Field
Query
Set
Rows
Z wyjątkiem BaseAdapter
, działanie tych klas zostało wyjaśnione w poprzednich rozdziałach. Gdy metody obiektów Table
lub Set
muszą komunikować się z bazą danych, powierzają to metodom adaptera, którego zadaniem jest wygenerowanie kodu SQL i ewentualnie wywołanie funkcji.
Na przykład:
db.mytable.insert(myfield='myvalue')
wywołuje
Table.insert(myfield='myvalue')
która deleguje adapter uruchamiając:
db._adapter.insert(db.mytable,db.mytable._listify(dict(myfield='myvalue')))
Tutaj db.mytable._listify
przekształca słownik argumentów do listy (field,value)
i wywołuje metodę insert
obiektu adapter
. db._adapter
robi mniej więcej rzecz następującą:
query = db._adapter._insert(db.mytable,list_of_fields)
db._adapter.execute(query)
gdzie pierwsza linia buduje zapytanie a druga je wykonuje.
BaseAdapter
definuje interfejs dla wszystkich adapterów.
Plik "gluon/dal.py" w momencie pisania tej książki zawierał następujące adaptery:
SQLiteAdapter rozszerzające BaseAdapter
JDBCSQLiteAdapter rozszerzające SQLiteAdapter
MySQLAdapter rozszerzające BaseAdapter
PostgreSQLAdapter rozszerzające BaseAdapter
JDBCPostgreSQLAdapter rozszerzające PostgreSQLAdapter
OracleAdapter rozszerzające BaseAdapter
MSSQLAdapter rozszerzające BaseAdapter
MSSQL2Adapter rozszerzające MSSQLAdapter
MSSQL3Adapter rozszerzające MSSQLAdapter
MSSQL4Adapter rozszerzające MSSQLAdapter
FireBirdAdapter rozszerzające BaseAdapter
FireBirdEmbeddedAdapter rozszerzające FireBirdAdapter
InformixAdapter rozszerzające BaseAdapter
DB2Adapter rozszerzające BaseAdapter
IngresAdapter rozszerzające BaseAdapter
IngresUnicodeAdapter rozszerzające IngresAdapter
GoogleSQLAdapter rozszerzające MySQLAdapter
NoSQLAdapter rozszerzające BaseAdapter
GoogleDatastoreAdapter rozszerzające NoSQLAdapter
CubridAdapter rozszerzające MySQLAdapter (eksperymentalne)
TeradataAdapter rozszerzające DB2Adapter (eksperymentalne)
SAPDBAdapter rozszerzające BaseAdapter (eksperymentalne)
CouchDBAdapter rozszerzające NoSQLAdapter (eksperymentalne)
IMAPAdapter rozszerzające NoSQLAdapter (eksperymentalne)
MongoDBAdapter rozszerzające NoSQLAdapter (eksperymentalne)
VerticaAdapter rozszerzające MSSQLAdapter (eksperymentalne)
SybaseAdapter rozszerzające MSSQLAdapter (eksperymentalne)
które nadpisują zachowanie BaseAdapter
.
Każdy adapter ma mniej więcej taką strukturę:
class MySQLAdapter(BaseAdapter):
# określenie sterownika
driver = globals().get('pymysql',None)
# odwzorowanie typów web2py na typy bazydanych
types = {
'boolean': 'CHAR(1)',
'string': 'VARCHAR(%(length)s)',
'text': 'LONGTEXT',
...
}
# połączenie z bazą danych przy użyciu sterownika
def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8',
credential_decoder=lambda x:x, driver_args={},
adapter_args={}):
# parsowanie łańcucha uri i zapisanie parametrów w driver_args
...
# zdefiniowanie funkcji połączenia
def connect(driver_args=driver_args):
return self.driver.connect(**driver_args)
# umiwszczenie jej w ouli
self.pool_connection(connect)
# ustawienie opcjonalnych parametrów (po połączeniu)
self.execute('SET FOREIGN_KEY_CHECKS=1;')
self.execute("SET sql_mode='NO_BACKSLASH_ESCAPES';")
# nadpisanie metod BaseAdapter, o ile potrzeba
def lastrowid(self,table):
self.execute('select last_insert_id();')
return int(self.cursor.fetchone()[0])
Wykorzystując różne adaptery jako przykład, napisanie własnego powinno być łatwe.
Podczas tworzenia instancji db
:
db = DAL('mysql://...')
przedrostek w łańcuchu uri określa adapter. Odwzorowanie jest zdefinowane również w "gluon/dal.py" w następującym słowniku:
ADAPTERS = {
'sqlite': SQLiteAdapter,
'spatialite': SpatiaLiteAdapter,
'sqlite:memory': SQLiteAdapter,
'spatialite:memory': SpatiaLiteAdapter,
'mysql': MySQLAdapter,
'postgres': PostgreSQLAdapter,
'postgres:psycopg2': PostgreSQLAdapter,
'postgres:pg8000': PostgreSQLAdapter,
'postgres2:psycopg2': NewPostgreSQLAdapter,
'postgres2:pg8000': NewPostgreSQLAdapter,
'oracle': OracleAdapter,
'mssql': MSSQLAdapter,
'mssql2': MSSQL2Adapter,
'mssql3': MSSQL3Adapter,
'mssql4' : MSSQL4Adapter,
'vertica': VerticaAdapter,
'sybase': SybaseAdapter,
'db2': DB2Adapter,
'teradata': TeradataAdapter,
'informix': InformixAdapter,
'informix-se': InformixSEAdapter,
'firebird': FireBirdAdapter,
'firebird_embedded': FireBirdAdapter,
'ingres': IngresAdapter,
'ingresu': IngresUnicodeAdapter,
'sapdb': SAPDBAdapter,
'cubrid': CubridAdapter,
'jdbc:sqlite': JDBCSQLiteAdapter,
'jdbc:sqlite:memory': JDBCSQLiteAdapter,
'jdbc:postgres': JDBCPostgreSQLAdapter,
'gae': GoogleDatastoreAdapter, # discouraged, for backward compatibility
'google:datastore': GoogleDatastoreAdapter,
'google:datastore+ndb': GoogleDatastoreAdapter,
'google:sql': GoogleSQLAdapter,
'couchdb': CouchDBAdapter,
'mongodb': MongoDBAdapter,
'imap': IMAPAdapter
}
łańcuch uri jest następnie parsowany na więcej szczegółów przez sam adapter.
Dla każdego adaptera można zamienić sterownik na inny przez:
import MySQLdb as mysqldb
from gluon.dal import MySQLAdapter
MySQLAdapter.driver = mysqldb
czyli mysqldb
musi być tym modułem z metody .connect()
. Opcjonalnie można określić argumenty sterownika i argumentu adaptera:
db =DAL(..., driver_args={}, adapter_args={})
Pułapki
SQLite
SQLite nie obsługuje usuwania i zmieniania kolumn. Oznacza to, że migracje web2py będa działy tylko do pewnego stopnia. Jeśli usunie się pole z tabeli, kolumna pozostanie w bazie danych, ale będzie niewidoczna dla web2py. Jeśli zdecydujesz się na przywrócenie kolumny, web2py będzie próbował ją odtworzyć, lecz bez powodzenia. W takim przypadku trzeba ustawić fake_migrate=True
, tak że metadane są odbudowywane bez próby ponownego dodania kolumny. Ponadto, z tego samego powodu, SQLite nie ma informacji o jakichkolwiek zmianach typu kolumny. Jeśli się wstawi liczbę w pole tekstowe, to będzie ona przechowywana jako łańcuch. Jeśli później zmieni się model i dokonana zostanie zamiana typu "string" na "integer", SQLite będzie nadal przechowywał liczbę jako tekst i może to spowodować problemy przy próbie pobrania danych.
SQLite nie ma typu logicznego. DAL web2py wewnętrznie odwzorowuje wszystkie wartości logiczne jako jeden znak tekstowy 'T' albo 'F', odpowiednio dla wartości True
i False
. DAL radzi sobie z tym całkowicie - abstrakcja logicznej prawdy działa dobrze. Jeśli jednak aktualizuje się bezposrednio tabelę SQLite w SQL, należy pamiętać o implementacji web2py i unikać wartości liczbowych 0 i 1, jako odpowiedników wartości logicznych.
MySQL
MySQL nie obsługuje w pojedynczej transakcji więcej niż jedno polecenia ALTER
TABLE
. Oznacza to, że proces migracji trzeba podzielić na kilka zatwierdzeń. Jeśli coś się stanie i proces migracji zostanie załamany, metadane web2py nie zostaną zsynchronizowane ze strukturą rzeczywistych tabel w bazie danych. Jest to niefortunne, ale można temu zapobiec, migrując tylko jedną tabelę na raz. Można to też naprawić po fakcie, odwracając model web2py do tego co odpowiada strukturze tabeli w bazie danych, przez ustawienie fake_migrate=True
i po przebudowaniu metadanych ustawieniu fake_migrate=False
i następnie powtórzając migrację.
Google SQL
Z Google SQL są te same problemy co z MySQL ale jest jeszcze ich więcej. W szczególności, same metadane tabeli muszą być przechowywane w bazie danych w tabeli, która nie jest objęta migracją przez web2py. Jest tak dlatego, że Google App Engine ma system plików tylko do odczytu. Migraje web2py w Google:SQL, w połączeniu z powyżej opisanym problemem MySQL mogą spowodować uszkodzenie metadanych. Tak jak poprzednio, można temu zapobiegać, migrując tabele pijedynczo i ustawiając migrate=False
, tak aby tabela metadanych nie była więcej dostępna albo naprawiając problem po fakcie, przez uzyskanie dostępu do bazy danych z kokpitu Google i usunięcie wszystkich uszkodzonych wpisów z tabeli o nazwie web2py_filesystem
.
MSSQL (Microsoft SQL Server)
MSSQL < 2012 nie obsługuje słowa kluczowego SQL OFFSET. Dlatego baza danych nie może być stronicowana. podczas wykonywania limitby=(a,b)
web2py będzie pobierał pierwsze wiersze b
i usuwał pierwsze a
. Może to doprowadzić do znacznego wzrostu narzutu w porównaniu z innymi silnikami baz danych. Jeśłi używa się MSSQL >= 2005, zalecanym adapterem jest mssql3://
, który posiada sposób unikniecia tego problemu, pobierając całe, niepostronicowane zestawy wyników. Jeśli korzysta się z MSSQL >= 2012, należy używać adaptera mssql4://
, który obsługuje konstrukcje OFFSET ... ROWS ... FETCH NEXT ... ROWS ONLY
do wykonywania natywnego stronicowania bez wzrostu wydajności, podobnie jak inne bazy danych. Adapter mssql://
wymusza również (ze względów historycznych) używanie kolumn text
, które są zastąþione w nowszych bazach danych (od wersji 2005) przez varchar(max)
. Powinno się używać adapterów mssql3://
i mssql4://
, jeśli nie chce się natknąć na pewne ograniczenia z tym związane - kolumny text
zostały oficjalnie zdeprecjonowane.
W MSSQL są problemy z odwołaniami cyklicznymi (ang. circular references) w tabelach, które mają włączoną opcje ONDELETE CASCADE. Jest to błąd MSSQL i można go obejść ustawiając atrybut ondelete dla wszystkich pól referencyjnych na "NO ACTION". Można to również zrobić raz i na zawsze, przed definicja tabel:
db = DAL('mssql://....')
for key in ['reference','reference FK']:
db._adapter.types[key]=db._adapter.types[key].replace(
'%(on_delete_action)s','NO ACTION')
W MSSQL występują również problemy z argumentami przekazywanymi do słowa kluczowego DISTINCT i dlatego podczas wykonywania
db(query).select(distinct=True)
to nie daje
db(query).select(distinct=db.mytable.myfield)
Oracle
Oracle również nie obsługuje stronicowania. Baza ta nie obsługuje słowa OFFSET ani LIMIT. Web2py osiąga paginację tłumacząc db(...).select(limitby=(a,b))
na złożoną, trójstopniowo zagnieżdżoną instrukcję wybierajacą (tak jak sugeruje to oficjalna dokumentacja Oracle). Działa to na prostych instrukcjach wybierajacych, ale może się załamać przy bardziej złożonych wyborach zawierających pola aliasowane i złączenia.
Google NoSQL (Datastore)
Google NoSQL (Datastore) nie pozwala wykonywać złaczeń, lewych złączeń, agregatów, wyrażeń, operatora OR zawierającego więcej niż jedna tabelę, operatora ‘like’ wyszukującego w polacha "text".
Transakcje są ograniczone i nie są dostarczane automatycznie przez web2py (trzeba użyć run_in_transaction
z API Google, z którym można się zapoznać czytając dokumentację internetową Google App Engine).
Google ogranicza również liczbę rekordów, jakie można pobrać w jednym zapytaniu (1000 w chwili pisania podręcznika). Identyfikatory rekordów na Google Datastore są liczbami całkowitymi, ale nie sę generowane po kolei. Podczas gdy w bazach SQL typ "list:string" jest odwzorowywany na typ "text" type, na Google Datastore jest to odwzorowywane na ListStringProperty
. Podobnie, "list:integer" i "list:reference" są odwzorowywane na "ListProperty". Sprawia to, że wyszukiwanie treści wewnątrz tych pól jest bardziej efektywne na Google NoSQL niz w bazach danych SQL.