Code snippets, ideas and events from IT related projects

by Robert Gawron

Jak zrobić mini CMS w całości w PL/pgSQL?

27.05.2008 16:07, Posted in , 3 comments, 0 pingbacks

Słowa klucze (podlinkowane do angielskiej Wikipedii): Apache, CGI, PostgreSQL, PL/pgSQL.

W poprzedniej pracy kiedyś szef powiedział, że gdyby miał czas to zrobił by aplikacje webową w całości w plpgsql. Pomysł jest o tyle oryginalny, że jest to język przeznaczony do obsługi bazy danych [uogólniając: by tworzyć lepsze zapytania niż w czystym SQL'u].

Post jest o tym, jak wykonać bardzo prostą aplikację webową, tak właśnie działającą. Jedyne co ona robi, to wyświetla userowi stronę, którą podał jako argument w URL'u. Cały kod to funkcje plpgsql'a wciągnięte do bazy. Dane (arkusz styli i strony) również znajdują się w bazie.

Jedynym plikiem na dysku jest skrypt powłoki, który wywołuje poprzez psql (konsolowy klient postgresa) funkcję index() z parametrem, który został przesłany w URL'u. Funkcja index() generuje stronę.

Do dzieła: przechodzimy do /etc/apache2 i logujemy się na roota (sudo bash). W sites-available tworzymy plik web-sql o zawartości:

NameVirtualHost 127.0.0.1:8888
<VirtualHost 127.0.0.1:8888>
        ServerAdmin webmaster@localhost

        DocumentRoot /opt/websql
        <Directory />
                Options FollowSymLinks
                AllowOverride None
        </Directory>
        <Directory /opt/websql>
                Options Indexes FollowSymLinks MultiViews ExecCGI
                AllowOverride None
                Order allow,deny
                allow from all
                # This directive allows us to have apache2's default start page
                # in /apache2-default/, but still have / go to the right place
                #RedirectMatch ^/$ /apache2-default/
        </Directory>

        ErrorLog /var/log/apache2/error.log

        # Possible values include: debug, info, notice, warn, error, crit,
        # alert, emerg.
        LogLevel warn

        CustomLog /var/log/apache2/access.log combined
        ServerSignature On

</VirtualHost>

W katalogu /etc/apache2/sites-enabled robimy link do utworzonego pliku. Po wpisaniu ls -la powinno to wyglądać tak:

lrwxrwxrwx 1 root root   26 2008-05-26 00:00 web-sql -> ../sites-available/web-sql

Utworzyliśmy wirtualny serwer działający na adresie 127.0.0.1 na porcie 8888, którego pliki są w /opt/websql. Przechodzimy więc do /opt i tworzymy tam katalog websql.

W katalogu tym tworzymy (ta nazwa jest dowolna ale pojawia się też później) main.cgi - skrypt powłoki o którym mówiliśmy na początku. Jego zawartość:

#!/bin/sh
echo "select index('$QUERY_STRING')" | psql  -U userszybkosci -t --no-align -d testszybkosci

Opcja -t sprawia, iż nie są wyświetlane informacje o ilości zwróconych wierszy, o nazwach kolumn, itp. Z kolei --no-align wycina spacje, które są wstawiane, by kolumny nie były 'poszarpane'. Obie te opcje sprawiają razem, iż zwracane jest tylko to, co jest wynikiem zapytań, żadnych dodatkowych info, które popsuły by strukturę dokumentu. Opcja -U oznacza usera bazy danych a -d nazwę bazy (o tym akapit niżej).

Utwórzmy usera, bazę danych i plpgsql na niej. Wykorzystałem to, co opisałem tutaj (createuser, createdb, createlang). Pomijamy tricki, że baza może być na innym hoście albo nasłuchiwać na innym porcie.

Na razie mamy tylko pustą bazę danych, więc utwórzmy tabele. Wpisałem to żywcem, z konsolki więc nie mam tego pliku ale wystarczy zrzut z pg_dump'a (z opcją -s, czyli bez danych), -- oznacza komentarz. W postgresie nie ma auto_increment jak w MySQL, trzeba utworzyć sekwencje i w insercie ja podawać jako id [tak w skrócie] - temu są te sekwencje :)

--
-- Name: atrybut; Type: TABLE; Schema: public; Owner: userszybkosci; Tablespace: 
--

CREATE TABLE atrybut (
    tag integer,
    nazwa text,
    wartosc text
);


ALTER TABLE public.atrybut OWNER TO userszybkosci;

--
-- Name: str_seq; Type: SEQUENCE; Schema: public; Owner: userszybkosci
--

CREATE SEQUENCE str_seq
    INCREMENT BY 1
    NO MAXVALUE
    NO MINVALUE
    CACHE 1;


ALTER TABLE public.str_seq OWNER TO userszybkosci;

--
-- Name: strona; Type: TABLE; Schema: public; Owner: userszybkosci; Tablespace: 
--

CREATE TABLE strona (
    id integer NOT NULL,
    tresc text,
    nazwa_pliku text,
    tytul text
);


ALTER TABLE public.strona OWNER TO userszybkosci;

--
-- Name: tag; Type: TABLE; Schema: public; Owner: userszybkosci; Tablespace: 
--

CREATE TABLE tag (
    id integer NOT NULL,
    nazwa text
);


ALTER TABLE public.tag OWNER TO userszybkosci;

--
-- Name: tag_seq; Type: SEQUENCE; Schema: public; Owner: userszybkosci
--

CREATE SEQUENCE tag_seq
    INCREMENT BY 1
    NO MAXVALUE
    NO MINVALUE
    CACHE 1;


ALTER TABLE public.tag_seq OWNER TO userszybkosci;

--
-- Name: strona_pkey; Type: CONSTRAINT; Schema: public; Owner: userszybkosci; Tablespace: 
--

ALTER TABLE ONLY strona
    ADD CONSTRAINT strona_pkey PRIMARY KEY (id);


--
-- Name: tag_pkey; Type: CONSTRAINT; Schema: public; Owner: userszybkosci; Tablespace: 
--

ALTER TABLE ONLY tag
    ADD CONSTRAINT tag_pkey PRIMARY KEY (id);

Teraz utwórzmy plik, a w nim wklejmy funkcje postgresa:

-- tutaj trafiaja zapytania, ona generuje tresc, taki
-- engine, mozna by dorobic obsluge akcji etc..
CREATE OR REPLACE FUNCTION index(text) RETURNS text AS $$
DECLARE
    nazwa_pliku ALIAS FOR $1;
    DECLARE output TEXT DEFAULT E'Content-Type: text/html\n\n';
BEGIN
    -- zadanie wyslania arkusza styli
    IF nazwa_pliku='style' THEN
        output = output || add_styles();
        RETURN output;
    END IF;
    output = output || '<html><link rel="stylesheet" href="main.cgi?style"/><body>' || 
             page_content(nazwa_pliku) ||
             '</html></body>';
    RETURN output;
END;
$$ LANGUAGE 'plpgsql';


-- zraca style, gotowe do wsadzenia w zanaczniki <style> albo do 
-- wpisanie w plik CSS
CREATE OR REPLACE FUNCTION add_styles() RETURNS text AS $$
DECLARE
    DECLARE record RECORD;
    DECLARE subrecord RECORD;
    DECLARE output TEXT DEFAULT '';
BEGIN
    -- petla po wszystkich tagach w arkuszy styli
    FOR record IN SELECT * FROM tag LOOP
        output:= output || record.nazwa || E' {\n'; -- poczatkowa klamerka
        -- petla po wszytkich atrybutach dla tagu
        FOR subrecord IN SELECT nazwa, wartosc FROM atrybut WHERE tag=record.id LOOP
            output:= output || subrecord.nazwa || ':' || subrecord.wartosc ||  E';\n';
        END LOOP;
        output:= output || E'}\n'; -- koncowa klamerka
    END LOOP;
    -- skoro jestesmy tuta to w zmiennej output mamy gotowy plik css 
    RETURN output;
END;
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION page_content(TEXT) RETURNS text AS $$
DECLARE
    DECLARE output TEXT DEFAULT '';
    klucz ALIAS FOR $1;
    DECLARE r RECORD;
BEGIN
    SELECT INTO r *  FROM strona WHERE nazwa_pliku=klucz;
    IF NOT FOUND THEN
        return 'Podana strona nie istnieje.';
    END IF;
    output = '<h1>tytul: ' || r.tytul || '</h1><hr/>tresc: ' || r.tresc;
    RETURN output;
END;
$$ LANGUAGE plpgsql;
funkcje wciągamy do bazy:
plpgsql -U nazwa_usera -d nazwa_bazy -f nazwa_pliku_z_funkcjami

To tyle, jako root resetujemy apache (/etc/init.d/apache2 restart), dodajemy dane do bazy (artykuły, ew. style CSS). Odpalamy i jest:

cms, postgresql, plpgsql Materiały do pobrania:

Pingbacks

No pingbacks yet

Comments

avatar
mulander , 25.03.2010 18:59, reply

Bardzo ciekawy post. Co ciekawe trafiłem na niego przez Google (pl/pgsql vs pl/sql -port -porting -translation) pomimo iż twojego bloga wcześniej przeglądałem w miarę dokładnie :) Interesuje mnie o jakiej wersji PostgreSQL myślałeś w trakcie pisania tego kodu. Język PL/pgSQL jest dość ciekawy, mnie interesuje głównie ze względu na zbieżności z Oraclowym PL/SQLem. Pierwsze co rzuca mi się w oczy to wykorzystywanie przez ciebie formy ‘ALIAS’ w celu odwoływania się do parametrów funkcji. Od wersji 8.0 nie jest to już konieczne, można zamiast tego stosować formę:

CREATE FUNCTION Simple_Example (my_parameter TEXT) RETURNS TEXT AS $$ BEGIN RETURN my_parameter; END; $$ LANGUAGE plpgsql;

Drugim ciekawym elementem twojego kodu jest poprzedzanie deklaracji zmiennych słowem kluczowym DECLARE:

DECLARE DECLARE record RECORD; DECLARE subrecord RECORD; DECLARE output TEXT DEFAULT ”; BEGIN — petla po wszystkich tagach w arkuszy styli …. END;

Z tego co wyczytałem w dokumentacji, struktura języka oparta jest na blokach o strukturze: DECLARE BEGIN EXCEPTION END;

Twój przykład interpretuje jako DECLARE DECLARE record RECORD; BEGIN NULL; END; DECLARE subrecord RECORD; BEGIN NULL; END; DECLARE output TEXT DEFAULT ”; BEGIN NULL; END; BEGIN — petla po wszystkich tagach w arkuszy styli …. END;

Jestem ciekaw, czy w ten sposób również traktuje ten kod samo PostgreSQL. Według dokumentacji, format bez DECLARE jest również poprawna:

DECLARE record RECORD; subrecord RECORD; output TEXT; BEGIN — petla po wszystkich tagach w arkuszy styli …. END;

Inną ciekawostką jest możliwość kotwiczenia typów, tak więc twój przykład można by przekształcić na:

DECLARE record tag%ROWTYPE; subrecord atrybut%ROWTYPE; output TEXT DEFAULT ”; BEGIN — petla po wszystkich tagach w arkuszy styli …. END;

Zajmowałeś się zawodowo pisaniem aplikacji pod PostgreSQL? Jeżeli tak to jestem ciekaw twojej ogólnej opinii na temat tej bazy i samego języka.

avatar
mulander , 25.03.2010 19:02, reply

Niestety formatowanie uległo małemu uszkodzeniu :P Pozwoliłem sobie przerzucić kod z mojego poprzedniego komentarza na pastebin: http://pastebin.com/EBhYpW7b

avatar
Robert , 26.03.2010 13:33, reply

Cześć,

w PostgreSQL nie pisałem dużo, a do tego było to dawno temu (z czasów jeszcze wcześniejszych niż ta notka) więc merytorycznie to bardzo mało z tego pamiętam/wiem.

Mi ten język przypadł do gustu, jako zwięzły, dość sporo miał też wbudowanych funkcji (też logicznych i spójnych) i dobry manual na stronie projektu. W samej bazie danych głownie podobała mi się budowa (w przeciwieństwie do MySQL) oparta na procesach a nie na wątkach. Z takich drobiazgów (ale miłych) to fajne są te polecenia /dt, /t, /x — szybko się je pisze w konsoli (vide MySQLowe długaśne odpowiedniki).

BTW co myślisz o koncepcji NoSQL?

Leave your reply

Let me know what you think

Required. 30 chars of fewer.

Required.

captcha image