Hi All,
Short question:
What's the best way (or your way) of facilitating an updatable view (a
view that will accept insert, delete and update, propagating changes
back to the underlying tables), especially for columns that are joined
to satisfy normalization?
Long version of question:
Properly normalizing a database means (in part) moving to their own
table any columns that contain mostly nulls. I can create a view to
join that information for the user to see. I can use "instead of"
triggers on the view to redirect insert, delete and update events on
the views to instead change the underlying tables. What's the best way
to do this? I have developed a method, but wonder if there's a better,
standard or popular approach.
Detailed example:
Let's say I have a table of refunds I receive from orders I make to
suppliers, such as when they can't source any more stock and cancel
the order. Each refund will have a primary key ID, the [Order ID] of
the original Order, the Date the refund occurred, and the Reason for
the refund.
Some refunds will also have note of the Paying method (an integer
numeration for Credit Card, Account, Direct Deposit, Cash, or PayPal
etc) if the paying method differs from the default (how I paid them
when ordering). Since most Refunds will be Paying via the default
method, only a few need to be recorded.
Similarly, the refund is normally the full amount of the original
order (ie the default). In some cases, it may only be a partial refund
(eg not all items in that order cancelled) or more than the original
amount. For those few refunds where the amount paid differs from the
default, I need to record the amount. The default amounts don't need
to be recorded.
So, I need an [Orders Refunds] table containing all the fields that
are always used. I need a separate [Orders Refunds Paying] table for
recording the ID and Paying method of only those refunds that are not
the default. I need a separate [Orders Refunds Amount] table to record
the amount if it is not the default value (ie full refund).
The tables look like this:
create table [Orders Refunds]
(
ID integer primary key
, [Order ID] --> Orders.ID
, Date date -- date stored as a real
, Reason text collate nocase
)
;
create table [Orders Refunds Paying] -- only insert in here if not
same as when purchased
(
ID integer primary key --> [Orders Refunds].ID
, Paying integer --> Paying.ID
)
;
create table [Orders Refunds Amount] -- only insert if Amount not same
as full purchase
(
ID integer primary key --> [Orders Refunds].ID
, Amount real -- money amount in dollars and decimal
cents
)
;
Incidentally, the Paying table is just a lookup list and looks like
this:
CREATE TABLE Paying
(
ID integer primary key
, Label text unique
)
;
Of course, it's often useful or simplest (eg when a human is looking
at the refunds) to be able to see all the columns joined together,
converting the Date into a readable YYYY-MM-DD string and just showing
null (ie blank) for missing (default) Paying and Amount values. I can
easily create a view to join the three tables together, like this:
create view [Orders Refunds Joined]
as
select
[Orders Refunds].ID as ID
, [Order ID]
, date(Date, 'localtime') as Date
, Reason
, Paying.Label as Paying
, Amount
from [Orders Refunds]
left join [Orders Refunds Paying] on [Orders Refunds].ID = [Orders
Refunds Paying].ID
left join [Orders Refunds Amount] on [Orders Refunds].ID = [Orders
Refunds Amount].ID
left join Paying on [Orders Refunds Paying].Paying = Paying.ID
;
Note above that I'm also converting the stored Date format (real
absolute date) into a more readable Date string adjusted for localtime
(eg '2008-02-06').
Now, if the user inserts a row into the [Orders Refunds Joined] view,
I want the database to redirect to instead insert in the three joined
tables, where needed. It will always insert into [Orders Refunds], but
will only insert into [Orders Refunds Paying] if there's an entry in
the Paying column (ie not null), and only insert into [Orders Refunds
Amount] if an Amount is given (ie not null). The new row in each of
the three tables should use the same ID, so they ar related. To do so,
I created this trigger:
create trigger [Insert Orders Refunds Joined]
instead of insert
on [Orders Refunds Joined]
for each row
begin
insert into [Orders Refunds]
(
ID
, [Order ID]
, Date
, Reason
)
select
new.ID
, new.[Order ID]
, julianday( new.Date, 'utc' ) -- convert date from string in
localtime to real
, new.Reason
;
insert into [Orders Refunds Paying]
(
ID
, Paying
)
select
last_insert_rowid()
, ( select ID from Paying where Label = new.Paying )
where new.Paying not null
;
insert into [Orders Refunds Amount]
(
ID
, Amount
)
select
last_insert_rowid()
, new.Amount
where new.Amount not null
;
end
;
Similarly, if the user deletes from the [Orders Refunds Joined] view,
I want to instead delete the related rows from [Orders Refunds], which
should in turn propagate deletes from [Orders Refunds Paying] and
[Orders Refunds Amount]. So my delete triggers are:
create trigger [Delete Orders Refunds Joined]
instead of delete
on [Orders Refunds Joined]
for each row
begin
delete from [Orders Refunds]
where old.ID = [Orders Refunds].ID
;
end
;
create trigger [Delete Orders Refunds]
before delete
on [Orders Refunds]
begin
delete from [Orders Refunds Paying]
where old.ID = [Orders Refunds Paying].ID
;
delete from [Orders Refunds Amount]
where old.ID = [Orders Refunds Amount].ID
;
end
;
So that takes care of insert and delete on the view. Lastly I need to
take care of any updates on the view, enacting changes instead on the
underlying joined tables. In the case of the fields in the [Orders
Refunds] table, that's fairly easy. I just have to create a trigger
for each field ([Order ID], Date, Reason) to update the corresponding
table field instead of the view field. The only tricky bit is
reversing the date string (in localtime) back to a real (in utc). So
the triggers are:
create trigger [Update Orders Refunds Joined Order ID]
instead of update of [Order ID]
on [Orders Refunds Joined]
for each row
begin
update [Orders Refunds]
set [Order ID] = new.[Order ID]
where [Orders Refunds].ID = new.ID
;
end
;
create trigger [Update Orders Refunds Joined Date]
instead of update of Date
on [Orders Refunds Joined]
for each row
begin
update [Orders Refunds]
set Date = julianday( new.Date, 'utc' )
where [Orders Refunds].ID = new.ID
;
end
;
create trigger [Update Orders Refunds Joined Reason]
instead of update of Reason
on [Orders Refunds Joined]
for each row
begin
update [Orders Refunds]
set Reason = new.Reason
where [Orders Refunds].ID = new.ID
;
end
;
Now for updating the Paying and Amount fields. Again, instead of the
view, I want to update the corresponding joined table for each. Paying
has to convert a text label to the related integer value. Because the
[Orders Refunds Paying] and [Orders Refunds Amount] tables are
designed to only contain a row if their value is not null, I have to:
1. Insert a row if it doesn't already exist for that ID, or
2. Delete the row if the value has changed to null, or
3. Simply update the value if the row exists and the new value is not
null.
So I have one trigger for updates to Paying, a second trigger for
updates to Amount, each of which performs either an insert, delete or
update:
create trigger [Update Orders Refunds Joined Paying]
instead of update of Paying
on [Orders Refunds Joined]
for each row
begin
-- insert if row doesn't exist and new value not null
insert into [Orders Refunds Paying]
(
ID
, Paying
)
select
new.ID
, (select ID from Paying where Label = new.Paying)
where new.Paying not null
and new.ID not in ( select ID from [Orders Refunds Paying] )
;
-- delete if row exists and new value is null
delete from [Orders Refunds Paying]
where new.Paying is null
and [Orders Refunds Paying].ID = new.ID
;
-- update if row exists and new value not null
update [Orders Refunds Paying]
set Paying = (select ID from Paying where Label = new.Paying)
where new.Paying not null
and [Orders Refunds Paying].ID = new.ID
;
end
;
create trigger [Update Orders Refunds Joined Amount]
instead of update of Amount
on [Orders Refunds Joined]
for each row
begin
-- insert if row doesn't exist and new value not null
insert into [Orders Refunds Amount]
(
ID
, Amount
)
select
new.ID
, new.Amount
where new.Amount not null
and new.ID not in ( select ID from [Orders Refunds Amount] )
;
-- delete if row exists and new value is null
delete from [Orders Refunds Amount]
where new.Amount is null
and [Orders Refunds Amount].ID = new.ID
;
-- update if row exists and new value not null
update [Orders Refunds Amount]
set Amount = new.Amount
where new.Amount not null
and [Orders Refunds Amount].ID = new.ID
;
end
;
So, is this the best way to tackle the objective of having updatable
views? Or is there a better way? Are there any bugs in my approach?
Any feedback appreciated.
Thanks,
Tom
BareFeet
_______________________________________________
sqlite-users mailing list
[email protected]
http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-users