INSERT or UPDATE on PostgreSQL views - sql

I'm starting with PostgreSQL views, since they are useful for my use case and will provide better performance than functions.
(This isn't relevant, but I'm using Django 1.7 on Heroku Postgresā€”just in case).
I have already created a view and can query it fine. I'd like to write a Django wrapper around the view so I can treat it like a table, and query and write to it accordingly. I've been reviewing the
Postgres docs on INSERT and UPDATE for views but to be honest I find their docs so hard to read I can barely parse through what they're saying.
Let's say I have the following view:
CREATE OR REPLACE VIEW links AS
SELECT
listing.id AS listing_id,
CONCAT('/i-', industry.slug, '-j-', listing.slug, '/') AS link,
'https://www.example.com' || CONCAT(industry.slug, '-SEP-', listing.slug, '/') AS full_link,
listing.slug AS listing_slug,
industry.slug AS industry_slug
FROM listing
INNER JOIN company ON company.id = listing.company_id
INNER JOIN industry ON industry.id = company.industry_id
Here, I'm using industry.slug and listing.slug to build links. I'd like to be able to update those two fields from this view, as such:
UPDATE links
SET listing_slug = 'my-new-slug'
WHERE listing_id = 5;
How do I create the rules to do this properly?

Because of the double join, is better for you to use a trigger procedure. To update the industry table, you need first to find the industry.id using the foreign key listing-company-industry.
A procedure could look like so:
CREATE OR REPLACE FUNCTION update_listing_and_industry() RETURNS TRIGGER AS
$$
DECLARE _company_id int; _industry_id int;
BEGIN
_company_id = (SELECT company_id FROM listing WHERE id = OLD.listing_id);
_industry_id = (SELECT industry_id FROM company WHERE id = _company_id);
UPDATE listing SET slug = NEW.listing_slug WHERE id = OLD.listing_id;
UPDATE industry SET slug = NEW.industry_slug WHERE id = _industry_id;
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
NOTE: a trigger procedure is a normal procedure which returns a TRIGGER. Depending on what the trigger do, the procedure must return NEW or OLD (in this case NEW).
And the trigger with the INSTEAD OF UPDATE clause:
CREATE trigger update_view INSTEAD OF UPDATE ON links
FOR EACH ROW EXECUTE PROCEDURE update_listing_and_industry();

The only column in your view, which can be updated is listing_slug.
Updating the other columns is impossible or pointless (e.g. it makes no sense to update industry_slug as there is no primary key for industry in the view).
In such a case you should use a conditional rule which precludes the ability to update other columns.
As it is described in the documentation, there must be an unconditional INSTEAD rule for each action you wish to allow on the view. Therefore you should create dummy INSTEAD rule for update and conditional ALSO rule:
CREATE RULE update_links_default
AS ON UPDATE TO links DO INSTEAD NOTHING;
CREATE RULE update_links_listing
AS ON UPDATE TO links
WHERE NEW.listing_slug <> OLD.listing_slug
DO ALSO
UPDATE listing
SET slug = NEW.listing_slug
WHERE id = OLD.listing_id;
If you'll add column industry.id as industry_id to the view, you can define appropriate rule for table industry:
CREATE RULE update_links_industry
AS ON UPDATE TO links
WHERE NEW.industry_slug <> OLD.industry_slug
DO ALSO
UPDATE industry
SET slug = NEW.industry_slug
WHERE id = OLD.industry_id;
Generally, you need one ALSO rule for a table.

Related

Trigger to update value with no of records

trying to set up a trigger but struggling to it to work the way i want, i want to update a field oppo_pono with the no of opportunity records created for a particular company record
so 1 company can have multiple opportunities and i want to record the no of master opportunities created for a company, so the first master opp created for a company would be set to 1 and so on
ive set the trigger up below but its setting the oppo_pono with the count from all companies rather then the one i am creating the opportunity for
my trigger below
USE [CRM]
GO
/****** Object: Trigger [dbo].[GeneratePNo] Script Date: 1/7/2021 3:55:27 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[GeneratePNo]
ON [dbo].[Opportunity]
FOR insert
AS
declare #OppPrimary Int
declare #company Int
declare #compid Int
declare #type nvarchar(40)
declare #childopp nchar(1)
declare #pono int
Select #OppPrimary = Oppo_OpportunityId,
#company = Oppo_PrimaryCompanyId,
#compid = comp_companyid,
#type = Oppo_Type,
#childopp = oppo_childoppo,
#pono = oppo_pono
FROM Inserted inner join company on Oppo_PrimaryCompanyId = #compid
Begin
UPDATE [Opportunity] SET oppo_pono = (select count(*) from vSearchListOpportunity where Oppo_Deleted is null and #type = 'Master' and Oppo_PrimaryCompanyId = ) +1
WHERE Oppo_OpportunityId =#OppPrimary
End
As mentioned in the comments, you have not taken into account the inserted pseudo-table having multiple rows. You also have a number of outright syntax errors.
EDIT: Following your comments I think I understand what you are trying to do. My original solution will not work here because indexed view cannot have ranking functions, but I have modified it to work with what you need.
Ideally, you wouldn't care about the actual IDs lining up, and just use an IDENTITY column, but often an ID series per group is needed.
Generally a view with correct indexing will be the more performant option, but it depends what you need.
Using a View
I will show you a solution that can be used for a lot of different types of aggregations which normally require triggers. This only works for your problem if you intend to have the numbering change if a row gets deleted out the middle of the grouping. If you want the numbering to remain then use a trigger instead
I am unsure the exact relation of Opportunity, Company and vSearchListOpportunity (seems to be a view on Opportunity) but you should be able to modify this to suit.
Create an indexed view on the data, and include a row number for each row:
CREATE VIEW vOpportunityNumbered
AS
SELECT
o.Oppo_OpportunityId,
o.Oppo_PrimaryCompanyId,
o.Oppo_Type,
o.oppo_childoppo,
ROW_NUMBER() OVER
(PARTITION BY o.Oppo_PrimaryCompanyId ORDER BY o.Oppo_OpportunityId)
-- Order by primary key to get deterministic ordering
FROM Opportunity AS o
WHERE o.Oppo_Deleted IS NULL;
GO
Now, to support this view, we cannot index it directly, as mentioned. We can, however, create an index on the base table that will support it:
CREATE UNIQUE CLUSTERED INDEX opp_CompanyOpportunity
ON Opportunity (Oppo_PrimaryCompanyId, Oppo_OpportunityId)
-- note the ordering of the columns
INCLUDE (Oppo_Type, oppo_childoppo)
WITH (OPTIMIZE_FOR_SEQUENTIAL_KEY = ON) -- ONLY FOR SQL2019
;
GO
This view will now give you a sequential row numbering of Opportunity for each distinct Company.
Triggers
If you wish for the IDs to always remain the same no matter what happens to intervening rows, you will need a trigger (i.e. a deleted row will leave a gap in the numbers).
Every trigger has two tables, inserted and deleted, which contain the data that was changed. For update triggers, both tables have data, a row in each for each changed row.
This means that the trigger is executed once per statement, and these tables contain all the relevant rows. You cannot, however, update them directly; you must join the real tables to them.
So let's take a look at how to write a trigger. Again, I'm somewhat guessing as to the relations of the tables:
CREATE OR ALTER TRIGGER [dbo].[GeneratePNo]
ON [dbo].[Opportunity]
AFTER INSERT -- FOR is an alternative syntax, AFTER is more usual
AS
-- No need for BEGIN and END, the whole batch until GO is the trigger
SET NOCOUNT ON; -- Prevent DONE_IN_PROC rowcount messages
IF (NOT EXISTS (SELECT 1 FROM inserted))
RETURN; -- Bail-out early if no rows
-- We do not declare variables because we cannot store multiple rows in variables
UPDATE
o
SET oppo_pono =
ISNULL( --If there are no other rows we would get a null
(SELECT MAX(allO.oppo_pono)
FROM Opportunity allO
-- no need for the following two filters as the oppo_pono needs to be unique anyway
-- where allO.Oppo_Deleted is null and allO.type = 'Master'
WHERE allO.Oppo_PrimaryCompanyId = inserted.Oppo_PrimaryCompanyId
), 0) + 1
FROM inserted i
JOIN Opportunity o ON o.Oppo_OpportunityId = i.Oppo_OpportunityId;
-- We join inserted table on primary key always
GO
There are more efficient ways to write that update, but it depends whether you are inserting a lot of rows. An INSTEAD OF trigger will also be more performant here, but I haven't attempted that as I don't have your table definition.

SQL Transaction Set Temporary Value

I am relatively new to databases and SQL, and I am not clear on how or whether transactions may relate to a problem that I am trying to solve. I want to be able to temporarily set a value in a database table, run some query, and then clear out the value that was set, and I don't want any operations outside of the transaction to be able to see or alter the temporary value that was set.
The reason I am doing this is so that I can create predefined views that query certain data depending on variables such as the current user's id. In order for the predefined view to have access to the current user's id, I would save the id into a special table just before querying the view, then delete the id immediately afterward. I don't want to worry about some other user overwriting the current user's id while the transaction is in process. Is this a proper use for a transaction?
I am using H2 if that makes a difference.
SET #tempVar=value;
I don't know if you really need to go through the pain of creating a temp table and setting the value. This seems far simpler.
You can then do - SELECT * FROM TABLE_NAME WHERE COLUMN=#tempVar;
I think you want a Procedure or Function. Both can take a parameter as input.
ex.
CREATE PROCEDURE pr_emp
(
#input INT
)
AS
SELECT *
FROM myTable
WHERE emp_id = #input
ex.
CREATE FUNCTION v_empid (#input INT)
RETURNS TABLE
AS
RETURN
SELECT * FROM myTABLE WHERE emp_id = #input;
These could let you to access information for an empid. For example:
SELECT * FROM v_empid(32)

UPDATE used ROWID or ROWNUM

I want to update CAR_CASE from CAR when I add a new row in HIRE used trigger
create or replace TRIGGER HIRE_CAR_CASE_UPDATE
AFTER INSERT OR UPDATE OF CAR_ID ON HIRE
REFERENCING OLD AS OLD NEW AS NEW
BEGIN
UPDATE CAR SET CAR_CASE =
(SELECT HIRE.CAR_CASE FROM HIRE where HIRE.CAR_ID = CAR.CAR_ID and TO_DATE (HIRE.DATE_) = TO_DATE(sysdate))
WHERE rowid = :NEW.ROWID;
END;
It appears that what you wanted to do was
CREATE OR REPLACE TRIGGER HIRE_CAR_CASE_UPDATE
AFTER INSERT OR UPDATE OF CAR_ID ON HIRE
FOR EACH ROW
BEGIN
UPDATE CAR c
SET c.CAR_CASE = :NEW.CAR_CASE
WHERE c.CAR_ID = :NEW.CAR_ID;
END HIRE_CAR_CASE_UPDATE;
Because I don't know what your database looks like or how it's to be used I can't say if a similar update should be made to the "old" CAR referred to by :OLD.CAR_ID.
Perhaps more to the point, however - this is business logic which really should not be implemented in a trigger. Put this kind of logic in your application, or perhaps put it into a stored procedure which performs all the necessary logic for a particular business task, rather than scattering logic willy-nilly among a bunch of triggers, procedures, individual SQL statements, etc.
Best of luck.

update multiple tables with trigger

I want to update two tables when a user wants to update a view.
create trigger update_mID
instead of update of mID on LateRating
for each row
begin
update Movie, Rating
set mID = new.mID
where mID = Old.mID;
end;
I want to update bot the Movie relation and the Rating relation, however, I have not yet experienced a trigger that is able to update multiple tables. Can someone please indicate how I can overcome this?
UPDATE: This is for a exercise to test my trigger scripting skills. The requirement is that I have to write it in one trigger query. #CL. I tried putting two update statements between the begin and end keywords, however, it says that there is a syntax error.... is there a specific way to put two updates between the begin and end?
A single UPDATE statement can modify only a single table.
Use two UPDATEs:
UPDATE Movie SET mID = NEW.mID WHERE mID = OLD.mID;
UPDATE Rating SET mID = NEW.mID WHERE mID = OLD.mID;
You could do a REPLACE INTO statement like the following:
DROP TRIGGER IF EXISTS `update_mID`;CREATE DEFINER=`USER`#`localhost` TRIGGER
`update_mID` AFTER UPDATE ON `tblname` FOR EACH ROW REPLACE INTO
USER_DATABASENAME.TBLNAME (COLUMNNAME1,COLUMNNAME1) SELECT COLUMNNAME1,COLUMNNAME1
FROM USER_DBNAME.TBLNAME
This can even be two separate databases like the example below:
DROP TRIGGER IF EXISTS `update_mID`;CREATE DEFINER=`USER`#`localhost` TRIGGER
`update_mID` AFTER UPDATE ON `tblname from DB1` FOR EACH ROW REPLACE INTO
USER_DATABASENAME1.TBLNAMEDB2 (COLUMNNAME1,COLUMNNAME1) SELECT
COLUMNNAME1,COLUMNNAME1 FROM USER_DBNAME2.TBLNAME

PostgreSQL dynamic table access

I have a products schema and some tables there.
Each table in products schema has an id, and by this id I can get this table name, e.g.
products
\ product1
\ product2
\ product3
I need to select info from dynamic access to appropriate product, e.g.
SELECT * FROM 'products.'(SELECT id from categories WHERE id = 7);
Of course, this doesn't work...
How I can do something like that in PostgreSQL?
OK, I found a solution:
CREATE OR REPLACE FUNCTION getProductById(cid int) RETURNS RECORD AS $$
DECLARE
result RECORD;
BEGIN
EXECUTE 'SELECT * FROM ' || (SELECT ('products.' || (select category_name from category where category_id = cid) || '_view')::regclass) INTO result;
RETURN result;
END;
$$ LANGUAGE plpgsql;
and to select:
SELECT * FROM getProductById(7) AS b (category_id int, ... );
works for PostgreSQL 9.x
If you can change your database layout to use partitioning instead, that would probably be the way to go. Then you can just access the "master" table as if it were one table rather than multiple subtables.
You could create a view that combines the tables with an extra column corresponding to the table it's from. If all your queries specify a value for this extra column, the planner should be smart enough to skip scanning all the rest of the tables.
Or you could write a function in PL/pgSQL, using the EXECUTE command to construct the appropriate query after fetching the table name. The function can even return a set so it can be used in the FROM clause just as you would a table reference. Or you could just do the same query construction in your application logic.
To me, it sounds like you've a major schema design problem: shouldn't you only have one products table with a category_id in it?
Might you be maintaining the website mentioned in this article?
http://thedailywtf.com/Articles/Confessions-The-Shopping-Cart.aspx