Part 1: Comparison between NoPlsql and SmartDB approach
Part 2: SmartDB, how to do it?
Also a pdf-version of both slide-decks:
Part 1+2 as one pdf.
Enjoy.
Toon
About how database web application development nowadays has gone absolutely and utterly absurd.
May 2008 - in Helsinki - a new approach for designing and implementing database web applications was presented. It was there and then declared to be "the only right way to build these kind of applications."
(New visitors: start here.)


create or replace package x_ctx asThe context that requires the package to populate it:
procedure p_set(p_namespace in varchar2
,p_attribute in varchar2
,p_value in varchar2);
end;
/
create or replace package body x_ctx as
procedure p_set(p_namespace in varchar2
,p_attribute in varchar2
,p_value in varchar2) as
begin
--
dbms_session.set_context(p_namespace,p_attribute,p_value);
--
end;
end;
/
create context bookorder_ctx using x_ctxAnd finally the UI-interface procedure:
/
create or replace procedure p_bookorder_ctxNow all that's left is to develop a view that uses a pipelined table function to retrieve the rows. A pipelined table function requires the creation of two types: a type that describes the structure of the rows that are being returned by that function, and on top of that, a table type. Here they are:
(p_username in varchar2
,p_ean in varchar2
,p_book_string in varchar2) as
begin
--
-- Put info in v$context.
--
x_ctx.p_set('BOOKORDER_CTX','USERNAME',p_username);
x_ctx.p_set('BOOKORDER_CTX','EAN',p_ean);
x_ctx.p_set('BOOKORDER_CTX','BOOK_STRING',p_book_string);
--
end;
/
create type f_bookorder_t is objectThese types are then used in the code of the pipelined table function:
(username varchar2(10)
,ean varchar2(9)
,title varchar2(95)
,amount number
,new_amount number)
/
create type f_bookorder_table_t as table of f_bookorder_t;
/
create or replace function tf_bookorderNote that I'm again hardcoding binds into the sql-text of the ref-cursor, which is of course a bad idea. The purpose here is to show the pipelined table function concept. Tim Hall has a great article here that demonstrates how the values available inside the context can be readily reused as binds, and (this is what makes it great as far as I'm concerned) at the same time preventing having to code different 'open ref-cursor for ... using ...' statements.
(p_username in varchar2
,p_ean in varchar2
,p_book_string in varchar2) return f_bookorder_table_t pipelined as
--
type refcursor is ref cursor;
c1 refcursor;
r1 f_bookorder_t;
pl_where varchar2(2000);
begin
--
if p_username is null
then
return;
elsif
p_ean is not null
then
pl_where := 'and o.username = '''||p_username||''''||'
and b.ean = '''||p_ean||'''';
elsif
p_book_string is not null
then
pl_where := 'and o.username = '''||p_username||''''||'
and upper(o.ean||'' ''||b.author||'' ''||b.title||'' / ''||b.publisher) like ''%''||'''||upper(p_book_string)||'''||''%''';
else
pl_where := 'and o.username = '''||p_username||'''';
end if;
--
open c1 for
'select f_bookorder_t
('''||p_username||'''
,b.ean
,o.ean||'' ''||b.author||'' ''||b.title||'' / ''||b.publisher
,o.amount
,to_number(null)) as bookorder
from book b
,bookorder o
where b.ean = o.ean '
||pl_where;
--
fetch c1 into r1;
--
while c1%FOUND
loop
--
pipe row(r1);
--
fetch c1 into r1;
--
end loop;
--
close c1;
--
return;
--
exception when no_data_needed
then close c1;
return;
end;
/
create or replace force view v_bookorder asWe invoke the table function in the FROM clause while at the same time supplying it with the three values that have been set into the context.
select o.username
,o.ean
,o.title
,o.amount
,o.new_amount
from table(tf_bookorder(sys_context('BOOKORDER_CTX','USERNAME')
,sys_context('BOOKORDER_CTX','EAN')
,sys_context('BOOKORDER_CTX','BOOK_STRING'))) o
/
So let's suppose the technology used for coding the UI-layer likes it when it gets an XML document that holds the rows that are to be displayed. Here's the procedure for a contract that returns the XML as a Clob:create or replace procedure get_xmlWe could of course also return a true XMLType value, by simply casting the Clob to XMLtype prior to returning it. Alternatively we could have built an XMLType value straight from the start by using all the XML function support available to us inside the DBMS.
(p_username in varchar2
,p_ean in varchar2
,p_book_string in varchar2
,p_xml out CLOB) as
--
type order_t is record
(username varchar2(10)
,ean varchar2(9)
,title varchar2(95)
,amount number
,new_amount number);
r1 order_t;
type refcursor is ref cursor;
c1 refcursor;
pl_where varchar2(2000);
begin
--
if p_username is null
then
return;
elsif
p_ean is not null
then
pl_where := 'and o.username = '''||p_username||''''||'
and o.ean = '''||p_ean||'''';
elsif
p_book_string is not null
then
pl_where := 'and o.username = '''||p_username||''''||'
and upper(o.ean||'' ''||b.author||'' ''||b.title||'' / ''||b.publisher) like ''%''||'''||upper(p_book_string)||'''||''%''';
else
pl_where := 'and o.username = '''||p_username||'''';
end if;
--
open c1 for
'select o.username
,o.ean
,substr(o.ean||'' ''||b.author||'' ''||b.title||'' / ''||b.publisher,1,95) as title
,o.amount
,to_number(null) as new_amount
from book b
,bookorder o
where b.ean = o.ean '
||pl_where;
--
fetch c1 into r1;
--
p_xml := ''||chr(10); ');
--
while c1%FOUND
loop
--
dbms_lob.append(p_xml,''||chr(10);
'||chr(10));
dbms_lob.append(p_xml,''||r1.username||' '||chr(10));
dbms_lob.append(p_xml,''||r1.ean||' '||chr(10));
dbms_lob.append(p_xml,''||r1.title||' '||chr(10));
dbms_lob.append(p_xml,''||r1.amount||' '||chr(10));
dbms_lob.append(p_xml,''||r1.new_amount||' '||chr(10));
dbms_lob.append(p_xml,'
--
fetch c1 into r1;
--
end loop;
--
close c1;
--
dbms_lob.append(p_xml,'
--
end;
/
create assertion no_more_than_50_per_user as
check(not exists(select 'a user with more than 50 books'
from (select username,sum(amount) as total
from orderline
group by username)
where total > 50)
);
create assertion no_more_than_80_per_title as
check(not exists(select 'a title with more than 80 ordered'
from (select ean,sum(amount) as total
from orderline
group by ean)
where total > 80)
);
create or replace procedure get_rows_page_1
(p_username in varchar2
,p_ean in varchar2
,p_book_string in varchar2
,p_cursor out sys_refcursor) is
--
begin
--
if p_username is null
then
--
open p_cursor for
select bo.username
,bo.ean
,substr(bo.ean||' '||b.author||' '||b.title||' / '||b.publisher,1,95) as title
,bo.amount
,to_number(null) as new_amount
from book b
,bookorder bo
where b.ean = bo.ean
and 1 = 0; -- ***** NO USER INFO ************************
--
else
--
if p_ean is null and p_book_string is null
then
--
open p_cursor for
select bo.username
,b.ean
,substr(bo.ean||' '||b.author||' '||b.title||' / '||b.publisher,1,95) as title
,bo.amount
,to_number(null) as new_amount -- **** NOTE: ALSO RETURNING THE UPDATE-ITEM *********
from book b
,bookorder bo
where b.ean = bo.ean
and bo.username = p_username; -- ***** NO ADDITIONAL FILTERS ************************
--
elsif p_ean is not null and p_book_string is null
then
--
open p_cursor for
select bo.username
,b.ean
,substr(bo.ean||' '||b.author||' '||b.title||' / '||b.publisher,1,95) as title
,bo.amount
,to_number(null) as new_amount
from book b
,bookorder bo
where b.ean = bo.ean
and bo.username = p_username
and bo.ean = p_ean; -- **** ONLY EAN FILTER ************************
--
elsif p_ean is null and p_book_string is not null
then
--
open p_cursor for
select bo.username
,b.ean
,substr(bo.ean||' '||b.author||' '||b.title||' / '||b.publisher,1,95) as title
,bo.amount
,to_number(null) as new_amount
from book b
,bookorder bo
where b.ean = bo.ean
and bo.username = p_username
and(b.title like '%'||p_book_string||'%' -- **** ONLY TITLE STRING FILTER ************************
or b.author like '%'||p_book_string||'%'
or b.publisher like '%'||p_book_string||'%');
--
else /* both filters not null */
--
open p_cursor for
select bo.username
,b.ean
,substr(bo.ean||' '||b.author||' '||b.title||' / '||b.publisher,1,95) as title
,bo.amount
,to_number(null) as new_amount
from book b
,bookorder bo
where b.ean = bo.ean
and bo.username = p_username
and bo.ean = p_ean
and(b.title like '%'||p_book_string||'%' -- **** BOTH FILTERS ************************
or b.author like '%'||p_book_string||'%'
or b.publisher like '%'||p_book_string||'%');
--
end if;
--
end if;
--
end;
/
toon,
Suppose we have a registration form on our web site, and the web guys want to check if the username supplied in the form is already in use.
This would be a stored procedure in my UI-api, right ? (one in parameter and a return value to represent true or false)
create table web_userIn addition to that I would introduce a table to hold “in-use check” calls.
(userid number not null
,username varchar2(30) not null check(username = upper(username))
,...more columns...
,primary key (userid)
,unique(username)
);
create table web_user_inuse_checkThe meaning of this table would be: “username [username] was found to be not in use when checked by session [sessionid] at [checktime].” The sessionid would likely map to the value of the http sesssion cookie that is maintained on the middle tier for every connected browser.
(sessionid varchar2(...) not null
,username varchar2(30) not null check(username = upper(username))
,checktime date not null
,primary key (sessionid)
,unique (username)
);
create assertion no_overlap asWhich would be implemented separately using triggers on both tables (not shown in this post).
check(not exists (select username from web_user
intersect
select username from web_user_inuse_check));
procedure p_check_already_usedAnd finally the user-creation procedure.
(p_username in varchar2
,p_sessionid in varchar2
,p_inuse out varchar2) as
begin
--
-- Serialize calls to this procedure.
--
Appropriate dbms_lock call to serialize *per username*.
Will raise exception if lock could not be acquired.
--
-- Possible scenarios:
-- 1) username already in use.
-- 2) username not in use and also no inuse_check available.
-- 3) username not in use and inuse_check available by other session.
-- 3a) inuse_check is more than 15 minutes ago.
-- 3b) inuse_check is 15 minutes or less ago.
-- 4) username not in use and inuse_check available by same session.
--
If [scenario 1]
Then p_inuse := ‘YES’
Elsif [scenario 2]
Then create (using merge on sessionid) inuse_check row for this session;
P_inuse := ‘NO’;
Elsif [scenario 3a]
Then delete inuse_check row of other session;
Create (merge) inuse_check row for this session;
P_inuse := ‘NO’;
Elsif [scenario 3b]
Then p_inuse := ‘YES’;
Elsif [scenario 4]
Then P_inuse := ‘NO’;
End if;
--
-- Commit transaction (also releases dbms_lock lock).
--
Commit;
--
Exception when “lock could not be acquired”
--
P_inuse := ‘YES’;
--
end;
procedure p_create_userI know this may seem an overly complex solution. And I would probably check with the customer first whether dealing with the “concurrency scenarios” is really necessary or not.
(p_sessionid in varchar2
,p_username in varchar2
,other user data) as
Begin
--
If “Our (sessionid!) inuse_check row has disappeared”
Then -- It took us more than 15 minutes to fill in the form, and
-- some other session has claimed this username now.
--
-- Quickly check if we can claim it again.
--
If p_check_already_used = ‘YES’
Then error
End if;
End if;
--
-- Now register our user.
--
Delete our inuse_check row;
Insert web_user row;
Commit;
--
End;
Going back to the picture I've shown you before:
What this means is that we map the DBMS like this:
During the presentation I then mention that inside the Java realm only two classes should be allowed: a Row class and a TableOfRows class. In Helsinki, Java is used only to generate UI: it receives the row or table-of-rows to be displayed from the DBMS. All it needs to do is display the data in a user friendly way. Needless to say that this approach completely and utterly disarms the Java developer(s). Your relationship with them turns into a love/hate relationship:
It should not suprise you that this approach completely turns around the main two disadvantages I mentioned earlier when building WoD applications in a mid-tier centric way. Designing and programming a WoD application in the Helsinki way, will greatly reduce the number of roundtrips to the DBMS.
I cannot repeat this enough: if the database design fits your WoD application needs, the Helsinki approach will always outperform any other architecture. Adversaries often bring up the argument, that by pushing 'application logic' (which they never clearly define) into the DBMS layer, that layer will immediately become the bottleneck. I have two responses to this argument.
I have experienced this first-hand. One of my customers had built a WoD application around 2002 in a database-centric Helsinki manner. They used the then hot JDeveloper release with its accompanying MVC frameworks: UIX, BC4J and Struts. Two years ago they were faced with a long overdue upgrade of the JEE technology stack: upgrade to newest release of application server, and newest release of JDeveloper with its current versions of the accompanying MVC frameworks. After having spent six months trying to get this done, it was decided to just rebuild all pages in Application Express (the now sexy du-jour UI-technology). Since no DL and BL logic sat outside the data tier, this was achieved in just over two months by four people, without any change in all BL and DL code that sat inside the DBMS.
I think that could be called agile, not?
A collegue of mine back then coined the phrase: UI-code is throw-away code. I could not agree more. It is the top of the technology stack where all the volatility sits, and continuous change happens. Use it thinly and you get agility.