firedac master - detail relationship doesnt work - master-detail

First definition of database tables:
CREATE TABLE UNCERTAINTY
(
ID Integer NOT NULL,
NAME Varchar(240) CHARACTER SET NONE NOT NULL,
CONSTRAINT PK_UNCERTAINTY PRIMARY KEY (ID),
UNIQUE (NAME)
);
CREATE TABLE UNCERTAINTYDETAIL
(
ID Integer NOT NULL,
UNCERTAINTY_ID Integer NOT NULL,
MEASTYPE Varchar(255) CHARACTER SET NONE,
CONDITION Varchar(255) CHARACTER SET NONE,
POWERFACTOR Double precision,
LOADTYPE Varchar(255) CHARACTER SET NONE,
UNCERTAINTY Double precision,
CONSTRAINT PK_UNCERTAINTYDETAIL PRIMARY KEY (ID)
);
ALTER TABLE UNCERTAINTYDETAIL ADD CONSTRAINT FK_UNCERTAINTY_UNCERTAINTYDETAI
FOREIGN KEY (UNCERTAINTY_ID) REFERENCES UNCERTAINTY (ID) ON DELETE CASCADE;
And now definition of my data sources and queries in dfm file:
object FDQuery1: TFDQuery
Connection = Connection1
SQL.Strings = (
'select * from UNCERTAINTY'
'order by NAME')
Left = 125
Top = 239
end
object FDQuery2: TFDQuery
MasterSource = DataSource1
MasterFields = 'ID'
Connection = Connection1
SQL.Strings = (
'select * from UNCERTAINTYDETAIL'
'where UNCERTAINTY_ID=:param1'
'order by ID')
Left = 456
Top = 224
ParamData = <
item
Name = 'PARAM1'
ParamType = ptInput
end>
end
object DataSource1: TDataSource
DataSet = FDQuery1
Left = 53
Top = 239
end
object DataSource2: TDataSource
DataSet = FDQuery2
Left = 392
Top = 224
end
Both data sources are connected to db grids.
In FormShow event I call following methods:
FDQuery1->Open();
FDQuery2->Open();
The master grid displays correctly all items stored in table UNCERTAINTY but detail grid is always empty. Why ?

The parameters of detail query should match MasterFields. So change param1 of FDQuery2 to ID to match value MasterFields.

Related

Create a different sequence in SQL Server

I need to create a sequence in the database that cannot be using sequence or identity.
There is a table in the database called File where all the files that users send in different areas of the system are stored.
It contains the id (primary key), name, type, folder, number, hash...
CREATE TABLE dbo.[File]
(
FileId uniqueidentifier NOT NULL,
Name nvarchar(30) NOT NULL,
FileTypeId int NOT NULL,
FileFolderId int NOT NULL,
Number int NOT NULL,
Hash nvarchar(50) NOT NULL
...
) ON [PRIMARY]
And then for each feature there is a table expanding the properties of the File table, an example is ContractFile.
It has the same id of the File table and with a few more fields and the id of the Contract table, creating the relation.
CREATE TABLE dbo.ContractFile
(
FileId uniqueidentifier NOT NULL,
ContractId uniqueidentifier NOT NULL
...
) ON [PRIMARY]
So the filename should follow a pattern.
050#H4G5H4G244#001.pdf
050#H4G5H4G244#002.pdf
060#H4G5H4G244#001.pdf
The first 3 digits is a code that is in the FileType table.
The digits in the middle is the code in the Contract table.
And the last 3 is the sequence that was inserted.
Then it groups the string by the FileType and the Contract.
So I created a trigger in the ContractFile table for when inserting it get the biggest number for that FileType and for the Contract and add +1, setting the Number field of the File table.
Then the file name is updated (in the same trigger)
CREATE TRIGGER [dbo].[tgContractFileInsert]
ON [dbo].[ContractFile]
FOR INSERT
AS
BEGIN
SET NOCOUNT ON
UPDATE dbo.File
SET Number = COALESCE(
(SELECT MAX(AR.Number)
FROM dbo.ContractFile NOA
INNER JOIN dbo.File AR
ON AR.FileId = NOA.FileId
WHERE NOA.ContractId = I.ContractId AND
AR.FileTypeId = T.FileTypeId
),
0) + 1
FROM dbo.File T WITH (XLOCK)
INNER JOIN Inserted I
ON I.FileId = T.FileId
WHERE T.Number IS NULL
UPDATE dbo.File
SET Name = dbo.fnFileName(AP.Code, NOB.Code, T.Numero, T.Name)
FROM dbo.File T
INNER JOIN Inserted I
ON I.FileId = T.FileId
INNER JOIN dbo.FileType AP
ON AP.FileTypeId = T.FileTypeId
INNER JOIN dbo.Contract NOB
ON NOB.ContractId = I.ContractId
END
At first it works, but when we have a large volume being inserted, there is a deadlock.
And from what I'm seeing also when inserting more than one record will end up getting the same number, since the Inserted table will bring two records and the +1 is not checking this.
How could I solve this? What is the best way?
Avoid deadlock, will the sequence be correct even inserting more than one record at a time and have a good performance?

Validate number of rows depending on master row field value

I need to add a constraint to validate that number of rows referencing master table is lower than value in master row, e.g we have a table
master(master_id int pk, max_val int) and slave(slave_id int pk, master_id fk ref master(master_id)) (so slave is de facto a colection of something), and I want that count(master_id) in slave is <= than max_val for this master_id. I have a constraint
constraint NO_MORE_PASS check ((select count(head_id) from parts p where
p.head_id = head_id) <= (select max_val from head where id = head_id));
(not sure if it is correct, however SQL Server tells that subqueries are not allowed (sql server 2017) so...).
I have also read Check Constraint - Subqueries are not allowed in this context, so the question: is there any other alternative (I would like to avoid using trigger)?.
I'am using this in spring app with spring data jpa (and hibernate) - may be useful, but would like to make it on db side rather than in the app.
Nethertheless entity it is like:
#Entity
#Table(name = "route_parts")
data class RoutePart(
#Id
#Column(name = "route_part_id")
#GeneratedValue(strategy = GenerationType.AUTO)
var id: Long? = null,
//...
#Column(nullable = false)
var slots: Int? = null,
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "route_part_passengers",
joinColumns = [(JoinColumn(name = "route_part_id"))],
inverseJoinColumns = [(JoinColumn(name = "user_id"))]
)
var passengers: Set<ApplicationUser> = setOf()
)
and in that case ApplicationUser is a slave (or better - another table will be created, and actually this will be that slave table) limited by slots value.
So the question is...
How can I achieve limiting number of ApplicationUser attached to each RoutePart
If you want your check constraints to be based on queries, you must use a user defined function for the check constraint to work with.
Here is a quick example:
Tables:
CREATE TABLE dbo.Parent
(
Id int,
MaxNumberOfChildren int NOT NULL
);
CREATE TABLE dbo.Child
(
Id int,
ParentId int
);
User defined function (All it does is return the difference between the MaxNumberOfChildren and the number of records in the Child table with the same ParentId):
CREATE FUNCTION dbo.RestrictNumbrOfChildren
(
#ParentId int
)
RETURNS int
AS
BEGIN
RETURN
(
SELECT MaxNumberOfChildren
FROM dbo.Parent
WHERE Id = #ParentId
)
-
(
SELECT COUNT(Id)
FROM dbo.Child
WHERE ParentId = #ParentId
)
END;
Add the check constraint to the Child table:
ALTER TABLE dbo.Child
ADD CONSTRAINT chk_childCount CHECK (dbo.RestrictNumbrOfChildren(ParentId) >= 0);
And that's basically all you need, unless MaxNumberOfChildren is nullable.
In that case, you should add ISNULL() to the first query, with either 0 if null means no children are allowed, or the maximum value of int (2,147,483,647) if null means no restriction on the number of children - so it becomes SELECT ISNULL(MaxNumberOfChildren, 0)... or SELECT ISNULL(MaxNumberOfChildren, 2147483647)....
To test the script, let's insert some data to the Parent table:
INSERT INTO Parent (Id, MaxNumberOfChildren) VALUES
(1, 3), (2, 2), (3, 1);
And insert some valid data to the Child table:
INSERT INTO Child (Id, ParentId) VALUES
(1, 1), (2, 2);
So far, we have not exceeded the maximum number of records allowed. Now let's try to do that by insert some more data to the Child table:
INSERT INTO Child (Id, ParentId) VALUES
(3, 1), (4, 1), (5, 1);
Now, this insert statement will fail with the error message:
The INSERT statement conflicted with the CHECK constraint "chk_childCount". The conflict occurred in database "<your database name here>", table "dbo.Child", column 'ParentId'.
You can see a live demo on rextester.

trigger for insert or update after checking relationship

I have this 3 tables:
And i need to build a trigger that: A date ("encontro") can only works when theres a friendship ("amizade") between 2 profiles ("perfis").
I've created this trigger but i feel lost.. HELP ME
CREATE TRIGGER relaƧoes_after_insert
ON encontros
INSTEAD OF insert -
as
begin
declare #idperfilA int;
declare #idperfilB int;
declare #data datetime;
declare #count int;
declare cursor_1 cursor for select * from inserted;
open cursor_1;
fetch next from cursor_1 into #idperfilA, #idperfilB, #data;
WHILE ##FETCH_STATUS = 0
BEGIN
if exists( select * from inserted i, amizade a
where i.IDPERFILA = a.IDPERFILA and i.IDPERFILB = a.IDPERFILB and GETDATE() > DATA)
RAISERROR('there isnt friendship', 16, 10);
else
insert into ENCONTROS select * from inserted;
end;
fetch next from cursor_1 into #idperfilA, #idperfilB, #data;
END
close cursor_1;
deallocate cursor_1;
I think the better answer would be to not create use a trigger for this at all. Instead I would create and enforce a foreign key constraint between encontros and amizade.
As far as I can tell, this will result in doing what you want without having to write your own code to try and recreate behavior provided by the database. It also makes it much easier to understand from a database design point of view.
alter table dbo.encontros
add constraint fk_amizade__encontros
foreign key (idperflia, idperflib) references dbo.amizade (idperflia, idperflib)
/* optional
on delete { no action | cascade | set null | set default } -- pick one, usual defualt is: no action
on update { no action | cascade | set null | set default } -- pick one, usual defualt is: no action
--*/*
;
More about table constraints.
NO ACTION
The SQL Server Database Engine raises an error and the delete action on the row in the parent table is rolled back.
CASCADE
Corresponding rows are deleted from the referencing table if that row is deleted from the parent table.
SET NULL
All the values that make up the foreign key are set to NULL when the corresponding row in the parent table is deleted. For this constraint to execute, the foreign key columns must be nullable.
SET DEFAULT
All the values that comprise the foreign key are set to their default values when the corresponding row in the parent table is deleted. For this constraint to execute, all foreign key columns must have default definitions. If a column is nullable and there is no explicit default value set, NULL becomes the implicit default value of the column.
Based on your reply to #3N1GM4:
#3N1GM4 if exists some friendship with a date after today (for example) it is an error, so the friendship doesnt exist. But i dont know if it matters at this point. IDPERFILA and IDPERFILB will match A and B at amizade table, but i need to make sure that they were not the same
You could create a check constraint on amizade that will prevent rows with invalid dates from being inserted into the table.
alter table dbo.amizade
add constraint chk_data_lt_getdate ([data] < get_date());
More about check constraints; more examples from Gregory Larson.
original answer:
I'm still waiting on some clarification on the question, but one of the versions in this should be on the right path:
create trigger relaƧoes_after_insert
on encontros
instead of insert
as
begin
/* To abort when any row doesn't have a matching friendship */
if not exists (
select 1
from inserted i
where exists (
select 1
from amizade a
where a.idperfila = i.idperfila
and a.idperfilb = i.idperfilb
and getdate() > data /* not sure what this part does */
/* as #3N1GM4 pointed out,
if the position doesn't matter between idperflia and idperflib then:
where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb)
or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila)
*/
)
begin;
raiserror('there isnt friendship', 16, 10);
else
insert into encontros
select * from inserted;
end;
end;
/* To insert all rows that have a matching friendship, you could use this instead */
insert into encontros
select i.*
from inserted i
where exists (
select 1
from amizade a
where a.idperfila = i.idperfila
and a.idperfilb = i.idperfilb
and getdate() > data /* not sure what this part does */
/* as #3N1GM4 pointed out,
if the position doesn't matter between idperflia and idperflib then:
where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb)
or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila)
*/
)
end;
The only potential issue I see with using an inner join instead of exists for the second option (inserting rows that have a friendship and ignoring ones that don't) is if there could ever be an issue where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb) or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila) would return duplicates of the inserted rows from each condition returning a match.

Create KeyValuePair<TKey, TValue> from columns in a single data row

I have the following table in Sql Server that stores the permissions the Foreign Key UserId has to the Foreign Keyed BlogId. What I would like to do is write a query with Dapper that takes each column after BlogId and returns it as a KeyValuePair<TKey, TValue>.
CREATE TABLE [dbo].[UserPermission] (
[UserPermissionId] INT IDENTITY (1, 1) NOT NULL,
[BlogId] INT NOT NULL,
[UserId] INT NOT NULL,
[CanCreateNewPosts] BIT NOT NULL,
[CanEditExistingPosts] BIT NOT NULL,
[CanDeleteExistingPosts] BIT NOT NULL,
[CanPublishDraftPosts] BIT NOT NULL,
CONSTRAINT [PK_UserPermission] PRIMARY KEY CLUSTERED ([UserPermissionId] ASC),
CONSTRAINT [FK_UserPermission_Blog] FOREIGN KEY ([BlogId]) REFERENCES [dbo].[Blog] ([BlogId]),
CONSTRAINT [FK_UserPermission_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([UserId])
);
I would like to query via Dapper like this:
using (var connection = new SqlConnection(this.connectionString))
{
string sql = "SELECT * FROM [Permission] WHERE UserId = #UserId";
await connection.OpenAsync();
IEnumerable<KeyValuePair<string, bool>> results = await connection.QueryAsync(
sql,
new { UserId = this.userId });
}
var p1 = results.First();
var p2 = results.Skip(1).First();
In the above example, I would like p1 to result in a KeyValuePair with the Key being CanCreateNewPosts and the Value being the column value, either true or false. Same applies with p2, where the Key would be CanEditExistingPosts, with it's corresponding value.
The underlying need for this is to simplify transforming the record into a list of Claims in Identity, one claim per column.
I looked at splitOn:, to try and split after the UserId column but that doesn't seem like it's what I want. It would require n-generic arguments for each column I split. Ideally I'd like to add columns to this table in the future and my security/data/servicing layer just handles turning it into a Claim - letting me just focus on the Controller Action that needs to check for the claim. Having the query and Dapper return map the column-name/values into a collection of KeyValuePairs would facilitate that need for me.
You can use the SQL UNPIVOT operation to transform columns into column values. Here's an example for your particular case:
SELECT u.BlogId, u.permission
FROM UserPermissions up
UNPIVOT
(
permission for perms in (
CanCreateNewPosts,
CanEditExistingPosts,
CanDeleteExistingPosts,
CanPublishDraftPosts
)
) u;

Sql Server string interning

We have a table where we store all the exceptions (message, stackTrace, etc..), the table is getting big and we would like to reduce it.
There are plenty of repeated StackTraces, Messages, etc, but enabling compression produces a modest size reduction (10%) while I think much bigger benefits could come if somehow Sql Server will intern the strings in some per-column hash-table.
I could get some of the benefits if I normalize the table and extract StackTraces to another one, but exception messages, exception types, etc.. are also repeated.
Is there a way to enable string interning for some column in Sql Server?
There is no built-in way to do this. You could easily do something like:
SELECT MessageID = IDENTITY(INT, 1, 1), Message
INTO dbo.Messages
FROM dbo.HugeTable GROUP BY Message;
ALTER TABLE dbo.HugeTable ADD MessageID INT;
UPDATE h
SET h.MessageID = m.MessageID
FROM dbo.HugeTable AS h
INNER JOIN dbo.Messages AS m
ON h.Message = m.Message;
ALTER TABLE dbo.HugeTable DROP COLUMN Message;
Now you'll need to do a few things:
Change your logging procedure to perform an upsert to the Messages table
Add proper indexes to the messages table (wasn't sure of Message data type) and PK
Add FK to MessageID column
Rebuild indexes on HugeTable to reclaim space
Do this in a test environment first!
Aaron's posting answers the questions of adding interning to a table, but afterwards you will need to modify your application code and stored-procedures to work with the new schema.
...or so you might think. You can actually create a VIEW that returns data matching the old schema, and you can also support INSERT operations on the view too, which are translated into child operations on the Messages and HugeTable tables. For readability I'll use the names InternedStrings and ExceptionLogs for the tables.
So if the old table was this:
CREATE TABLE ExceptionLogs (
LogId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Message nvarchar(1024) NOT NULL,
ExceptionType nvarchar(512) NOT NULL,
StackTrace nvarchar(4096) NOT NULL
)
And the new tables are:
CREATE TABLE InternedStrings (
StringId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Value nvarchar(max) NOT NULL
)
CREATE TABLE ExceptionLogs2 ( -- note the new name
LogId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Message int NOT NULL,
ExceptionType int NOT NULL,
StackTrace int NOT NULL
)
Add an index to InternedStrings to make the value lookups faster:
CREATE UNIQUE NONCLUSTERED INDEX IX_U_InternedStrings_Value ON InternedStrings ( Value ASC )
Then you would also have a VIEW:
CREATE VIEW ExeptionLogs AS
SELECT
LogId,
MessageStrings .Value AS Message,
ExceptionTypeStrings.Value AS ExceptionType,
StackTraceStrings .Value AS StackTrace
FROM
ExceptionLogs2
INNER JOIN InternedStrings AS MessageStrings ON
MessageStrings.StringId = ExceptionLogs2.Message
INNER JOIN InternedStrings AS ExceptionTypeStrings ON
ExceptionTypeStrings.StringId = ExceptionLogs2.ExceptionType
INNER JOIN InternedStrings AS StackTraceStrings ON
StackTraceStrings.StringId = ExceptionLogs2.StackTrace
And to handle INSERT operations from unmodified clients:
CREATE TRIGGER ExceptionLogsInsertHandler
ON ExceptionLogs INSTEAD OF INSERT AS
DECLARE #messageId int = SELECT StringId FROM InternedStrings WHERE Value = inserted.Message
IF #messageId IS NULL
BEGIN
INSERT INTO InternedStrings ( Text ) VALUES ( inserted.Message )
SET #messageId = SCOPE_IDENTITY()
END
DECLARE #exceptionTypeId int = SELECT StringId FROM InternedStrings WHERE Value = inserted.ExceptionType
IF #exceptionTypeId IS NULL
BEGIN
INSERT INTO InternedStrings ( Text ) VALUES ( inserted.ExceptionType )
SET #exceptionTypeId = SCOPE_IDENTITY()
END
DECLARE #stackTraceId int = SELECT StringId FROM InternedStrings WHERE Value = inserted.StackTrace
IF #stackTraceId IS NULL
BEGIN
INSERT INTO InternedStrings ( Text ) VALUES ( inserted.StackTrace )
SET #stackTraceId = SCOPE_IDENTITY()
END
INSERT INTO ExceptionLogs2 ( Message, ExceptionType, StackTrace )
VALUES ( #messageId, #exceptionTypeId, #stackTraceId )
Note this TRIGGER can be improved: it only supports single-row insertions, and is not entirely concurrency-safe, though because previous data won't be mutated it means that there's a slight risk of data duplication in the InternedStrings table - and because of a UNIQUE index the insert will fail. There are different possible ways to handle this, such as using a TRANSACTION and changing the queries to use holdlock and updlock.

Resources