Сделать значения в 2 столбцах в одной таблице взаимно уникальными - PullRequest
1 голос
/ 21 июня 2019

Мне нужна ситуация, в которой я хочу сделать значения в 2 столбцах одной и той же таблицы уникальными. Я хочу установить правило, при котором любое из значений не может появиться снова ни в одном из 2 столбцов таблицы.

Например, рассмотрим таблицу mail_address_book (pk_serial_no, address_a, address_b) и address_a & address_b - это два столбца, в которых я хочу установить взаимную уникальность.

Если кто-то пытается запустить следующие операторы вставки, это должно быть:

create table mail_address_book (pk_serial_no number, address_a varchar2(5), address_b  varchar2(5))
insert into mail_address_book(1,'A','B'); --Allow
insert into mail_address_book(2,'B','A'); --Error
insert into mail_address_book(3,'C','A'); --Error
insert into mail_address_book(4,'C','C'); --Error
insert into mail_address_book(5,'C',null); --Allow

Ответы [ 3 ]

2 голосов
/ 28 июня 2019

Подход, аналогичный тому, который был предложен ФП, с использованием фиктивной таблицы, обеспечивающей уникальность, и триггера для управления законными действиями:

create table hack_table (address varchar2(5) primary key);

create trigger hack_trigger
before insert or update or delete on mail_address_book
for each row
begin
  if inserting then
    if :new.address_a is not null then
      insert into hack_table (address) values (:new.address_a);
    end if;
    if :new.address_b is not null then
      insert into hack_table (address) values (:new.address_b);
    end if;
  elsif updating then
    -- maybe skip this is column values have swapped
    if :old.address_a is null and :new.address_a is not null then
      insert into hack_table (address) values (:new.address_a);
    elsif :old.address_a is not null and :new.address_a is null then
      delete from hack_table where address = :old.address_a;
    elsif :new.address_a != :old.address_b then
      update hack_table set address = :new.address_a where address = :old.address_a;
    end if;

    if :old.address_b is null and :new.address_b is not null then
      insert into hack_table (address) values (:new.address_b);
    elsif :old.address_b is not null and :new.address_b is null then
      delete from hack_table where address = :old.address_b;
    elsif :new.address_b != :old.address_a then
      update hack_table set address = :new.address_b where address = :old.address_b;
    end if;
  else
    delete from hack_table where address in (:old.address_a, :old.address_b);
  end if;
end;
/

Очевидно, выберите более подходящие имена объектов и размеры столбцов * 8 -)

Тогда некоторые образцы вставок получают:

insert into mail_address_book (pk_serial_no, address_a, address_b) values (1,'A','B'); --Allow

1 row inserted.

insert into mail_address_book (pk_serial_no, address_a, address_b) values (2,'B','A'); --Error

ORA-00001: unique constraint (STACKOVERFLOW.SYS_C00141064) violated

insert into mail_address_book (pk_serial_no, address_a, address_b) values (3,'C','A'); --Error

ORA-00001: unique constraint (STACKOVERFLOW.SYS_C00141064) violated

insert into mail_address_book (pk_serial_no, address_a, address_b) values (4,'C','C'); --Error

ORA-00001: unique constraint (STACKOVERFLOW.SYS_C00141064) violated

insert into mail_address_book (pk_serial_no, address_a, address_b) values (5,'C',null); --Allow

1 row inserted.

insert into mail_address_book (pk_serial_no, address_a, address_b) values (6,'D','C'); --Error

ORA-00001: unique constraint (STACKOVERFLOW.SYS_C00141064) violated

insert into mail_address_book (pk_serial_no, address_a, address_b) values (7,'C','D'); --Error

ORA-00001: unique constraint (STACKOVERFLOW.SYS_C00141064) violated

insert into mail_address_book (pk_serial_no, address_a, address_b) values (8,'D','E'); --Allow

1 row inserted.

insert into mail_address_book (pk_serial_no, address_a, address_b) values (9,'E','F'); --Error

ORA-00001: unique constraint (STACKOVERFLOW.SYS_C00141064) violated

и вы получите только:

PK_SERIAL_NO ADDRE ADDRE
------------ ----- -----
           1 A     B    
           5 C          
           8 D     E    

дб <> скрипка

2 голосов
/ 23 июня 2019

Если вы хотите, чтобы значения 2 столбцов в одной и той же таблице были уникальными, тогда возникает проблема с моделью данных - два или более столбцов содержат информацию одинакового типа.Возможно, лучшим решением будет переопределить DM и создать отдельные таблицы:

create table mail_address_book (serial_no number primary key /* maybe FK to somewhat */)
/
create table mail_address_entries (
    serial_no number, addrno number, address varchar2(5) unique,
    constraint pk_fk_mail_address_entries primary key(serial_no, addrno),
    constraint fk_mail_address_entries foreign key (serial_no) references mail_address_book (serial_no))
/

В качестве обходного пути вы можете преобразовать физическую таблицу в представление, а затем использовать это представление вместо таблицы для всех запросов и операторов DML.Рассмотрим следующий пример:

create table mail_address_entries (
    pk_serial_no number, addrno number, address varchar2(5) unique,
    constraint pk_mail_address_entries primary key (pk_serial_no, addrno)
)
/
create or replace view mail_address_book as
    select a.pk_serial_no, a.address address_a, b.address address_b
    from mail_address_entries a  
    join mail_address_entries b on (
        b.pk_serial_no = a.pk_serial_no and a.addrno = 1 and b.addrno = 2 
    );

create or replace trigger trig_mail_address_book
instead of insert on mail_address_book
begin
    if inserting then -- the same for updating, deleting 
        insert into mail_address_entries values (:new.pk_serial_no, 1, :new.address_a);
        insert into mail_address_entries values (:new.pk_serial_no, 2, :new.address_b);
    end if;
end;
/

Вставьте тестовые данные:

create or replace type addrRow force is object (pk_serial_no number, address_a varchar2(5), address_b varchar2(5));  
/
create or replace type addrRows is table of addrRow;
/
exec dbms_errlog.create_error_log (dml_table_name => 'mail_address_book');

declare
    testdata addrRows; 
begin
    testdata := addrRows (
        addrRow (1, 'A', 'B'),
        addrRow (2, 'B', 'A'),
        addrRow (3, 'C', 'A'),
        addrRow (4, 'C', 'C'),
        addrRow (5, 'C', null),
        addrRow (6, null, null),
        addrRow (7, 'D', 'E'),
        addrRow (8, 'E', 'F')
    ); 
    for r in (select * from table (testdata)) loop 
        begin
            insert into mail_address_book values (r.pk_serial_no, r.address_a, r.address_b);
        exception when dup_val_on_index then 
            insert into err$_mail_address_book (pk_serial_no, address_a, address_b, ora_err_mesg$)
            values (r.pk_serial_no, r.address_a, r.address_b, 'error'); 
        end;
    end loop;
end;
/

Результат:

select to_char (pk_serial_no) no, address_a a, address_b b, 'ok' msg 
from mail_address_book 
union all
select pk_serial_no, address_a, address_b, ora_err_mesg$ msg 
from err$_mail_address_book
order by 1
;

NO    A     B     MSG       
----- ----- ----- ----------
1     A     B     ok        
2     B     A     error     
3     C     A     error     
4     C     C     error     
5     C     null  ok        
6     null  null  ok        
7     D     E     ok        
8     E     F     error     

db <> Fiddle

1 голос
/ 28 июня 2019

Я хотел получить решение, не создавая таблицу поиска каким-либо образом с использованием функционального индекса и триггера, однако, поскольку я не могу найти решение и из-за отсутствия полного контрольного ответа, пожалуйста, найдите мой подход ниже:

Ниже приведен поток кода / алгоритм. Я использую таблицу поиска в базе данных с уникальным индексом, однако таблица скрыта для конечного пользователя исходной таблицы.

create table distinct_add (address_code varchar2(25) not null);

create unique index distinct_add_uq1 on distinct_add(address_code);

Trigger before insert or update or delete on mail_address_book
------

IF INSERTING OR UPDATING
Then

   IF UPDATING
       IF(:old.address_a is not null and nvl(:old.address_a,'garbage') != nvl(:new.address_a,'garbage'))
       THEN

          delete :old.address_a from distinct_add

          catch exception raise error

       END IF

       IF(:old.address_b is not null and nvl(:old.address_b,'garbage') != nvl(:new.address_b,'garbage'))
       THEN

          delete :old.address_b from distinct_add

          catch exception raise error

       END IF

   END IF

   IF(:new.address_a is not null and nvl(:old.address_a,'garbage') != nvl(:new.address_a,'garbage'))
   THEN

       insert :new.address_a into distinct_add

       catch exception raise error

   END IF

   IF(:new.address_b is not null and nvl(:old.address_b,'garbage') != nvl(:new.address_b,'garbage'))
   THEN

       insert :new.address_b into distinct_add

       catch exception raise error

   END IF


END IF


IF DELETING
Then

    delete nvl(:new.address_a,'garbage') nvl(:new.address_b,'garbage') from distinct_add

    catch exception raise error

END IF
...