views:

52

answers:

2

Hello All,

I have a SQL Server 2005 database comprised of Customers, Quote, QuoteDetail tables. I want/need to enforce referential integrity such that when an insert is made on QuoteDetail, the quote and customer tables are also affected. I have tried my best to set up primary/foreign keys on my tables but need some help. Here's the scripts for my tables as they stand now (please don't laugh):

Customers:

CREATE TABLE [dbo].[Customers](
    [pkCustID] [int] IDENTITY(1,1) NOT NULL,
    [CompanyName] [nvarchar](50) NULL,
    [Address] [nvarchar](50) NULL,
    [City] [nvarchar](50) NULL,
    [State] [nvarchar](2) NULL,
    [ZipCode] [nvarchar](5) NULL,
    [OfficePhone] [nvarchar](12) NULL,
    [OfficeFAX] [nvarchar](12) NULL,
    [Email] [nvarchar](50) NULL,
    [PrimaryContactName] [nvarchar](50) NULL,

    CONSTRAINT [PK_Customers] 
      PRIMARY KEY CLUSTERED([pkCustID] ASC)
)

Quotes:

CREATE TABLE [dbo].[Quotes](
    [pkQuoteID] [int] IDENTITY(1,1) NOT NULL,
    [fkCustomerID] [int] NOT NULL,
    [QuoteDate] [timestamp] NOT NULL,
    [NeedbyDate] [datetime] NULL,
    [QuoteAmt] [decimal](6, 2) NOT NULL,
    [QuoteApproved] [bit] NOT NULL,
    [fkOrderID] [int] NOT NULL,

    CONSTRAINT [PK_Bids] 
       PRIMARY KEY CLUSTERED ([pkQuoteID] ASC)
) 
GO

ALTER TABLE [dbo].[Quotes] WITH CHECK 
   ADD CONSTRAINT [fkCustomerID] 
   FOREIGN KEY([fkCustomerID]) REFERENCES [dbo].[Customers] ([pkCustID])

QuoteDetail:

CREATE TABLE [dbo].[QuoteDetail](
[ID] [int] IDENTITY(1,1) NOT NULL,
[fkQuoteID] [int] NOT NULL,
[fkCustomerID] [int] NOT NULL,
[fkPartID] [int] NULL,
[PartNumber1] [float] NOT NULL,
[Qty1] [int] NOT NULL,
[PartNumber2] [float] NULL,
[Qty2] [int] NULL,
[PartNumber3] [float] NULL,
[Qty3] [int] NULL,
[PartNumber4] [float] NULL,
[Qty4] [int] NULL,
[PartNumber5] [float] NULL,
[Qty5] [int] NULL,
[PartNumber6] [float] NULL,
[Qty6] [int] NULL,
[PartNumber7] [float] NULL,
[Qty7] [int] NULL,
[PartNumber8] [float] NULL,
[Qty8] [int] NULL,
[PartNumber9] [float] NULL,
[Qty9] [int] NULL,
[PartNumber10] [float] NULL,
[Qty10] [int] NULL,
[PartNumber11] [float] NULL,
[Qty11] [int] NULL,
[PartNumber12] [float] NULL,
[Qty12] [int] NULL,
[PartNumber13] [float] NULL,
[Qty13] [int] NULL,
[PartNumber14] [float] NULL,
[Qty14] [int] NULL,
[PartNumber15] [float] NULL,
[Qty15] [int] NULL,
[PartNumber16] [float] NULL,
[Qty16] [int] NULL,
[PartNumber17] [float] NULL,
[Qty17] [int] NULL,
[PartNumber18] [float] NULL,
[Qty18] [int] NULL,
[PartNumber19] [float] NULL,
[Qty19] [int] NULL,
[PartNumber20] [float] NULL,
[Qty20] [int] NULL,
    CONSTRAINT [PK_QuoteDetail] 
       PRIMARY KEY CLUSTERED([ID] ASC)
)
GO

ALTER TABLE [dbo].[QuoteDetail] WITH CHECK 
  ADD CONSTRAINT [FK_QuoteDetail_Customers] 
  FOREIGN KEY([fkCustomerID]) REFERENCES [dbo].[Customers] ([pkCustID])

ALTER TABLE [dbo].[QuoteDetail] WITH CHECK 
   ADD CONSTRAINT [FK_QuoteDetail_PartList] 
   FOREIGN KEY([fkPartID]) REFERENCES [dbo].[PartList] ([RecID])

ALTER TABLE [dbo].[QuoteDetail] WITH CHECK 
   ADD CONSTRAINT [FK_QuoteDetail_Quotes] 
   FOREIGN KEY([fkQuoteID]) REFERENCES [dbo].[Quotes] ([pkQuoteID])

Your advice/guidance on how to set these up so that customer ID in Customers is the same as in Quotes (referential integrity) and that CustomerID is inserted on Quotes and Customers when an insert is made to QuoteDetial would be much appreciated.

Update: Made table changes, here's the sproc now:

USE [Diel_inventory]
GO
/****** Object:  StoredProcedure [dbo].[AddQuote]    Script Date: 05/08/2010 14:57:28 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER procedure [dbo].[AddQuote] as
Declare @CustID int,
@CompanyName nvarchar(50),
@Address nvarchar(50),
@City nvarchar(50),
@State nvarchar(2),
@ZipCode nvarchar(5),
@Phone nvarchar(12),
@FAX nvarchar(12),
@Email nvarchar(50),
@ContactName nvarchar(50),
@QuoteID int,
@QuoteDate datetime,
@NeedbyDate datetime,
@QuoteAmt decimal,
@ID int,
@QuoteDetailPartID int,
@PartNumber float,
@Quantity int

begin

Insert into dbo.Customers
(CompanyName, Address, City, State, ZipCode, OfficePhone, OfficeFAX, Email, PrimaryContactName)
Values (@CompanyName, @Address, @City, @State, @ZipCode, @Phone, @FAX, @Email, @ContactName)

set @CustID = scope_identity()


Insert into dbo.Quotes 
(fkCustomerID,NeedbyDate,QuoteAmt)
Values(@CustID,@NeedbyDate,@QuoteAmt)

set @QuoteID = scope_identity() 


Insert into dbo.QuoteDetail
(ID) values(@ID)

set @ID=scope_identity()



Insert into dbo.QuoteDetailParts
(QuoteDetailPartID, QuoteDetailID, PartNumber, Quantity)
values (@ID, @QuoteDetailPartID, @PartNumber, @Quantity) 
END

In SSMS, the parameters are not visible in the treeview of this sproc. Also, when I try calling this sproc from my ASPX page, I receive

System.Data.SqlClient.SqlException: Procedure AddQuote has no parameters and arguments were supplied

Here's the SQLDataSource defined on my ASPX page:

<asp:SqlDataSource runat="server" ID="SDSAddQuote" 
    ConnectionString="<%$ ConnectionStrings:Diel_inventoryConnectionString %>" 
    InsertCommand="AddQuote" InsertCommandType="StoredProcedure">
    <InsertParameters>
    <asp:ControlParameter ControlID="txtCompanyName" type="String" Name="CompanyName" />
    <asp:ControlParameter ControlID="txtCompanyAddress" Type="String" Name="CompanyAddress" />
    <asp:ControlParameter ControlID="txtCompanyCity" Type="String" Name="CompanyCity" />
    <asp:ControlParameter ControlID="ddlCompanyState" Type="String" Name="CompanyState" />
    <asp:ControlParameter ControlID="txtCompanyZip" Type="String" Name="CompanyZip" />
    <asp:ControlParameter ControlID="calNeedByDate" Type="DateTime" Name="NeedByDate" />
    <asp:ControlParameter ControlID="txtPartNumber1" Type="String" Name="PartNumber1" />
    <asp:ControlParameter ControlID="txtQty1" Type="Int16" Name="Qty1" />
            <asp:ControlParameter ControlID="txtPartNumber2" Type="String" Name="PartNumber2" />
    <asp:ControlParameter ControlID="txtQty2" Type="Int16" Name="Qty2" />
            <asp:ControlParameter ControlID="txtPartNumber3" Type="String" Name="PartNumber3" />
    <asp:ControlParameter ControlID="txtQty3" Type="Int16" Name="Qty3" />
            <asp:ControlParameter ControlID="txtPartNumber4" Type="String" Name="PartNumber4" />
    <asp:ControlParameter ControlID="txtQty4" Type="Int16" Name="Qty4" />
            <asp:ControlParameter ControlID="txtPartNumber5" Type="String" Name="PartNumber5" />
    <asp:ControlParameter ControlID="txtQty5" Type="Int16" Name="Qty5" />
            <asp:ControlParameter ControlID="txtPartNumber6" Type="String" Name="PartNumber6" />
    <asp:ControlParameter ControlID="txtQty6" Type="Int16" Name="Qty6" />
            <asp:ControlParameter ControlID="txtPartNumber7" Type="String" Name="PartNumber7" />
    <asp:ControlParameter ControlID="txtQty7" Type="Int16" Name="Qty7" />
            <asp:ControlParameter ControlID="txtPartNumber8" Type="String" Name="PartNumber8" />
    <asp:ControlParameter ControlID="txtQty8" Type="Int16" Name="Qty8" />
            <asp:ControlParameter ControlID="txtPartNumber9" Type="String" Name="PartNumber9" />
    <asp:ControlParameter ControlID="txtQty9" Type="Int16" Name="Qty9" />
            <asp:ControlParameter ControlID="txtPartNumber10" Type="String" Name="PartNumber10" />
    <asp:ControlParameter ControlID="txtQty10" Type="Int16" Name="Qty10" />
            <asp:ControlParameter ControlID="txtPartNumber11" Type="String" Name="PartNumber11" />
    <asp:ControlParameter ControlID="txtQty11" Type="Int16" Name="Qty11" />
            <asp:ControlParameter ControlID="txtPartNumber12" Type="String" Name="PartNumber12" />
    <asp:ControlParameter ControlID="txtQty12" Type="Int16" Name="Qty12" />
            <asp:ControlParameter ControlID="txtPartNumber13" Type="String" Name="PartNumber13" />
    <asp:ControlParameter ControlID="txtQty13" Type="Int16" Name="Qty13" />
            <asp:ControlParameter ControlID="txtPartNumber14" Type="String" Name="PartNumber14" />
    <asp:ControlParameter ControlID="txtQty14" Type="Int16" Name="Qty14" />
            <asp:ControlParameter ControlID="txtPartNumber15" Type="String" Name="PartNumber15" />
    <asp:ControlParameter ControlID="txtQty15" Type="Int16" Name="Qty15" />
            <asp:ControlParameter ControlID="txtPartNumber16" Type="String" Name="PartNumber16" />
    <asp:ControlParameter ControlID="txtQty16" Type="Int16" Name="Qty16" />
            <asp:ControlParameter ControlID="txtPartNumber17" Type="String" Name="PartNumber17" />
    <asp:ControlParameter ControlID="txtQty17" Type="Int16" Name="Qty17" />
            <asp:ControlParameter ControlID="txtPartNumber18" Type="String" Name="PartNumber18" />
    <asp:ControlParameter ControlID="txtQty18" Type="Int16" Name="Qty18" />
            <asp:ControlParameter ControlID="txtPartNumber19" Type="String" Name="PartNumber19" />
    <asp:ControlParameter ControlID="txtQty19" Type="Int16" Name="Qty19" />     
           <asp:ControlParameter ControlID="txtPartNumber20" Type="String" Name="PartNumber20" />
    <asp:ControlParameter ControlID="txtQty20" Type="Int16" Name="Qty20" />   
    </InsertParameters>
</asp:SqlDataSource>

Why aren't my parameters available, despite declaring them at the start of my sproc?
Thanks, Sid

+2  A: 

What exactly is it you're trying to do??

I want/need to enforce referential integrity such that when an insert is made on quotedetail, the quote and customer tables are also affected.

What do you mean by "...quote and customer table are also affected". Affected how??

Referential integrity by using foreign key means you cannot insert a Quote with a value of fkCustomerID that cannot be found in the Customers table.

Likewise, you cannot insert a QuoteDetail which has a value of fkQuoteID that is not found in the Quote table.

So what do you want to do when a QuoteDetail is inserted? How should the two other tables be "affected" ??

A side note: since your QuoteDetail table references the Quote table, which in turn references the Customers table, there's really no point in having the fkCustomerId in the QuoteDetails. I assume if a Quote is assigned to customer no. 12345, then all the QuoteDetails that belong to that quote will also be relevant for customre no. 12345, right?? So just reference Quote from QuoteDetails, and that's plenty good enough! Otherwise, you're just overcomplicating your model without gaining anything, really.

Side note #2: I would strongly recommend you get rid of those twenty pairs of PartNumberX / QtyX in your QuoteDetail - put those into a separate table, e.g. QuoteDetailParts, which references the QuoteDetail detail and has

QuoteDetailPartID    INT   PK
QuoteDetailID        INT   FK  --> to QuoteDetail table
PartNumber           FLOAT
Quantity             INT

as its fields. That's the much cleaner approach and design - this will allow you to store just two or three parts, if needed - but also 21, 25 if you ever need to - without changing the database!

UPDATE: if you want to insert values into QuoteDetail, you need to first have an entry in Quote and thus an entry in Customers. In your stored proc, you can do something like:

CREATE PROCEDURE dbo.InsertQuoteDetail
  (define list of parameters you want to pass in)
AS BEGIN
  DECLARE @CustomerID INT

  INSERT INTO dbo.Customers(list of fields)
    VALUES(list of values)

  SET @CustomerID = SCOPE_IDENTITY()

  DECLARE @QuoteID INT

  INSERT INTO dbo.Quote(CustomerID, (list of other fields))
     VALUES(@CustomerID, ....other values.....)

  SET @QuoteID = SCOPE_IDENTITY()

  INSERT INTO dbo.QuoteDetails(QuoteID, ....other fields....)
     VALUES(@QuoteID, ...... other values......)
marc_s
@marc_s: Thanks for your reply, nightowls rule! :) When a new record is inserted in QuoteDetail, I want Quotes and Customers tables both updated. A new CustomerID is inserted into Customers and that is also referenced in Quotes. Likewise, a new QuoteID is inserted into Quotes and referenced in QuoteDetail. Please let me know if this doesn't make sense, I'll take another stab at it in the morning. Thanks!
SidC
@SidC: it's 10:40am on Saturday here :-) Morning birds rule! :-)
marc_s
@SIdC: you cannot do that - you would have to insert Customer first, make a note of the new CustomerID; then insert the Quote settnig its fkCustomerID to the newly inserted CustomerID, make a note of QuoteID; and then lastly insert QuoteDetails as needed, referencing just the Quote table using the newly created QuoteID. That's what referential integrity is all about.
marc_s
@marc_s: So, in my sproc, I need to insert fields into the Customer table first, but how do I get the CustomerID and can I grab it from the sproc? Then, can I do the insert into the Quote table from the same sproc? Likewise for QuoteDetails and QuoteDetailParts? Thanks much:)
SidC
@SidC: yes, you need to insert values into the Customers table first. You can get the new customer id using `CustomerID = SCOPE_IDENTITY()` and likewise for the Quote table.
marc_s
@marc_s: You should not be inserting anything into the `Customers` and `Quotes` tables in your `dbo.InsertQuoteDetail` sproc.
FreshCode
@FreshCode: the OP **specifically** asked how he could insert something directly into the QuoteDetail table - that won't work unless you **first** insert a customer and next a Quote. But I agree - I don't like that approach either, but if you insist on doing this, that's the only way to do it, really.
marc_s
+1  A: 

Fix your QuoteDetail table design.

Your referential integrity will take care of itself if you design your database correctly. Refer to this good example on the third-normal form (3NF).

Having 20 "PartNumber" columns per record limits each quote to 20 items, complicates quote calculations, is error-prone and duplicates data.

  • Your Customers table is OK, but
  • Quotes should not have a QuoteAmt column: this should be calculated from your QuoteDetail table.
  • Since QuoteDetail is related to Quotes, which references a CustomerID, you don't need to store CustomerID in QuoteDetail again.

Any data that is "referenced", should always come from the referenced table. A customer's name should be provided by the Customers table; it should not be stored in your Quotes table. Likewise, Quotes should not store its (quote) total, because that value depends on the data in QuoteDetail.

Here is a better design for QuoteDetail:

CREATE TABLE [dbo].[QuoteDetail](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [fkQuoteID] [int] NOT NULL,   // which Quote this line/detail belongs to
    [fkPartID] [int] NOT NULL,    // which Part this line describes
    [Quantity] [int] NOT NULL,    // how many Parts
    [UnitPrice] [float] NOT NULL  // how much per Part
)

Now you can calculate each LineTotal as Quantity * UnitPrice, which you then sum to calculate your quote total (QuoteAmt). This design will ensure that your quote total is always correct and no data is ever duplicated. You can (and should) use views to calculate these values for display.

Refer to my question on database design for invoices and quotes (which are very similar to invoices). Ignore the part about revisions and look at the table design.

FreshCode
Thanks much! Would ID in QuoteDetail be a PK?
SidC
**Yes**. You could use `fkQuoteID` and `fkPartID` as a combined key, but then you could not have two of the same parts at different unit prices (which may or may not be appropriate for your domain problem). To stay flexible, I would not recommend it. That approach has merit for shopping cart items where you may only want to adjust the quantity. A single PK also makes deletes and updates more elegant, since you only have to deal with one parameter.
FreshCode
I went with just the one PK in this table. Another question for you: My ASPX form has 20 textbox controls for PartNumber and 20 textbox controls for Quantity. How do I go about sending the values of these to the database? That is, what insert syntax would I use? e.g for each txtPartNumber (in 1 to 20) insert txtPartNumber.text values (@PartNumber) ?
SidC
Get input working for adding **one line** to a quote first before trying to do more. If your `QuoteDetails` table references your `Parts` table (as opposed to generic lines with text descriptions), then you don't want the user to provide `txtPartName`. It should be pulled from your `Parts` table for display. Provide the user with a parts list which only submits the part ID, eg. a drop-down or autocomplete field. Once you have that working, you could use an input ID attribute naming schema of `line[n].ID`, `line[n].Quantity` and optionally `line[n].UnitPrice` for multiple lines.
FreshCode
also see http://blog.stevensandrson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/
FreshCode