ZASADY PROGRAMOWANIA I MODELOWANIA BAZ DANYCH PLATFORMY 4NET Historia zmian Data zmiany Wersja Autor Opis 2012-01-01 1 Michał Gołoś Utworzenie pierwszej wersji dokumentu 2014-06-11 2 Sebastian Daniluk Odświeżenie zasad 2015-01-22 3 Adrian Otto Doprecyzowanie punktu 1.7 1. ZASADY MODELOWANIA KONWENCJA NAZEWNICZA 1.1. Nazwy obiektów powinny być w języku angielskim w liczbie pojedynczej. 1.2. Nazwy obiektów nie powinny zaczynać się od cyfry i nie powinny zawierać spacji. 1.3. Nazwy tabel i widoków powinny być poprzedzone prefiksem (wymyślonym wg. zdrowego rozsądku np. pierwsze spółgłoski z nazwy) dla tabel stosujemy prefiks pięcioznakowy, np. cmn.busno_business_node dla tabel głównych systemu piątą literą w prefiksie powinna być litera x, np. cmn.patix_patient dla tabel asocjacji prefiks składa się ze złączenia prefiksów tabel, które łączymy rozdzielonych znakiem _. Pierwszy powinien być ten ważniejszy (wg. zdrowego rozsądku ). Przykład: prodx_servx (połączenie tabel prodx_product, servx_service) dla widoków stosujemy prefiks sześcioznakowy, przy czym szóstym znakiem powinna być litera v, np. cmn.shrsxv_reservation_service_instance. nazwy widoków będące nazwą tabeli z dodaną literą v do prefiksu są zarezerwowane, np. cmn.montxv_month. Zazwyczaj są one wykorzystywane do widoków generowanych na tabelach wykorzystujących rozwiązanie wielojęzyczności 1.4. Nazwy kluczy głównych powinny być zgodne ze wzorcem: nazwa kolumny nazwa ograniczenia (constraint), gdzie <table_prefix>_id PK_<table_name>
<table_prefix> - prefik tabeli np. dla tabeli cmn.factx_facility powinno to być odpowiednio: factx_id i PK_factx_facility W przypadku gdy kolumna PK pochodzi z innej tabeli, tzn. jest jednocześnie PK i FK, to powinna mieć nazwę według klucza: <pkcolumnname>_< table_prefix >, gdzie <table_prefix> oznacza prefiks tabeli, w której dodajemy taki skopiowany klucz np. kolumna shdlx_id_shblk w tabeli shdl.shblk_schedule_blockade 1.5. Nazwy kluczy obcych powinny być zgodne ze wzorcem: nazwa kolumny nazwa ograniczenia (constraint) <foreign_column_name>[_<relation_type>] FK_<table_prefix>_<foreign_column_name>[_<relation_type>], gdzie <foreign_column_name> - nazwa kolumny w innej tabeli do której się odnosimy <relation_type> - opcjonalne określenie typu relacji w przypadku wielokrotnego odwoływania się do tej samej tabeli np. kolumna locat_id w tabeli cmn.factx_facility jest referencją do klucza głównego w tabeli cmn.locat_location, nazwa ograniczenia w tym przypadku jest FK_factx_locat_id kolumny servt_id_additional_service, servt_id_ffs_patient, servt_id_contract_patient z tabeli slr.sersc_service_salary_configuration są referencjami do klucza głównego w tabeli slr.servt_service_valuation_type 1.6. Nazwy ograniczeń typu default powinny być zgodne ze wzorcem: DF_<table_prefix>_<column_name> np. nazwa ograniczenia DF_patix_patix_active dla kolumny patix_active w tabeli cmn.patix_patient 1.7. Nazwy ograniczeń typu unique powinny być zgodne ze wzorcem: UQ_<table_prefix>_<column_name> Zalecane jest używanie indeksów zamiast constraint. 1.8. Nazwy ograniczeń typu check powinny być zgodne ze wzorcem: CK_<table_prefix>_ <column_name> 1.9. Nazwy indeksów powinny być zgodne ze wzorcem: IX_<table_prefix >_<column_list> 1.10. Nazwy procedur powinny być zgodne ze wzorcem: sp<applicationname>_<action><table name or Logical instance etc.>, gdzie <ApplicationName> - nazwa aplikacji, np. Recp, EHR, Prd, GPP, itp. np: cmn.spehr_searchpatient 1.11. Nazwy funkcji powinny być zgodne ze wzorcem: udf<applicationname>_<action><table name or Logical instance etc.> KOMPETENCJE ZESPOŁU DB DEV 1.12. Do kompetencji należących wyłącznie do Zespół DB Dev a dotyczących modelowania i programowania baz danych należy min.: akceptacja zmian modelu
tworzenie kluczowych procedur, funkcji, widoków itd. w szczególności tych które mają widoczny wpływ na wydajność całego systemu od strony baz danych dodawanie nowych funkcji CLR (lista dostępnych funkcji - patrz niżej) tworzenie wyszukiwarek tekstowych optymalizacja/strojenie, w tym tworzeniem indeksów 1.13. Zmiany do modelu muszą być zgłoszone do Zespołu DB Dev w celu weryfikacji i akceptacji POZOSTAŁE ZASADY 1.14. Nie używamy kluczy głównych (PK) na kilku kolumnach. Jeżeli tabela posiada taki klucz, to należy utworzyć sztuczny klucz PK, a na dotychczasowych kolumnach, jeżeli była wymagana unikalność, trzeba założyć unikalny indeks. 1.15. Do grupowania rozwiązań stosujemy schema, nazwa jak najkrótsza, np. prd. Wyjątkiem są obiekty ogólnego zastosowania, które występują we wszystkich bazach danych. 1.16. Kolumny z wartością DEFAULT powinny być NOT NULL, constraint zawsze z nazwą. 1.17. Używamy typów nvarchar(max), varchar(max), varbinary(max) zamian text, ntext, image. Przy czym długość pola max powinna być użyta w ostateczności. Jeżeli to możliwe liczba znaków powinna zostać jawnie określona i dostosowana do przetrzymywanych danych. 1.18. Dla kolumn do przechowywania wartości pieniężnych używamy typu money. 1.19. Nie zaszywamy nazw serwera i bazy danych w definicji obiektu. 1.20. Do przechowywania wartości wielojęzycznych korzystamy z rozwiązania opisanego w punkcie 3, patrz niżej. 1.21. Do przechowywania konfiguracji aplikacji po stronie bazy danych używamy mechanizmu AppConfig, opisany osobno. 1.22. Do audytowania danych korzystamy ze specjalnego mechanizmu patrz niżej. 1.23. Kolumny typu mapa bitowa posiadają sufix _bit i są typu całkowitoliczbowego. Korzystanie z mapy bitowej, patrz niżej. 1.24. Kolumny z ograniczeniem typu CHECK posiadają sufix _chk oraz muszą posiadać opisy w tabeli cmn.chkdx_check_dictionary, patrz niżej. 1.25. Każdy model lub jego zmiana wchodząca na produkcję musi być zaakceptowana przez Zespół DB. 1.26. Nie zakładamy sami indeksów, wyjątek PK i UQ. 1.27. Nie używamy narzędzia DTA (Database Tuning Advisor), optymalizacją zajmuje się Zespół DB. 1.28. Nie używamy narzędzia Profiler, do monitorowania zapytań dla silników w wersji 2012 lub wyżej jest przygotowany osobny mechanizm, opisany niżej. Dla starszych silników monitorowanie na życzenie w Zespole DB. 2. ZASADY PROGRAMOWANIA 2.1. Kod powinien być opatrzony komentarzem. Komentarze powinny napisane być w języku polskim. 2.2. W kodzie wszystkie słowa kluczowe powinny być pisane z wielkiej litery (UPPER CASE) 2.3. Nie usuwamy danych, tylko je dezaktywujemy (usuwamy tylko te niepotrzebne) 2.4. Obiekty w jednej bazie danych nie mogą odwoływać się do obiektów w innej. Wyjątek baza data_federation, która do tego służy, patrz niżej. 2.5. Nowe procedury, funkcje i widoki powinny zawierać wypełniony nagłówek o postaci: -- ============================================= -- Author: -- Create date: -- Description: -- =============================================
3. ROZWIĄZANIA BAZODANOWE BAZA DATA_FEDERATION UWAGA! W przypadku niedużych tabel rekomendowane jest użycie replikacji zamiast bazy DATA_FEDERATION Baza służy do łączenia odwołań do kilku baz danych w jednym miejscu. Baza nie przechowuje danych. Dla każdego obiektu z bazy zewnętrznej tworzony jest synonim. Synonim tworzony jest w schema o nazwie składanej z <DB_prefix>_<schema> Obiekty (widoki, funkcje itd.) tworzymy w schema obiektu rozszerzanego. Odwołania w obiektach tylko do synonimów. OBSŁUGA WIELOJĘZYCZNOŚCI Do przechowywania wszystkich teksów wielojęzycznych w bazach danych używamy typu danych XML z użyciem schema walidującej [dbo].[4net_multilangcollection] oraz typie XML DOCUMENT. Np.: [factp_name] XML (DOCUMENT [dbo].[4net_multilangcollection]) NOT NULL Dla wszystkich tabel z wartościami wielojęzycznymi są generowane automatycznie widoki obsługujące pobieranie danych. Widoki te nie powinny być modyfikowane. Nie odwołujemy się bezpośrednio do wartości, tylko z użyciem w/w widoków. Widoki są tworzone w schema takiej samej, co tabela słownikowa. Informacje o języku są przekazywane w kolumnie langx_code (stare rozwiązanie langx_id). Należy obsługiwać sytuację, w której dla wartości słownikowej nie ma wartości tekstowej w wybranym języku, w takich przypadkach należy zwracać wartość dla języka polskiego. Widoki generowane automatycznie obsługują te przypadki. Do obsługi modyfikacji wielojęzyczności XML służy funkcja dbo.udfgetmultilangvalue. Funkcja przyjmuje 3 parametry: @exist_value typu XML, przekazywany jest XML do modyfikacji, w przypadku UPDATE może to być nazwa kolumny zawierającej XML, dla INSERT ustawiamy NULL @new_value typu NVARCHAR(MAX), przekazuje tekst, jaki ma zostać ustawiony, jeżeli chcemy usunąć wartość to ustawiamy NULL @langx_code - - typu VARCHAR(6), przekazujemy kod języka w którym dotyczy modyfikacja, jeżeli podamy NULL to przyjmuje się wartość pl Dzięki takiej funkcji można pisać instrukcje INSERT/UPDATE dla wielu wierszy w locie. DECLARE @x XML SELECT dbo.udfgetmultilangvalue(@x, 'Pierwsza nazwa', NULL) SELECT dbo.udfgetmultilangvalue(@x, 'Pierwsza nazwa', 'EN')
INSERT INTO cmn.factp_facility_special_type (factp_active, factp_name) VALUES (1, dbo.udfgetmultilangvalue(null, N'ABW', 'PL')) UPDATE cmn.factp_facility_special_type SET factp_name = dbo.udfgetmultilangvalue(factp_name, N'New Name', 'EN') WHERE factp_id = 2 AUDYT DANYCH Na potrzeby audytu danych w bazach LM, powstał mechanizm zapewniający takie działanie. Mechanizm można włączyć dla dowolnej tabeli, która spełnia poniższe wymagania: 1. Posiada klucz podstawowy (PK). 2. Nie zawiera klucza podstawowego na kilku kolumnach (multi PK). 3. PK nie może być zmieniany, zmiany PK nie są audytowane. 4. Audyt nie obsługuje typów danych: text, ntext, image, geography, geometry, binary, varbinary, timestamp. Sposób instalacji: 1. Dostarczony skrypt uruchomić na bazie, na której chcemy uruchomić mechanizm audytu. Skrypt utworzy 2 tabele: I. [audit].[audtt_definition_table] tabela zawierająca listę tabel, które chcemy audytować, należy wypełnić samemu II. [audit].[audtc_definition_column] tabela zawierająca listę kolumn z w/w tabel, które chcemy audytować, należy wypełnić samemu 2 procedury składowane: I. [audit].[spaudit_create] do uruchamiania audytu dla podanej w parametrach tabeli II. [audit].[spaudit_createall] do uruchomienia audytu na wszystkich zdefiniowanych w tabeli [audit].[audtt_definition_table] tabelach 2. Należy uzupełnić tabelę [audit].[audtt_definition_table], objaśnienie kolumn: [audtt_id] [int] id tabeli, tylko do odczytu [audtt_object_id] kolumna tylko do odczytu, podaje id obiektu, jeżeli znajdzie taką tabelę w strukturze bazy danych [audtt_schema] [sysname] NOT NULL nazwa schemy [audtt_table] [sysname] NOT NULL nazwa tabeli [audtt_length] [varchar](4) NOT NULL wszystkie audytowane dane są konwertowane do typu NVARCHAR, w tej kolumnie podajemy długość przechowywanych danych, np. 16, MAX itp. [audtt_is_common] [bit] NOT NULL DEFAULT (1)- decyduje o sposobie logowania czy będzie to osobna tabela czy wspólna (opcja domyślna) 3. Należy uzupełnić tabelę [audit].[audtc_definition_column], objaśnienie kolumn: [audtc_id] [int] - id wiersza, tylko do odczytu [audtt_id] [int] NOT NULL id tabeli z tabeli [audit].[audtt_definition_table] [audtc_column] [sysname] NOT NULL nazwa kolumny [audtc_alias] [sysname] NULL ewentualny alias kolumny [audtc_is_additional] [bit] NOT NULL DEFAULT (0) czy kolumna jest dodatkową kolumna klucza, domyślnie (False), jeżeli chcemy do audytowanych danych dołączyć wartość z tej kolumny należy ustawić na True 4. Uruchomić mechanizm audytu dla wybranej tabeli przy użyciu procedury [audit].[spaudit_create] podając, jako parametry wejściowe @SchemaName i @TableName lub dla wszystkich zdefiniowanych tabel przy użyciu procedury [audit].[spaudit_createall] Do audytowanej tabeli zostaną utworzone: 3 wyzwalacze po jednym dla operacji INSERT, UPDATE, DELETE Tabelę przechowującą historię zmian, jeżeli tak tabela jeszcze nie istnieje, w przypadku audytowania wspólnego (wartość kolumny [audtt_is_common] = True) tabela nazywać się będzie [audit].[audtl_audit], jeżeli audytowanie ma przebiegać osobno nazwa
będzie w formie [audit].[audtl_<schema audytowanej tabeli_nazwa audytowanej tabeli>_audit] Bezpośrednio do audytowanej tabeli zostaną dodane kolumny: [audtl_row_version] [int] wersja wiersza, automatycznie zwiększana w przypadku modyfikacji danych [audtl_created] [datetime] przechowuje datę utworzenia rekordu, jeżeli rekord został wstawiony przed włączeniem audytu to przechowuję datę włączenia audytu [audtl_created_by] [nvarchar] (128) - przechowuje nazwę użytkownika, który utworzył rekord, jeżeli rekord został wstawiony przed włączeniem audytu to przechowuję nazwę użytkownika włączającego audytu [audtl_operation] [char](1) przechowuje typ operacji (I,U,D) [audtl_owner] [nvarchar] (128) - nazwę użytkownika, który ostatnio modyfikował dane [audtl_from] [datetime] data od kiedy obowiązuję/obowiązywała wersja rekordu [audtl_to] [datetime] data do kiedy obowiązuję/obowiązywała wersja rekordu Zrealizowany został mechanizm ustawiania wartości kolumn [audtl_created_by] i [audtl_owner] na podstawie kontekstu sesji. Aby przekazać do audytu informację o użytkowniku wykonującym modyfikację należy po otworzeniu sesji wstawić nazwę użytkownika do parametru CONTEXT_INFO Przykładowy kod, gdzie 'username' to przekazywana nazwa użytkownika: DECLARE @context VARBINARY(128) = CAST('username' AS VARBINARY(128)) SET CONTEXT_INFO @context TYPY I OPERATORY BITOWE Kolumny typu mapa bitowa posiadają sufix _bit i są typu całkowitoliczbowego. Do pracy z mapami bitowymi służą operatory bitowe: & - Bitwise AND - Bitwise OR ^ - Bitwise Exclusive OR Przykład zastosowania: [operation].[slr].[worcn_worker_contract].[worcn_contract_data_bit] Mapa bitowa [worcn_contract_data_bit] przedstawia wartości umowy. Kolejne bity odpowiadają wartościom: Nr. bitu Dzień tygodnia BIN DEC 1 worcn_to_be_exported 00000001 1 2 worcn_use_fixed_salary 00000010 2 3 worcn_use_service_salary 00000100 4 4 worcn_use_additional_service_valuation 00001000 8 5 worcn_nfz_points_salary 00010000 16 6 worcn_poz_patients_salary 00100000 32 7 worcn_use_service_variant_control_change 01000000 64 8 worcn_use_worked_hours_salary 10000000 128
Poniższe zapytanie zwraca wyniki dla rekordów, które w mapie [worcn_contract_data_bit] mają ustawione bity 2(worcn_use_fixed_salary) oraz 8(worcn_use_worked_hours_salary). select * from [slr].[worcn_worker_contract] where [worcn_contract_data_bit] & 2>0 AND [worcn_contract_data_bit] & 128>0 TABELA CMN.CHKDX_CHECK_DICTIONARY Tabela przechowuje wartości zawarte w ograniczeniach typu CHECK. Kolumny: [chkdx_id][int] - ID, generowane automatycznie [chkdx_code][char](1) dopuszczalna wartość ograniczenia [chkdx_target_column][varchar](400) nazwa kolumny zawierającej ograniczenie [chkdx_sort_order][int] kolejność wartości [chkdx_name] [XML] nazwa odpowiadająca danej wartości Każda wartość to oddzielny w wpis w tabeli [cmn].[chkdx_check_dictionary] Przykład: 1. Dodanie Ograniczenia typu CHECK ALTER TABLE [cmn].[serva_service_variant] WITH CHECK ADD CONSTRAINT [CK_serva_esignature_chk] CHECK (([serva_esignature_chk]=' T ' OR [serva_esignature_chk]='n')) 2. Wpis do [cmn].[chkdx_check_dictionary] INSERT INTO cmn.chkdx_check_dictionary (chkdx_code,chkdx_target_column,chkdx_sort_order,chkdx_name) VALUES ('T','cmn.serva_service_variant.serva_esignature_chk',1, dbo.udfgetmultilangvalue(null, N'TAK', 'PL')), ('N','cmn.serva_service_variant.serva_esignature_chk',2, dbo.udfgetmultilangvalue(null, N'NIE', 'PL')) FUNKCJE CLR Nazwa dbo.concat( <@value, NVARCHAR(MAX),>, <@delimiter, NVARCHAR(MAX),>) dbo.regexmatch ( <@pattern, NVARCHAR(MAX),>, <@options, INT,>) dbo.regexismatch ( Miejsce użycia Opis SELECT dbo.concat(c, ; ) FROM (VALUES ('a'), ('b'), ('c')) t(c) SELECT dbo.regexmatch('me@mymail.com', '^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,6}$', 1)
<@pattern, NVARCHAR(MAX),>, <@options, INT,>) dbo.regexindex ( <@pattern, NVARCHAR(MAX),>, <@options, INT,>) dbo.regexescape ( <@input, NVARCHAR(MAX),>) dbo.regexreplace ( <@pattern, NVARCHAR(MAX),>, <@replacement, NVARCHAR(MAX),>) dbo.regexreplacex ( <@pattern, NVARCHAR(MAX),>, <@replacement, NVARCHAR(MAX),>, <@options, INT,>) dbo.regexoptionenumeration ( <@IgnoreCase, BIT,>,<@MultiLine, BIT,>,<@ExplicitCapture, BIT,>,<@Compiled, BIT,>,<@SingleLine, BIT,>,<@IgnorePatternWhitespace, BIT,>,<@RightToLeft, BIT,>,<@ECMAScript, BIT,>,<@CultureInvariant, BIT,>) dbo.fuzzymatch ( <@firststring, NVARCHAR(MAX),>,<@secondString, NVARCHAR(MAX),>,<@ignoreCase, BIT,>) dbo.trim ( <@text, NVARCHAR(MAX),>) dbo.isvalidpesel ( <@pesel, NVARCHAR(11),>) dbo.isvalidnip ( <@nip, NVARCHAR(10),>) dbo.formatdatatime ( <@dt, DATATIME,>,<@formatstring, NVARCHAR(64),>) dbo.removeaccent ( <@text, NVARCHAR(MAX),>) dbo.split ( <@Input, NVARCHAR(MAX),>,<@Delimiter, NVARCHAR(255),>) gdzie RegEx SELECT dbo.regexismatch('me@mymail.com', '^[A- Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,6}$', 1) SELECT dbo.regexindex('me@mymail.com','\bcom\b',1) SELECT dbo.regexescape('[*]') SELECT dbo.regexreplace('warszawa (Mazowieckie)', '.*?\((.*?)\).*', '$1') SELECT dbo.regexreplacex('powered by http://www.luxmed.pl/', '\b(https? ftp file)://([-a-z0-9+&@#/%?=~_!:,.;]*[-a-z0-9+&@#/%=~_ ])', '<a href="$2">$2</a>',1) SELECT dbo.regexoptionenumeration (1,0,0,0,0,0,0,0,0) SELECT [dbo].[fuzzymatch] ('ala', 'ola', 1) SELECT [dbo].[trim] (' ala ') SELECT [dbo].[isvalidpesel] ('80123108468') SELECT [dbo].[isvalidnip] ('8926131155') SELECT [dbo].[formatdatatime]('20130103 13:52:45', 'dd-mm-yyyythh:mm:ss') SELECT [dbo]. [RemoveAccent]('ąćęńóśźż') SELECT * FROM [dbo].[split]('a,b,cd,e,f,gh', ',') MONITOROWANIE ZAPYTAŃ Dla silników SQL Server w wersji 2012 lub wyższej włączony jest mechanizm Extended Events, utworzony jest event o nazwie Profiler, który umożliwia monitorowanie ruchu na serwerze. Event można znaleźć z poziomu SSMS, po zalogowaniu do serwera, w katalogu Management-> Extended Events->Profiler, klikamy PPM i wybieramy opcję Watch Live Data, przykład:
4. DOBRE PRAKTYKI PROGRAMOWANIA W TSQL 1.1. Unikamy stosowania symbolu *, klauzuli DISTINCT oraz LIKE w zapytaniach. 1.2. Jeżeli potrzebujemy uzyskać identyfikator wstawianego wiersza to należy używać SCOPE_IDENTITY(), a nie @@IDENTITY. Powód @@IDENTITY zwraca identyfikator ostatnio dodanego wiersza, nie koniecznie tego, którego wstawiliśmy. Możliwe jest, że w wyniku naszego wstawienia został wyzwolony jakiś trigger, który dokonał innego wstawienia do innej tabeli, wtedy otrzymamy jego identyfikator.