Bazy danych - Politechnika Gdańska

Transkrypt

Bazy danych - Politechnika Gdańska
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
Instrukcja do laboratorium z przedmiotu:
Bazy danych
Laboratorium nr 4.
Funkcje własne, procedury wyzwalane i przetwarzanie transakcyjne
Opracował A. Bujnowski
2010-04-08
Projekt „Przygotowanie i realizacja kierunku inżynieria biomedyczna – studia międzywydziałowe”
współfinansowany ze środków Unii Europejskiej w ramach Europejskiego Funduszu Społecznego.
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
1. Cele laboratorium – zapoznanie się z metodami tworzenia własnych funkcji, technologią
procedur wyzwalanych oraz przetwarzaniem transakcyjnym
2. Przykładowa baza danych:
Jako przykład bazy danych posłuży nam przygotowana uprzednio struktura bazy dla
wypożyczalni płyt DVD. Dla przypomnienia diagram relacyjny przykładowej bazy danych
przedstawia poniższy rysunek.
CREATE TABLE klient (
imie varchar(20) not null,
nazwisko varchar(40) not null,
nr_dowodu char(10),
id_klienta serial primary key);
CREATE TABLE gatunek(
nazwa varchar(30) not null,
id_gatunku serial PRIMARY KEY
);
CREATE TABLE plyta(
tytul varchar(40) not null,
numer serial primary key,
cena numeric(4,2),
gatunek integer REFERENCES gatunek ON DELETE SET NULL ON
UPDATE CASCADE);
CREATE TABLE wypozyczenie(
kto_wypozyczyl int not null REFERENCES klient ON DELETE
RESTRICT ON UPDATE RESTRICT,
co_wypozyczyl int not null REFERENCES plyta ON DELETE
RESTRICT ON UPDATE CASCADE,
data_wypozyczenia timestamp default noow(),
2
BAZY DANYCH, laboratorium nr 2 , A. Bujnowski
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
data_zwrotu timestamp,
primary key(kto_wypozyczyl, co_wypozyczyl,
d_wypozyczenia) );
CREATE TABLE jest_pracownikiem(
rabat int,
id_klienta int primary key references klient);
Cel zajęć:
Funkcje własne użytkownika.
Funkcje agregacji
Dla przypomnienia sprawdźmy działanie poniższych wywołań:
SELECT
SELECT
SELECT
SELECT
2+2;
sin(1);
log(numer) FROM plyta;
sin(cena) FROM plyta;
Ale istnieją także inne funkcje:
SELECT
SELECT
SELECT
SELECT
SELECT
count(*) FROM gatunek;
count(imie) FROM klient;
max(id_klienta) FROM klient;
min(id_klienta) FROM klient;
avg(id_klienta) FROM klient;
Te funkcje w odróżnieniu od poprzednich zwracają pojedynczy wynik a operują na grupach krotek.
Funkcje takie noszą miano funkcji agregacji, gdyż dokonują analizy na grupie krotek. Gdy
wykonamy funkcję sin(id_klienta) to zwróci ona tyle wyników na ilu krotkach się ona wykonała,
natomiast funkcje takie jak max(), min(), avg(), variance(), stdev() czy count() zwrócą pojedynczy
wynik.
Z tymi funkcjami związana jest dodatkowa klauzula zapytania SELECT : GROUP BY.
GROUP BY powoduje przestawienie wyników w taki sposób, aby stanowiły one "przegrupowaną”
tabelę względem kryterium. Na tak przegrupowanej tabeli możliwe jest wykonywanie funkcji
agregacji i dalsza analiza wyników. Przykładowo – chcemy dokonać statystycznego zestawienia
imion występujących w tabeli klient:
SELECT imie, count(imie) FROM klient GROUP BY imie;
Z klauzulą GOUP BY związana jest klauzula HAVING, która działa tak samo jak WHERE dla
"normalnej” wersji SELECT:
3
BAZY DANYCH, laboratorium nr 2 , A. Bujnowski
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
SELECT imie, count(imie) FROM klient GROUP BY imie HAVING imie <
'J';
W ramach laboratorium pokazana została już własność polecenia SELECT polegająca na możliwości
wywoływania funkcji lub operowania na danych (operacje arytmetyczne itp.). Pokazane zostały
również własności agregujące (COUNT, MIN, MAX itp.) pozwalające na wykonywanie operacji
zwracających pojedynczy wynik dla grup krotek. Przydatnym jednak może okazać się możliwość
tworzenia własnych funkcji dających dodatkowe możliwości twórcy baz danych.
Do tworzenia własnych funkcji służy polecenie CREATE FUNCTION. Zostało ono zdefiniowane w
normie SQL:1999 i późniejszych. Składnia stosowana w PostgreSQL jest podobna, ale nie w pełni
kompatybilna. Atrybuty nie są przenaszalne, jak również odmienne są języki programowania.
Składnia polecenia CREATE FUNCTION podana jest poniżej. W celu uzyskania dokładniejszego opisu
można skorzystać z materiałów podanych na wykładzie, bądź skorzystać z dokumentacji projektu
na stronie www.postgresql.org
CREATE [ OR REPLACE ] FUNCTION
name ( [ [ argmode ] [ argname ] argtype [, ...] ] )
[ RETURNS rettype ]
{ LANGUAGE langname
| IMMUTABLE | STABLE | VOLATILE
| CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT
| [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
| COST execution_cost
| ROWS result_rows
| SET configuration_parameter { TO value | = value | FROM CURRENT }
| AS 'definition'
| AS 'obj_file', 'link_symbol'
} ...
[ WITH ( attribute [, ...] ) ]
W poniższych przykładach będziemy korzystali z funkcji pisanych w dwóch dostępnych językach
programowania : SQL oraz plpgsql.
Pierwsza funkcja. Napiszmy funkcję, która zwróci nam pole koła:
CREATE FUNCTION polekola( float) RETURNS float
LANGUAGE 'plpgsql'
AS '
BEGIN
RETURN 3.1415*$1*$1;
END;
';
Gdy zachodzi konieczność poprawienia funkcji, wówczas nie trzeba jej kasować. Możliwe jest
użycie polecenia
4
BAZY DANYCH, laboratorium nr 2 , A. Bujnowski
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
CREATE OR REPLACE FUNCTION polekola(float) ….
Ważne jest aby przy takim użyciu polecenia redefiniowana funkcja nie zmieniała typu ani ilości
danych wejściowych.
Zatem, poprawiamy funkcję korzystając z gotowych funkcji np. zwracających stałe, aby otrzymać
dokładniejszy wynik:
CREATE OR REPLACE FUNCTION polekola( float) RETURNS float
LANGUAGE 'plpgsql'
AS '
DECLARE wpi float;
BEGIN
select pi() into wpi;
RETURN wpi*$1*$1;
END;
';
Wywołanie tej funkcji odbywa się przez:
SELECT polekola(5);
Możliwe jest również wywołanie takiej funkcji dla całej kolumny w tabeli. Sprawdź jak zadziała
wykonanie poniższego wywołania:
SELECT polekola(id_klienta) FROM klient;
Usuwanie funkcji:
Do usunięcia funkcji używa się polecenia DROP FUNCTION podając nazwę funkcji i jej parametry
wejściowe:
DROP FUNCTION polekola(float); - nie rób tego !!
pozwoli na usunięcie funkcji, która za chwilę zostanie utworzona. Paarametry wejściowe są
konieczne ze względu na to że w PostgreSQL możliwe jest zdefiniowanie funkcji o tej samej nazwie
działającej na róznych typach danych. Przypomina to bardzo przeciążanie operatorów.
Zdefiniowaną funkcję można obejrzeć korzystając z zapytania:
SELECT * from pg_proc WHERE proname='polekola';
SELECT prosrc FROM pg_proc WHERE proname='polekola';
Listę funkcji zdefiniowanych można obejrzec korzystając z \df
Zadanie:
1. Stwórz funkcję o nazwie poletrojkata, która wyznaczy pole trójkąta ze wzoru 0.5*a*h, gdzie
5
BAZY DANYCH, laboratorium nr 2 , A. Bujnowski
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
a jest podstawą a h wysokością trójkąta.
2. Stwórz funkcją, która wyznaczy deltę z równania kwadratowego.
Zademonstruj działanie funkcji prowadzącemu zajęcia.
Bardziej wartościowe działanie funkcji.
Załóżmy, że w bazie danych dotyczących naszej wypożyczalni mamy osobę (klienta) , jakiś obiekt
wypożyczeń oraz tabelę wypożyczeń. Obiekt posiada cenę.
Należy się zastanowić, czy klientowi naszej wypożyczalni przysługuje rabat, w związku z tym
sprawdźmy sumę wartości jego wszystkich wypożyczeń, jeśli przekroczyła zadaną kwotę (np.
30pln) to zwróćmy wartość rabatu równą 10, jeśli nie to 0.
CREATE FUNCTION retrabat(int) RETURNS int
LANGUAGE plpgsql
AS
'
declare
suma float;
rabat int;
begin
select sum(cena) into suma from plyta,wypozyczenie where
plyta.numer=wypozyczenie.co_wypozyczyl AND
wypozyczenie.kto_wypozyczyl = $1;
if suma > 30
then rabat = 10;
else rabat = 0;
end if;
return rabat;
end;
';
Sprawdź działanie tej funkcji dla konkretnego klienta:
SELECT retrabat(1);
lub lepiej :
SELECT retrabat((SELECT id_klienta from klient WHERE nazwisko like
'Kowalski'));
Możesz też sprawdzić działanie tej funkcji dla wszystkich klientów w bazie:
SELECT imie,nazwisko,retrabat(id_klienta) from klient;
Zastanów się nad podobną funkcją, ale uzależniającą rabat od ilości wypożyczeń np. w ciągu
6
BAZY DANYCH, laboratorium nr 2 , A. Bujnowski
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
ostatnich 3 miesięcy. Zastanów się jak przekazać próg udzielania rabatu i okres sprawdzania
wypożyczeń przez parametry wejściowe funkcji.
Triggery
TRIGGER, po polsku nazywany procedurą wyzwalaną, jest funkcją zdefiniowaną przez użytkownika
wywoływaną, gdy pojawi sie jakieś wydarczenie (event) w tabeli. W Postgresie można wywoływać
triggery przed/po wstawieniu, modyfikacji lub usunięciu rekordu. Triggery mogą być wykonywane
raz dla całej instrukcji (statement level) lub po kolei dla każdego modyfikowanego rekordu (row
level).
Trigger definiuje się następującą składnią:
CREATE TRIGGER nazwa (każdy trigger musi się jakoś
nazywać)
BEFORE albo AFTER (czy trigger ma być wykonany przed czy
po wydarzeniu)
INSERT albo UPDATE albo DELETE (których wydarzen trigger
dotyczy, można połączyć kilka przez OR)
ON tabela (trigger zawsze zakłada się na konkretną
tabelę)
FOR EACH
ROW albo STATEMENT (czy trigger ma być wywołany raz na
rekord, czy raz na instrukcję)
EXECUTE PROCEDURE procedura(parametry); (co ma być
wywołane jako obsługa triggera)
Trigger usuwa się następującą składnią:
DROP TRIGGER nazwa ON tabela; (usunięce konkretnego triggera z konkretnj tabeli)
Tak więc, aby wywołać funkcję skasowano() po każdym skasowaniu rekordu z bazy rejestr należy
wydać następujące polecenie:
CREATE TRIGGER mojtrigger AFTER DELETE ON rejestr FOR EACH ROW EXECUTE PROCEDURE
skasowano() ;
Funkcja skasowano() musi zwracać typ TRIGGER.
Sprawdź jak to działa:
Stwórzmy funkcję skasowano()
CREATE FUNCTION skasowano() RETURNS TRIGGER
LANGUAGE plpgsql
7
BAZY DANYCH, laboratorium nr 2 , A. Bujnowski
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
AS
' BEGIN
RAISE NOTICE ''Skasowano jeden rekord'';
RETURN NULL;
END; ';
teraz stwórzmy trigger:
CREATE TRIGGER tr_delkli AFTER DELETE ON klient FOR EACH ROW
EXECUTE PROCEDURE skasowano();
i podobny do niego :
CREATE FUNCTION skasowano1() RETURNS TRIGGER
LANGUAGE plpgsql
AS
' BEGIN
RAISE NOTICE ''Skasowano rekordy'';
RETURN NULL;
END; ';
CREATE TRIGGER tr_delkli1 AFTER DELETE ON klient FOR EACH
statement EXECUTE PROCEDURE skasowano1();
Sprawdźmy jak to działa:
delete from klient where id_klienta not in (select kto_wypozyczyl
from wypozyczenie) and id_klienta not in (select id_klienta from
jest_pracownikiem);
Język PL/pgSQL może być używany do tworzenia procedur wyzwalanych. Najpierw należy stworzyć
funkcję poleceniem CREATE FUNCTION deklarując ją jako funkcję bez argumentów wejściowych i
zwracającą typ trigger. Zauważ, że funkcja musi być zdeklarowana bez argumentów wejściowych
nawet jeżeli takowych wymaga. Wówczas argumenty przekazywane są do funkcji przez zmienną
systemową o nazwie TG_ARGV.
Kiedy wywoływana jest funkcja w PL/pgSQL jako TRIGGER kilka specjalnych zmiennych jest
tworzonych. Są to:
NEW
Dana typu RECORD; zawiera nowy wiersz dla operacji typu INSERT/UPDATE w wyzwalaczach
poziomu wierszowego. Przyjmuje NULL w wyzwalaczach typu statement-level.
OLD
Dana typu RECORD; zawiera stary wiersz dla operacji typu INSERT/UPDATE w wyzwalaczach
poziomu wierszowego. Przyjmuje NULL w wyzwalaczach typu statement-level.
8
BAZY DANYCH, laboratorium nr 2 , A. Bujnowski
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
TG_NAME
Nazwa name; nazwa triggera, który został aktualnie uruchomiony.
TG_WHEN
Zmienna tekstowa przechowująca wiadomość czy trigger został stworzony BEFORE czy AFTER
zdarzenia.
TG_LEVEL
Zmienna tekstowa przechowująca wiadomość czy trigger został stworzony dla ROW czy
STATEMENT.
TG_OP
Zmienna tekstowa przechowująca wiadomość czy trigger został stworzony dla INSERT,
UPDATE czy DELETE.
TG_RELID
ID objektu, który wywołał trigger.
TG_RELNAME
Nazwa tabeli, która wywołała trigger.
TG_NARGS
Typ danych integer; Liczba argumentów podanych do procedury funkcji triggera.
TG_ARGV[]
Macierz danych tekstowych; argumenty podawane do procedury funkcji triggera.
Funkcja deklarowana na potrzeby wyzwalaczy musi zwrócić NULL albo wartość typu rekord/wiersz
mającą dokładnie strukturę tabeli, dla której trigger zadziałał.
Triggery wyzwalane dla wierszy działające przed akcją (BEFORE) mogą zwracać null aby odwołać
akcję, która została dla danego wiersza odpalona (np. wszystkie następujące wyzwalacze i działania
typu INSERT/UPDATE/DELETE nie zostaną wykonane dla tego wiersza. Jeśli trigger zwróci wartość
nie-null wszystko co następuje po tej funkcji zostanie dla tego wiersza wykonane.
9
BAZY DANYCH, laboratorium nr 2 , A. Bujnowski
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
Zwracając wartość wiersza różną od wartości oryginalnej dla NEW podmienia wartość tego wiersza
w przypadku INSERT lub UPDATE ale nie ma bezpośredniego wpływu na DELETE. Aby podmienić
zawartość wiersza możliwa jest podmiana pojedynczych wartości tego wiersza bezpośrednio lub
stworzenie kompletnie nowego wiersza (struktury) typu NEW.
Wartość zwracana dla wywołania typu STATEMENT, lub AFTER dla wywołania typu ROW jest zawsze
ignorowana – może być wartością typu NULL. Tym niemniej dla każdego z tych typów triggerów
akcja może zostać przerwana poprzez wymuszenie komunikatu błędu (raise error).
Efekty uboczne w fazie BEFORE
Triggery w fazie BEFORE mogą mieć dwa różne efekty uboczne:
•
•
W przypadku wszystkich wydarzeń - jeżeli procedura triggera zwróci wartość NULL, to dana
czynność ma zostać anulowana i dalsze triggery związane z tą czynnością nie zostaną
wywołane. Często mówi się, że taki trigger wetuje jakąś operację.
W przypadku wydarzeń UPDATE i INSERT procedura triggera może zwrócić nową zawartość
rekordu i wtedy ta właśnie wartość (a nie ta wynikająca z parametrów instrukcji UPDATE lub
INSERT INTO) znajdzie się w bazie.
Macierz właściwości
wydarzenie/f
BEFORE
aza
•
•
INSERT
•
•
•
•
UPDATE
•
AFTER
Możliwe jest zawetowanie
wstawienia.
Wstawiane wartości dostępne są w
tablicy NEW.
Modyfikacja zawartości tablicy NEW
powoduje wstawienie
zmodyfikowanych danych.
W przypadku braku weta funkcja
powinna zwrócić NEW.
Możliwe jest zawetowanie
uaktualnienia.
Aktualne wartości dostępne są w
tablicy OLD a nowe w tablicy NEW.
Modyfikacja zawartości tablicy NEW
powoduje wstawienie
zmodyfikowanych danych.
10
BAZY DANYCH, laboratorium nr 2 , A. Bujnowski
•
•
•
•
Dane są już fizycznie wstawione
do tabeli.
Wstawione dane znajdują się w
tablicy NEW.
Dane są już fizycznie
zmodyfikowane w tabeli.
Poprzednie wartości dostępne
są w tablicy OLD a aktualne w
tablicy NEW.
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
•
W przypadku braku weta funkcja
powinna zwrócić NEW.
•
Możliwe jest jedynie zawetowanie
usunięcia.
Nie ma możliwości uaktualnienia
zamiast skasowania.
Pola kasowanego rekordu dostępne
są w tablicy OLD
•
DELETE
•
•
•
Rekord jest już fizycznie
skasowany
Pola skasowanego rekordu
dostępne są w tablicy OLD
Transakcje
Transakcja pozwala na zgrupowanie pewnego ciągu zdarzeń w bazie danych w jedną nierozerwalną
całość. Każda transakcja spełnia podstawowe reguły określane z j. Angielskiego ACID (patrz
wykład) – niepodzielna, spójna, izolowana , trwała (atomic, consistent,isolated , durable).
Oznacza to, że każda transakcja jako blok danych albo zostanie wykonana w całości, albo wcale –
niezależnie od innych transakcji, system bazodanowy po transakcji będzie spójny i dane po
zakończeniu transakcji będą trwałe – nawet w przypadku awarii systemu zarządzania bazą danych...
Każdą transakcję zaczynamy słowem BEGIN, po czym następuje dowolnie długi ciąg zdarzeń w SQLu i kończymy utrwalając zmiany słowem COMMIT, bądź je odrzucając poprzez ROLLBACK.
Sprawdź jak to działa:
połącz się z bazą danych w dwóch równoległych oknach (wcześniej stwórz klienta, który nie ma nic
wypożyczone i zapamiętaj id)
wpisuj co następuje [o1: - w oknie pierwszym; o2: - w oknie drugim] :
o1: BEGIN;
o1: SELECT * FROM klient ;
o2: SELECT * FROM klient ;
o1: DELETE FROM klient WHERE id_klienta=12; – tu wpisz zapamiętane ID
o2: SELECT * FROM klient ;
o1: SELECT * FROM klient ;
o1: ROLLBACK;
o1: SELECT * FROM klient;
o2: SELECT * FROM klient ;
a teraz spróbuj tego samego z zatwierdzeniem zmian;
o1:
o1:
o2:
o1:
BEGIN;
SELECT * FROM klient ;
SELECT * FROM klient ;
DELETE FROM klient WHERE id_klienta=12;;
11
BAZY DANYCH, laboratorium nr 2 , A. Bujnowski
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
o2:
o1:
o1:
o1:
o2:
SELECT *
SELECT *
COMMIT;
SELECT *
SELECT *
FROM klient ;
FROM klient ;
FROM klient;
FROM klient ;
Blokady tabel przy transakcjach:
o1:
o1:
o2:
o1:
begin;
lock table klient;
select * from klient;
commit;
Zakleszczenia
Przykład:
o1:
o2:
o1:
o2:
o1:
o2:
begin;
begin;
update
update
update
update
klient
klient
klient
klient
set
set
set
set
imie='Jan' where id_klienta=3;
imie = 'Ewa' where id_klienta=4;
imie='Jan' where id_klienta=4;
imie = 'Ewa' where id_klienta=3;
Komentarze:
Komentarze można umieszczać pisząc kod w SQL w dowolnym edytorze tekstowym. Taki komentarz
nie zostanie zinterpretowany przez SZBD, - pozostanie jedynie w pliku tekstowym. Komentarze tego
typu oznacza się dwoma znakami '-' i taki komentarz trwa do końca linii.
np. :
-- to jest komentrarz do SQL
Istnieje również inna metoda zapisu dodatkowej informacji w projekcie. Dzięki możliwości
przechowywania tekstu w bazie można opisać i przechować w ten sposób komentarz do
dowolnego obiektu.
Tego typu komentarze tworzy się jako:
COMMENT ON
{
TABLE object_name |
COLUMN table_name.column_name |
AGGREGATE agg_name (agg_type) |
12
BAZY DANYCH, laboratorium nr 2 , A. Bujnowski
Politechnika Gdańska, międzywydziałowy kierunek „INŻYNIERIA BIOMEDYCZNA”
CONSTRAINT constraint_name ON table_name |
DATABASE object_name |
DOMAIN object_name |
FUNCTION func_name (arg1_type, arg2_type, ...) |
INDEX object_name |
OPERATOR op (leftoperand_type, rightoperand_type) |
RULE rule_name ON table_name |
SCHEMA object_name |
SEQUENCE object_name |
TRIGGER trigger_name ON table_name |
TYPE object_name |
VIEW object_name
} IS 'text'
czyli np.:
COMMENT ON TABLE klient IS ' To jest tabela przechowująca listę
klientów naszego banku';
Sprawdźmy jak to działa:
po wpisaniu komentarza spróbuj zrobić backup bazy danych. Załółżmy, że baza nazywa się XXXX
psql XXXX
COMMENT on ... (jaK WYŻEJ)
\q
pg_dump XXXX > mojabazaXXXX.sql
dropdb XXXX
– teraz możesz obejrzeć plik z wynikami twojej bazy.
Załóż bazę spowrotem:
createdb XXXX
psql XXXX -f mojabazaXXXX.sql
sprawdź tabele (zwłaszcza klient)
Zadania do realizacji:
1. Stwórz funkcję i wyzwalacz, która przy próbie wypożyczenia płyty nie pozwoli wypożyczyć
nowej płyty użytkownikowi, który ma 5 lub więcej niezwróconych pozycji.
2. Opisz wszystkie tabele stosownymi komentarzami
3. Stwórz funkcję, która pozwoli sprawdzić i naliczyć karę użytkownikowi za zbyt długie
przetrzymywanie płyty. Jeżeli przekracza ono 2 tygodnie to za każdy dzień dolicz 10 pln.
4. Wewnątrz transakcji dodaj nowego użytkownika i informację, że jest on pracownikiem,
przewidź rabat 50%.
5. Stwórz w bazie danych nową tabelę o nazwie kary – zastanów się jak ją powiązać z
istniejącą strukturą i jak przechowywane będą dane.
13
BAZY DANYCH, laboratorium nr 2 , A. Bujnowski

Podobne dokumenty