Множество наемане в софтуерна система се нарича разделяне на данни според набор от критерии, за да се удовлетвори набор от цели. Големината/обхватът, естеството и окончателното изпълнение на това разделяне зависят от тези критерии и цели. Многократното наемане е основно случай на разделяне на данни, но ние ще се опитаме да избегнем този термин поради очевидните причини (терминът в PostgreSQL има много конкретно значение и е запазен, тъй като декларативното разделяне на таблици беше въведено в postgresql 10).
Критериите могат да бъдат:
- съгласно идентификатора на важна основна таблица, която символизира идентификатора на наемателя, който може да представлява:
- компания/организация в рамките на по-голяма холдингова група
- отдел в рамките на компания/организация
- регионален офис/клон на същата компания/организация
- според местоположението/IP адреса на потребителя
- според позицията на потребителя във фирмата/организацията
Целите могат да бъдат:
- разделяне на физически или виртуални ресурси
- разделяне на системните ресурси
- сигурност
- точност и удобство на управлението/потребителите на различните нива на компанията/организацията
Забележете, като изпълняваме цел, ние изпълняваме и всички цели по-долу, т.е. изпълнявайки A, изпълняваме и B, C и D, изпълнявайки B, изпълняваме също C и D и т.н.
Ако искаме да изпълним цел А, можем да изберем да разположим всеки клиент като отделен клъстер от база данни в рамките на собствен физически/виртуален сървър. Това дава максимално разделяне на ресурсите и сигурността, но дава лоши резултати, когато трябва да видим всички данни като едно цяло, т.е. консолидиран изглед на цялата система.
Ако искаме да постигнем само цел Б, може да разположим всеки клиент като отделен екземпляр на postgresql в същия сървър. Това ще ни даде контрол върху това колко място ще бъде присвоено на всеки екземпляр, както и известен контрол (в зависимост от операционната система) върху използването на CPU/mem. Този случай не се различава по същество от A. В съвременната ера на изчисленията в облак, разликата между A и B има тенденция да става все по-малка и по-малка, така че A най-вероятно ще бъде предпочитаният начин пред B.
Ако искаме да постигнем цел C, т.е. сигурност, тогава е достатъчно да имаме един екземпляр на база данни и да разположим всеки клиент като отделна база данни.
И накрая, ако се грижим само за „мекото“ разделяне на данни или с други думи различни изгледи на една и съща система, можем да постигнем това само с един екземпляр на база данни и една база данни, като използваме множество техники, обсъдени по-долу като окончателни (и основна) тема на този блог. Говорейки за многократно наемане, от гледна точка на DBA, случаите A, B и C имат много прилики. Това е така, тъй като във всички случаи имаме различни бази данни и за да се свържат тези бази данни, тогава трябва да се използват специални инструменти и технологии. Ако обаче необходимостта от това идва от отделите за анализи или бизнес разузнаване, тогава може би изобщо не е необходимо свързване, тъй като данните могат да бъдат много добре репликирани на някакъв централен сървър, посветен на тези задачи, което прави свързването ненужно. Ако наистина е необходимо такова свързване, тогава трябва да използваме инструменти като dblink или чужди таблици. В днешно време предпочитаният начин е чуждестранните таблици чрез Foreign Data Wrappers.
Ако обаче използваме опция D, тогава консолидирането вече е дадено по подразбиране, така че сега трудната част е обратната:разделяне. Така че обикновено можем да категоризираме различните опции в две основни категории:
- Меко разделяне
- Трудно разделяне
Трудно разделяне чрез различни бази данни в същия клъстер
Да предположим, че трябва да проектираме система за въображаем бизнес, предлагащ коли и лодки под наем, но тъй като тези две се управляват от различни законодателства, различни контроли, одити, всяка компания трябва да поддържа отделни счетоводни отдели и по този начин бихме искали да запазим техните системи разделени. В този случай избираме да имаме различна база данни за всяка компания:rentaldb_cars и rentaldb_boats, които ще имат идентични схеми:
# \d customers
Table "public.customers"
Column | Type | Collation | Nullable | Default
-------------+---------------+-----------+----------+---------------------------------------
id | integer | | not null | nextval('customers_id_seq'::regclass)
cust_name | text | | not null |
birth_date | date | | |
sex | character(10) | | |
nationality | text | | |
Indexes:
"customers_pkey" PRIMARY KEY, btree (id)
Referenced by:
TABLE "rental" CONSTRAINT "rental_customerid_fkey" FOREIGN KEY (customerid) REFERENCES customers(id)
# \d rental
Table "public.rental"
Column | Type | Collation | Nullable | Default
------------+---------+-----------+----------+---------------------------------
id | integer | | not null | nextval('rental_id_seq'::regclass)
customerid | integer | | not null |
vehicleno | text | | |
datestart | date | | not null |
dateend | date | | |
Indexes:
"rental_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"rental_customerid_fkey" FOREIGN KEY (customerid) REFERENCES customers(id)
Да предположим, че имаме следните наеми. В rentaldb_cars:
rentaldb_cars=# select cust.cust_name,rent.vehicleno,rent.datestart FROM rental rent JOIN customers cust on (rent.customerid=cust.id);
cust_name | vehicleno | datestart
-----------------+-----------+------------
Valentino Rossi | INI 8888 | 2018-08-10
(1 row)
и в rentaldb_boats:
rentaldb_boats=# select cust.cust_name,rent.vehicleno,rent.datestart FROM rental rent JOIN customers cust on (rent.customerid=cust.id);
cust_name | vehicleno | datestart
----------------+-----------+------------
Petter Solberg | INI 9999 | 2018-08-10
(1 row)
Сега ръководството би искало да има консолидиран поглед върху системата, напр. унифициран начин за преглед на наемите. Може да решим това чрез приложението, но ако не искаме да актуализираме приложението или нямаме достъп до изходния код, тогава може да решим това, като създадем централна база данни rentaldb и чрез използване на чужди таблици, както следва:
CREATE EXTENSION IF NOT EXISTS postgres_fdw WITH SCHEMA public;
CREATE SERVER rentaldb_boats_srv FOREIGN DATA WRAPPER postgres_fdw OPTIONS (
dbname 'rentaldb_boats'
);
CREATE USER MAPPING FOR postgres SERVER rentaldb_boats_srv;
CREATE SERVER rentaldb_cars_srv FOREIGN DATA WRAPPER postgres_fdw OPTIONS (
dbname 'rentaldb_cars'
);
CREATE USER MAPPING FOR postgres SERVER rentaldb_cars_srv;
CREATE FOREIGN TABLE public.customers_boats (
id integer NOT NULL,
cust_name text NOT NULL
)
SERVER rentaldb_boats_srv
OPTIONS (
table_name 'customers'
);
CREATE FOREIGN TABLE public.customers_cars (
id integer NOT NULL,
cust_name text NOT NULL
)
SERVER rentaldb_cars_srv
OPTIONS (
table_name 'customers'
);
CREATE VIEW public.customers AS
SELECT 'cars'::character varying(50) AS tenant_db,
customers_cars.id,
customers_cars.cust_name
FROM public.customers_cars
UNION
SELECT 'boats'::character varying AS tenant_db,
customers_boats.id,
customers_boats.cust_name
FROM public.customers_boats;
CREATE FOREIGN TABLE public.rental_boats (
id integer NOT NULL,
customerid integer NOT NULL,
vehicleno text NOT NULL,
datestart date NOT NULL
)
SERVER rentaldb_boats_srv
OPTIONS (
table_name 'rental'
);
CREATE FOREIGN TABLE public.rental_cars (
id integer NOT NULL,
customerid integer NOT NULL,
vehicleno text NOT NULL,
datestart date NOT NULL
)
SERVER rentaldb_cars_srv
OPTIONS (
table_name 'rental'
);
CREATE VIEW public.rental AS
SELECT 'cars'::character varying(50) AS tenant_db,
rental_cars.id,
rental_cars.customerid,
rental_cars.vehicleno,
rental_cars.datestart
FROM public.rental_cars
UNION
SELECT 'boats'::character varying AS tenant_db,
rental_boats.id,
rental_boats.customerid,
rental_boats.vehicleno,
rental_boats.datestart
FROM public.rental_boats;
За да видите всички наеми и клиенти в цялата организация, ние просто правим:
rentaldb=# select cust.cust_name, rent.* FROM rental rent JOIN customers cust ON (rent.tenant_db=cust.tenant_db AND rent.customerid=cust.id);
cust_name | tenant_db | id | customerid | vehicleno | datestart
-----------------+-----------+----+------------+-----------+------------
Petter Solberg | boats | 1 | 1 | INI 9999 | 2018-08-10
Valentino Rossi | cars | 1 | 2 | INI 8888 | 2018-08-10
(2 rows)
Това изглежда добре, изолацията и сигурността са гарантирани, консолидацията е постигната, но все пак има проблеми:
- клиентите трябва да се поддържат отделно, което означава, че един и същ клиент може да се окаже с два акаунта
- Приложението трябва да спазва идеята за специална колона (като tenant_db) и да го добавя към всяка заявка, което го прави податливо на грешки.
- Получените изгледи не могат да се актуализират автоматично (тъй като съдържат UNION)
Меко разделяне в същата база данни
Когато се избере този подход, тогава консолидацията се дава извън кутията и сега трудната част е разделянето. PostgreSQL ни предлага множество решения, за да приложим разделяне:
- Прегледи
- Сигурност на ниво роля
- Схеми
С изгледите приложението трябва да зададе настройка за запитване, като например име_на_приложение, ние скриваме основната таблица зад изглед и след това във всяка заявка към която и да е от дъщерните (както в зависимостта на FK) таблиците, ако има такива, на тази основна таблица се присъединяват с тази гледка. Ще видим това в следващия пример в база данни, която наричаме rentaldb_one. Вграждаме идентификацията на фирмата наемател в основната таблица:
rentaldb_one=# \d rental_one
Table "public.rental_one"
Column | Type | Collation | Nullable | Default
------------+-----------------------+-----------+----------+------------------------------------
company | character varying(50) | | not null |
id | integer | | not null | nextval('rental_id_seq'::regclass)
customerid | integer | | not null |
vehicleno | text | | |
datestart | date | | not null |
dateend | date | | |
Indexes:
"rental_pkey" PRIMARY KEY, btree (id)
Check constraints:
"rental_company_check" CHECK (company::text = ANY (ARRAY['cars'::character varying, 'boats'::character varying]::text[]))
Foreign-key constraints:
"rental_customerid_fkey" FOREIGN KEY (customerid) REFERENCES customers(id)
Изтеглете Бялата книга днес Управление и автоматизация на PostgreSQL с ClusterControl Научете какво трябва да знаете, за да внедрите, наблюдавате, управлявате и мащабирате PostgreSQLD Изтеглете Бялата книга Схемата на клиентите на масата остава същата. Нека видим текущото съдържание на базата данни:
rentaldb_one=# select * from customers;
id | cust_name | birth_date | sex | nationality
----+-----------------+------------+-----+-------------
2 | Valentino Rossi | 1979-02-16 | |
1 | Petter Solberg | 1974-11-18 | |
(2 rows)
rentaldb_one=# select * from rental_one ;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(2 rows)
Използваме новото име rental_one, за да скрием това зад новия изглед, който ще има същото име на таблицата, което приложението очаква:rental. Приложението ще трябва да зададе името на приложението, за да обозначава наемателя. Така че в този пример ще имаме три екземпляра на приложението, едно за автомобили, едно за лодки и едно за висшето ръководство. Името на приложението се задава така:
rentaldb_one=# set application_name to 'cars';
Сега създаваме изгледа:
create or replace view rental as select company as "tenant_db",id,customerid,vehicleno,datestart,dateend from rental_one where (company = current_setting('application_name') OR current_setting('application_name')='all');
Забележка:Запазваме едни и същи колони и имена на таблици/изгледи, доколкото е възможно, ключовият момент в решенията с множество наематели е да запазим нещата еднакви от страната на приложението и промените да бъдат минимални и управляеми.
Нека направим някои селекции:
rentaldb_one=# задайте application_name на 'cars';
rentaldb_one=# set application_name to 'cars';
SET
rentaldb_one=# select * from rental;
tenant_db | id | customerid | vehicleno | datestart | dateend
-----------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
(1 row)
rentaldb_one=# set application_name to 'boats';
SET
rentaldb_one=# select * from rental;
tenant_db | id | customerid | vehicleno | datestart | dateend
-----------+----+------------+-----------+------------+---------
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(1 row)
rentaldb_one=# set application_name to 'all';
SET
rentaldb_one=# select * from rental;
tenant_db | id | customerid | vehicleno | datestart | dateend
-----------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(2 rows)
Третият екземпляр на приложението, който трябва да зададе името на приложението на „всички“, е предназначен за използване от висшето ръководство с оглед на цялата база данни.
По-стабилно решение, по отношение на сигурността, може да се основава на RLS (защита на ниво ред). Първо възстановяваме името на таблицата, не забравяйте, че не искаме да нарушаваме приложението:
rentaldb_one=# alter view rental rename to rental_view;
rentaldb_one=# alter table rental_one rename TO rental;
Първо създаваме двете групи потребители за всяка компания (лодки, автомобили), които трябва да виждат собственото си подмножество от данни:
rentaldb_one=# create role cars_employees;
rentaldb_one=# create role boats_employees;
Сега създаваме политики за сигурност за всяка група:
rentaldb_one=# create policy boats_plcy ON rental to boats_employees USING(company='boats');
rentaldb_one=# create policy cars_plcy ON rental to cars_employees USING(company='cars');
След предоставяне на необходимите грантове на двете роли:
rentaldb_one=# grant ALL on SCHEMA public to boats_employees ;
rentaldb_one=# grant ALL on SCHEMA public to cars_employees ;
rentaldb_one=# grant ALL on ALL tables in schema public TO cars_employees ;
rentaldb_one=# grant ALL on ALL tables in schema public TO boats_employees ;
създаваме по един потребител във всяка роля
rentaldb_one=# create user boats_user password 'boats_user' IN ROLE boats_employees;
rentaldb_one=# create user cars_user password 'cars_user' IN ROLE cars_employees;
И тест:
[email protected]:~> psql -U cars_user rentaldb_one
Password for user cars_user:
psql (10.5)
Type "help" for help.
rentaldb_one=> select * from rental;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
(1 row)
rentaldb_one=> \q
[email protected]:~> psql -U boats_user rentaldb_one
Password for user boats_user:
psql (10.5)
Type "help" for help.
rentaldb_one=> select * from rental;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(1 row)
rentaldb_one=>
Хубавото при този подход е, че нямаме нужда от много екземпляри на приложението. Цялата изолация се извършва на ниво база данни въз основа на ролите на потребителя. Следователно, за да създадем потребител в висшето управление, всичко, което трябва да направим, е да предоставим на този потребител и двете роли:
rentaldb_one=# create user all_user password 'all_user' IN ROLE boats_employees, cars_employees;
[email protected]:~> psql -U all_user rentaldb_one
Password for user all_user:
psql (10.5)
Type "help" for help.
rentaldb_one=> select * from rental;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(2 rows)
Разглеждайки тези две решения, виждаме, че решението за изглед изисква промяна на основното име на таблицата, което може да е доста натрапчиво, тъй като може да се наложи да стартираме точно същата схема в решение без множество наематели или с приложение, което не е наясно с име_на_приложение , докато второто решение обвързва хората с конкретни наематели. Ами ако едно и също лице работи напр. на наемателите на лодки сутрин и на наемателите на автомобили следобед? Ще видим 3-то решение, базирано на схеми, което според мен е най-универсалното и не страда от нито едно от предупрежденията на двете решения, описани по-горе. Това позволява на приложението да работи по начин, независим от наемателите, а системните инженери да добавят наематели в движение, когато възникнат нужди. Ще запазим същия дизайн, както преди, със същите тестови данни (ще продължим да работим върху примерния db rentaldb_one). Идеята тук е да добавите слой пред основната таблица под формата на обект на база данни в отделна схема което ще бъде достатъчно рано в пътеката за_търсене за този конкретен наемател. Пътят за търсене може да бъде зададен (в идеалния случай чрез специална функция, която дава повече опции) в конфигурацията на връзката на източника на данни на слоя на сървъра на приложения (следователно извън кода на приложението). Първо създаваме двете схеми:
rentaldb_one=# create schema cars;
rentaldb_one=# create schema boats;
След това създаваме обектите на базата данни (изгледи) във всяка схема:
CREATE OR REPLACE VIEW boats.rental AS
SELECT rental.company,
rental.id,
rental.customerid,
rental.vehicleno,
rental.datestart,
rental.dateend
FROM public.rental
WHERE rental.company::text = 'boats';
CREATE OR REPLACE VIEW cars.rental AS
SELECT rental.company,
rental.id,
rental.customerid,
rental.vehicleno,
rental.datestart,
rental.dateend
FROM public.rental
WHERE rental.company::text = 'cars';
Следващата стъпка е да зададете пътя за търсене във всеки клиент, както следва:
-
За наемателите на лодки:
set search_path TO 'boats, "$user", public';
-
За наемателите на автомобили:
set search_path TO 'cars, "$user", public';
- За най-горния наемател на управление оставете го по подразбиране
Нека тестваме:
rentaldb_one=# select * from rental;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(2 rows)
rentaldb_one=# set search_path TO 'boats, "$user", public';
SET
rentaldb_one=# select * from rental;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(1 row)
rentaldb_one=# set search_path TO 'cars, "$user", public';
SET
rentaldb_one=# select * from rental;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
(1 row)
Свързани ресурси ClusterControl за PostgreSQL PostgreSQL тригери и съхранени функции Основи Настройка на входно/изходни (I/O) операции за PostgreSQL Вместо set search_path можем да напишем по-сложна функция за обработка на по-сложна логика и да я извикаме в конфигурацията на връзката на нашето приложение или пул за връзки.
В примера по-горе използвахме една и съща централна таблица, намираща се в публична схема (public.rental) и два допълнителни изгледа за всеки наемател, като използвахме щастливия факт, че тези два изгледа са прости и следователно могат да се записват. Вместо изгледи можем да използваме наследяване, като създадем една дъщерна таблица за всеки наемател, наследяващ от публичната таблица. Това е отлично съвпадение за наследяване на таблици, уникална характеристика на PostgreSQL. Горната таблица може да бъде конфигурирана с правила за забрана на вмъквания. В решението за наследяване ще е необходимо преобразуване за попълване на дъщерните таблици и за предотвратяване на достъпа до родителската таблица, така че това не е толкова просто, колкото в случая с изгледите, който работи с минимално въздействие върху дизайна. Може да напишем специален блог как да направим това.
Горните три подхода могат да бъдат комбинирани, за да дадат още повече възможности.