Chapter 6: Warstwa abstracji bazy danych

Warstwa abstrakcji bazy danych

DAL

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.

sterowniki 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:

DAL
SQLite
MySQL
PostgresSQL
Oracle
MSSQL
FireBird
DB2
Informix
Sybase
Teradata
MongoDB
CouchDB
SAPDB
Cubrid

Baza danychSterowniki (źródło)
SQLitesqlite3 lub pysqlite2 lub zxJDBC [zxjdbc] (na Jython)
PostgreSQLpsycopg2 [psycopg2] lub pg8000 [pg8000] lub zxJDBC [zxjdbc] (na Jython)
MySQLpymysql [pymysql] lub MySQLdb [mysqldb]
Oraclecx_Oracle [cxoracle]
MSSQLpyodbc [pyodbc]
FireBirdkinterbasdb [kinterbasdb] lub fdb lub pyodbc
DB2pyodbc [pyodbc]
Informixinformixdb [informixdb]
Ingresingresdbi [ingresdbi]
Cubridcubriddb [cubridb]
SybaseSybase [Sybase]
Teradatapyodbc [Teradata]
SAPDBsapdb [SAPDB]
MongoDBpymongo [pymongo]
IMAPimaplib [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:

sqlite
1
db = DAL('sqlite://storage.db')
define_table

Table reprezentuje tabele bazy danych. Nie tworzy się bezpośrednio instancji z klasy Table. Zamiast tego wykorzystuje się metodę DAL.define_table.

1
db.define_table('mytable', Field('myfield'))

Najważniejsze metody Table, to:

insert
truncate
drop
import_from_csv_file
count

.insert, .truncate, .drop i .import_from_csv_file.

Field

Field reprezentuje pole bazy danych. Można tworzyć jego instancje i przekazywać argument obiektu do metody DAL.define_table.

Rows
Row
DAL Rows jest obiektem zwracanym przez metodę select. Może być traktowany jak lista obiektów Row:

1
rows = db(db.mytable.myfield!=None).select()
Row

Obiekt Row zawiera wartości pola.

1
2
for row in rows:
    print row.myfield
Query

Query jest obiektem reprezentującym klauzulę "where" SQL:

1
myquery = (db.mytable.myfield != None) | (db.mytable.myfield > 'A')
Set

Set jest obiektem reprezentującym zestaw rekordów. Jego najważniejsze metody, to count, select, update i delete. Na przykład:

1
2
3
4
myset = db(myquery)
rows = myset.select()
myset.update(myfield='somevalue')
myset.delete()
Expression

Expression jest czymś w rodzaju wyrażenia orderby lub groupby. Klasa Field jest pochodną klasy Expression. Oto przykład.

1
2
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:

1
2
3
   
from gluon import DAL, Field
# można też rozważyć: from gluon.validators import *

Konstruktor DAL

Podstawowe użycie:

1
>>> 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.

_uri
1
2
>>> print db._uri
sqlite://storage.db

i nazwę bazy danych

_dbname
1
2
>>> 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)

łańcuch połączenia

Połączenie z bazą danych jest ustanawiane przez utworzenie instancji klasy DAL:

1
>>> 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
SQLitesqlite://storage.db
MySQLmysql://username:password@localhost/mydb
PostgreSQLpostgres://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
FireBirdfirebird://username:password@localhost/mydb
Oracleoracle://username/password@mydb
DB2db2://username:password@mydb
Ingresingres://username:password@localhost/mydb
Sybasesybase://username:password@localhost/mydb
Informixinformix://username:password@mydb
Teradatateradata://DSN=dsn;UID=user;PWD=pass;DATABASE=mydb
Cubridcubrid://username:password@localhost/mydb
SAPDBsapdb://username:password@localhost/mydb
IMAPimap://user:password@server:port
MongoDBmongodb://username:password@localhost/mydb
Google/SQLgoogle:sql://project:instance/database
Google/NoSQLgoogle:datastore
Google/NoSQL/NDBgoogle: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:

1
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:

1
db = DAL('...', db_codec='latin1')

w przeciwnym razie otrzyma się bilety UnicodeDecodeError.

Buforowanie połączeń

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:

1
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

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:

1
2
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:

1
check_reserved=['postgres', 'postgres_nonreserved']

Następujące motory baz danych obsługują zarezerwowane słowa kluczowe.

PostgreSQLpostgres(_nonreserved)
MySQLmysql
FireBirdfirebird(_nonreserved)
MSSQLmssql
Oracleoracle

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 = Trueentity_quoting = True

Oto przykład:

1
2
3
4
5
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 tabel
  • fake_migrate = False ustawia domyślne zachowanie fake_migrate dla wszystkich tabel
  • migrate_enabled = True, jeśli ustawione na False, wyłącza wszystkie migracje
  • fake_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

define_table
Field

Sygnatura define_table

Tabele definiowane są w DAL poprzez define_table:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
>>> 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.

pole id

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:

1
2
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.

1
>>> db.define_table('person', Field('name'), format='%(name)s')

lub

1
>>> db.define_table('person', Field('name'), format='%(name)s %(id)s')

albo nawet coś bardziej skomplikowanego przy użyciu funkcji:

1
2
>>> 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:

1
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:

1
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:

1
2
3
4
5
 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

1
db.sometable.somefield == some_value
, które powoduje wcześniejsze zdefiniwanie sometable. Jest to sytuacja opisana w on_define.

Leniwe tabele - główny sposób zwiększenia wydajności

leniwe tabele

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:

1
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:

1
db.table._extra = {} 

Konstruktor Field

konstruktor Field

Oto domyślne wartości konstruktora Field:

1
2
3
4
5
6
7
8
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śli length 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, parametr default 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 gdy notnull, unique i ondelete 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.
    ondelete
  • ondelete 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 parametr uploadfield jest ustawiony na True, 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,

1
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 na True, 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ści uploadseparate 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ć na PyFileSystem.
    PyFileSystem
    uploadfs
  • 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 jako dict 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 pol uploaded 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:

1
2
3
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

typy pól
Typ polaDomyślne walidatory pola
stringIS_LENGTH(length) domyślna długość, to 512
textIS_LENGTH(65536)
blobNone
booleanNone
integerIS_INT_IN_RANGE(-1e100, 1e100)
doubleIS_FLOAT_IN_RANGE(-1e100, 1e100)
decimal(n,m)IS_DECIMAL_IN_RANGE(-1e100, 1e100)
dateIS_DATE()
timeIS_TIME()
datetimeIS_DATETIME()
passwordNone
uploadNone
reference <table>IS_IN_DB(db,table.field,format)
list:stringNone
list:integerNone
list:reference <table>IS_IN_DB(db,table.field,format,multiple=True)
jsonIS_JSON()
bigintNone
big-idNone
big-referenceNone

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.

blob

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:

tables
1
2
>>> print db.tables
['person']

Można również uzyskać listę pól zdefiniowanych dla określonej tabeli:

fields
1
2
>>> print db.person.fields
['id', 'name']

Można zapytać o typ tabeli:

Table
1
2
>>> print type(db.person)
<class 'pydal.objects.Table'>

i można uzyskać dostęp do tabeli z połączenia DAL używając:

1
2
>>> 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:

1
2
3
4
5
6
>>> 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:

1
2
3
4
5
6
7
8
>>> 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:

1
2
3
4
5
6
>>> 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

migracja

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":

1
>>> 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,

1
>>> 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 i ondelete.

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

fake_migrate

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:

1
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:

1
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:

1
2
3
4
5
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:

insert
1
2
3
4
>>> 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.

truncate
1
>>> 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):

1
2
>>> 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.

1
db.person.truncate('RESTART IDENTITY CASCADE')

Argument ten jest surowym kodem SQL i dlatego zależy od motoru bazy danych.

bulk_insert

web2py udostępnia też metodę bulk_insert

1
2
>>> 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:

commit
1
>>> db.commit()

Aby to sprawdzić, wstaw nowy rekord:

1
2
>>> db.person.insert(name="Bob")
2

i cofnąć transakcję, czyli zignorować wszystkie operacje od ostatniego zatwierdzenia:

rollback
1
>>> db.rollback()

Jeśli teraz dokona się ponownie wstawienia, licznik znowu zostanie ustawiony na 2, ponieważ poprzednia operacja wstawienia została wycofana.

1
2
>>> db.person.insert(name="Bob")
2

Kod w modelach, widokach i kontrolerach jest zamykany w kodzie web2py, co wygląda tak:

1
2
3
4
5
6
7
8
9
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.

executesql
1
2
>>> 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.

1
[{field1: value1, field2: value2}, {field1: value1b, field2: value2b}]

Argument fieldsjest 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:

_lastdb
1
2
3
>>> 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:

drop
1
>>> 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:

1
2
3
>>> 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.

tabele z kluczem

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:

1
2
3
4
5
6
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

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:

1
2
db_a = DAL('postgres://...')
db_b = DAL('postgres://...')

W modelach i kontrolerach można zatwierdzać zmiany do nich jednocześnie:

1
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

1
2
>>> 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:

1
2
>>> 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:

1
2
>>> 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:

1
2
3
4
5
6
>>> 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:

1
2
3
4
5
6
7
>>> 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:

Table
1
>>> person = db.person

Można również w zmiennej przechowywać pole, takie jak name. Przykładowo, można to zrobić tak:

Field
1
>>> name = person.name

Można nawet zbudować zapytanie (używając operatorów takich jak ==, !=, <, >, <=, >=, like, belongs) i przechować ją w zmiennej q:

Query
1
>>> 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:

Set
1
>>> 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
select

1
>>> rows = s.select()
Row

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:

1
2
3
>>> for row in rows:
        print row.id, row.name
1 Alex

Można wykonać te wszystkie czynności w jednym poleceniu:

1
2
3
>>> for row in db(db.person.name=='Alex').select():
        print row.name
Alex
ALL

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":

1
2
3
4
5
>>> 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:

1
2
3
4
5
>>> 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:

1
2
3
4
5
>>> 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.

1
2
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:

1
2
for row in rows.render():
    print row.myfield

Ma to również zastosowanie do wycinka:

1
2
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:

1
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

skróty DAL

DAL obsługuje różne skróty upraszczające kod. W szczególności:

1
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

1
myrecord = db(db.mytable.id==id).select().first()

Można usuwać rekord odwołując się do id:

1
del db.mytable[id]

co jest równoważne z

1
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:

1
db.mytable[0] = dict(myfield='somevalue')

Jest to ekwiwalenetem wyrażenia

1
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:

1
db.mytable[id] = dict(myfield='somevalue')

co jest ekwiwalentem dla

1
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:

1
2
3
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

rekursywne polecenia select

Rozważmy poprzednia tabelę "person" i nowa tabelę "thing" odwołująca się do "person":

1
2
3
>>> db.define_table('thing',
        Field('name'),
        Field('owner_id','reference person'))

Wybierzmy z tej tabeli zestaw rekordów, w ten sposób:

1
>>> things = db(db.thing).select()

Jest to równoważne poleceniu

1
>>> 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.

_id

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):

1
>>> 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"):

1
2
3
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

1
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:

SQLTABLE
1
2
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:

1
2
3
{{extend 'layout.html'}}
<h1>Records</h1>
{{=rows}}

Co jest równoważne z:

1
2
3
{{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ślnie None);

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:

1
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:

1
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ługujemy headers='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:

1
2
3
4
5
6
7
{{extend 'layout.html'}}
<h1>Records</h1>
{{=SQLTABLE(rows,
     headers='fieldname:capitalize',
     truncate=100,
     upload=URL('download'))
}}

SQLFORM.grid
SQLFORM.smartgrid

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:

1
2
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:

orderby
groupby
having

1
2
3
4
5
6
>>> 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ę):

1
2
3
4
5
6
>>> 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:

1
2
3
4
5
6
>>> 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:

1
2
import random
rows=db(...).select().sort(lambda row: random.random())

Można sortować rekordy z wykorzystaniem wielu pól, posługując się znakiem "|":

1
2
3
4
5
6
>>> 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):

1
2
3
4
5
6
7
>>> 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
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:

1
2
3
4
5
>>> 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:

1
2
3
4
5
>>> 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):

limitby
1
2
3
4
>>> for row in db().select(db.person.ALL, limitby=(0, 2)):
        print row.name
Alex
Bob
orderby_on_limitby
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:

1
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 "&":

and
or
not

1
2
3
>>> 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 "|":

1
2
3
>>> 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 "!=":

1
2
3
4
>>> 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 "~":

1
2
3
4
>>> 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 "&amp;" 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:

count
isempty

1
2
>>> 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:

1
2
>>> print db(db.person.id > 0).isempty()
False

lub równoważnie:

1
2
>>> print db(db.person).isempty()
False

Można usuwać rekordy z zestawu:

delete
1
>>> db(db.person.id > 3).delete()

Można również aktualizować wszystkie rekordy w zestawie przekazując nazwane argumenty odpowiadające aktualizowanym polom:

update
1
>>> 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:

1
2
3
4
5
>>> 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:

1
2
3
4
5
>>> 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:

1
2
3
4
5
6
7
>>> 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

update_record

web2py umożliwia również aktualizowanie pojedynczego rekordu, który jest już w pamięci, używając polecenia update_record

1
2
>>> row = db(db.person.id==2).select().first()
>>> row.update_record(name='Curt')

Polecenie update_record nie powinno być mylone z

1
>>> 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:

1
2
3
>>> 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:

1
db[tablename].insert(**{fieldname:value})

Aktualizację rekordu o określonym id można wykonać z:

_id

1
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

first
last

Rozpatrzmy obiekt Rows zawierający rekordy:

1
2
3
>>> rows = db(query).select()
>>> first_row = rows.first()
>>> last_row = rows.last()

co jest równoważne z

1
2
>>> 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

as_list
as_dict

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:

1
2
3
>>> 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):

1
2
3
>>> 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:

1
2
3
4
>>> rows3 = rows1 & rows2
>>> print rows3
name
Tim

Można wykonać złączenie union rekordów usuwając powtórzenia:

1
2
3
4
5
6
>>> rows3 = rows1 | rows2
>>> print rows3
name
Max
Tim
John

find, exclude, sort

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
>>> 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:

1
2
3
4
5
6
7
8
>>> 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

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:

1
2
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:

1
2
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:

1
2
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

validate_and_insert
validate_and_update

Funkcja

1
ret = db.mytable.validate_and_insert(field='value')

działa badzo podobnie jak:

1
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

1
ret = db(query).validate_and_update(field='value')

działa tak samo jak

1
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

compute

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:

1
2
3
4
5
6
7
8
>>> 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

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:

1
2
3
>>> db.define_table('item',
        Field('unit_price','double'),
        Field('quantity','integer'),

Zdefiniujmy wirtualne pole total_price jako

1
2
3
>>> 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:

1
>>> 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:

1
2
3
4
5
>>> 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ę:

1
2
3
>>> db.define_table('item',
        Field('unit_price','double'),
        Field('quantity','integer'),

Teraz zdefiniujemy pole wirtualne total_price:

1
2
3
4
>>> 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:

1
2
3
4
5
6
7
8
9
>>> 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> 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:

1
2
3
>>> class MyVirtualFields(object):
        def lazy_total_price(self):
            return lambda self=self: self.item.unit_price                 * self.item.quantity

Relacje jeden do wielu

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:

1
2
3
4
5
6
7
>>> 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:

1
2
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ć:

1
2
>>> print db.thing.owner_id.type
reference person

Teraz wstawimy trzy rzeczy, dwie własności Alexa i jedna własności Boba:

1
2
3
4
5
6
>>> 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:

1
2
3
4
>>> 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:

referencing
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> 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
złączenia wewnętrzne
złączenia
INNER JOIN

1
2
3
4
5
6
>>> 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:

1
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ć:

1
row.person.name

lub:

1
row.thing.name

Oto alternatywna składnia INNER JOINS:

1
2
3
4
5
6
>>> 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
left outer join
outer join

1
2
3
4
5
6
7
8
9
>>> 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:

1
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:

grouping
1
2
3
4
5
6
>>> 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

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ć:

1
2
3
4
5
6
7
>>> 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:

1
2
3
>>> 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:

1
>>> 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:

1
2
>>> 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:

1
2
3
4
5
6
>>> 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:

1
2
3
4
>>> for row in persons_and_things(db.person.name=='Alex').select():
        print row.thing.name
Boat
Chair

oraz wszystkich właścicieli łodzi:

1
2
3
4
>>> 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

list:string
list:integer
list:reference
contains
multiple
tags

Platforma web2py udostępnia następne specjalne typy pól:

1
2
3
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:

1
2
3
4
5
6
7
8
9
>>> 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 operatora contains(value) odwzorowywane są na zapytanie, które sprawdza listy zawierające value. Operator contains działa również dla zwykłych pól typu string i text oraz jest odwzorowywane na wyrażenie LIKE '%value%'.

Typ list:reference i operator contains(value) są szczególnie przydatne do de-normalizacji relacji wiele-do-wielu. Oto przykład:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
>>> 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

1
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 i list:string nie. Tak więc te dwa typy pól potrzebują walidatora IS_IN_SET lub IS_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ą.

date
datetime
time

1
2
3
>>> 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).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> 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

like
ilike
startswith
endswith
regexp
contains
upper
lower

Pola mają operator like, który można wykorzystać do dopasowania ciągów znakowych:

1
2
3
>>> 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:

1
db.mytable.myfield.like('value',case_sensitive=False)

Platforma web2py dostarcza również kilka skrótów:

1
2
3
db.mytable.myfield.startswith('value')
db.mytable.myfield.endswith('value')
db.mytable.myfield.contains('value')

co w przybliżeniu odpowiedno odpowiada:

1
2
3
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:

upper
lower

1
2
3
>>> for row in db(db.log.event.upper().like('PORT%')).select():
        print row.event
port scan

year, month, day, hour, minutes, seconds

hour
minutes
seconds
day
month
year

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:

1
2
3
4
5
>>> 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):

belongs
1
2
3
4
>>> 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.

nested select
1
2
3
4
5
6
>>> 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:

1
2
3
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.

nested_select

Zagnieżdżone polecenie select może być używane jako wartość poleceń insert lub update, w takim przypadku składnia jest inna:

1
2
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

sum
avg
min
max
Poprzednio używaliśmy operatora 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:

1
2
3
>>> 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:

1
2
3
>>> 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:

1
2
>>> 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:

1
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

raw 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

_insert

1
2
>>> print db.person._insert(name='Alex')
INSERT INTO person(name) VALUES ('Alex');

Przykład dla _count

_count

1
2
>>> print db(db.person.name=='Alex')._count()
SELECT count(*) FROM person WHERE person.name='Alex';

Przykład dla _select

_select

1
2
>>> print db(db.person.name=='Alex')._select()
SELECT person.id, person.name FROM person WHERE person.name='Alex';

Przykład dla _delete

_delete

1
2
>>> print db(db.person.name=='Alex')._delete()
DELETE FROM person WHERE person.name='Alex';

Przykład dla _update

_update

1
2
>>> 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

export
import

CSV (jedna tabela za jednym razem)

Podczas konwertowania obiektu *Rows* do ciągu znakowego jest on automatycznie serializowany w CSV:

csv
1
2
3
4
5
6
>>> 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":

1
>>> open('test.csv', 'wb').write(str(db(db.person.id).select()))

Jest to równoważne z:

1
2
>>> rows = db(db.person.id).select()
>>> rows.export_to_csv_file(open('test.csv', 'wb'))

Można z powrotem odczytać plik CSV stosując:

1
>>> 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:

1
>>> db.export_to_csv_file(open('somefile.csv', 'wb'))

Import wszystkich tabel:

1
>>> 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:

1
2
TABLE tablename
field1, field2, field3, ...

Dwie tabele są rozdzielane sekwencją \r\n\r\n. Plik kończy się linią

1
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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:

1
2
3
4
5
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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.

XML-RPC

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

Obiekty Rows (podobnie jak helpery) posiadaja również metodę xml, która serializuje je do formatu XML/HTML:

HTML
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> 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>
własne znaczniki Rows

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 (*):

XML
1
2
3
4
5
6
7
8
9
>>> 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

export_to_csv_file

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.

colnames

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ślnie csv.QUOTE_MINIMAL)

Oto przykład:

1
2
3
4
5
6
>>> 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:

1
"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.

cache select
1
2
3
def cache_db_select():
    logs = db().select(db.log.ALL, cache=(cache.ram, 60))
    return dict(logs=logs)
cacheable

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:

1
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:

1
rows = db(query).select(cache=(cache.ram,3600),cacheable=True)

Samoodnoszenie tabel i aliasy

self reference
alias

Możliwe jest zdefiniowanie tabel z polami, które odnoszą się do siebie, oto przykład:

reference table
1
2
3
4
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:

1
2
3
4
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.

with_alias

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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

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:

1
2
db.define_table('person', Field('name'))
db.define_table('doctor', db.person, Field('specialization'))
dummy table

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:

1
2
3
4
5
6
7
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

filter_in
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ć:

1
2
3
4
5
6
7
8
9
>>> 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

_before_insert
_after_insert
_before_update
_after_update
_before_delete
_after_delete

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:

1
2
3
4
5
6
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.

1
2
3
4
5
6
7
8
>>> 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.

1
2
3
4
5
6
7
8
9
>>> 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.

update_naive

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

_enable_record_versioning

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ę:

1
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ę:

1
2
3
4
5
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:

1
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

common fields
multi tenancy
obsługa wielu podmiotów
wspólne pola

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:

1
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:

ignore_common_filters

1
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)

SQLCustomType

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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:

1
2
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:

1
2
3
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)

PostGIS
StatiaLite
rozszerzenia geograficzne
rozszerzenia geometryczne
geoPoint
geoLine
geoPolygon

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
st_astext
st_contains
st_distance
st_equals
st_intersects
st_overlaps
st_simplify
st_touches
st_within

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:

1
2
3
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:

1
2
3
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:

1
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():

1
2
3
4
5
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):

1
2
3
4
5
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:

1
2
3
4
5
6
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:

1
2
3
4
5
6
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ę:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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)

limitby

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:

1
2
3
4
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.

 top