Динамические иностранные ключи - как реализовать? - PullRequest
6 голосов
/ 02 ноября 2009

У меня есть 4 таблицы (назначенные, класс, выборные, статусные), которые я хочу перевести в одну таблицу (члены). Значения из 4 таблиц зависят от времени на основе таблицы истории (members_history). Желаемый результат состоит в том, что запрос должен выводить всех членов и текущую назначенную позицию или текущую выбранную позицию, класс и статус в строке членов и включать дополнительную информацию, полученную из внешних строк.

Так что вместо того, чтобы просто возвращаться:

id, имя пользователя, пароль, соль, name_first, name_last, date_join & date_leave;

Запрос вернет

id, имя пользователя, пароль, соль, name_prefix, name_first, name_last, hours_extra, date_join, date_leave, appointed, class, elected & status;

Там, где добавленный столбец не имеет текущего значения в истории, его результат должен быть НЕДЕЙСТВИТЕЛЕН.

Теперь я думаю, что могу сделать это с помощью подзапросов, но до сих пор бился головой о клавиатуру. Позже я сделаю еще один удар, но до тех пор, кто-нибудь еще, кто захочет сделать это, или попытается указать мне правильное направление?

Структура моих таблиц SQL (без каламбура) выглядит следующим образом:

CREATE TABLE IF NOT EXISTS `members` (
 `id` mediumint(3) unsigned NOT NULL auto_increment COMMENT 'Members Unique Id',
 `username` varchar(32) collate utf8_bin NOT NULL COMMENT 'Mebers Username',
 `password` varchar(64) collate utf8_bin NOT NULL COMMENT 'Members Password Hash',
 `salt` varchar(32) collate utf8_bin NOT NULL COMMENT 'Members Password Salt',
 `name_first` varchar(32) collate utf8_bin NOT NULL COMMENT 'Members First Name',
 `name_last` varchar(32) collate utf8_bin NOT NULL COMMENT 'Members Last Name',
 `date_join` date NOT NULL COMMENT 'Members Join Date',
 `date_leave` date default NULL COMMENT 'Members Resgination Date (If Applicable)',
 PRIMARY KEY  (`id`),
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='Members id in this table = mid in other tables';

CREATE TABLE IF NOT EXISTS `members:apointed` (
 `id` tinyint(3) unsigned NOT NULL auto_increment COMMENT 'Unique value',
 `name_prefix` varchar(8) collate utf8_bin NOT NULL COMMENT 'Prefix Added to Members Name',
 `hours_extra` decimal(4,2) NOT NULL COMMENT 'Hours Given as Bonus for Holding this Position.',
 `position` varchar(40) collate utf8_bin NOT NULL COMMENT 'Name of the Posisiton',
 PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='Undefined within the SOP or By-Laws.';

CREATE TABLE IF NOT EXISTS `members:class` (
 `id` tinyint(3) unsigned NOT NULL auto_increment COMMENT 'Unique Id',
 `class` varchar(8) collate utf8_bin NOT NULL COMMENT 'Unique Value',
 PRIMARY KEY  (`id`),
 UNIQUE KEY `value` (`class`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='Article I, Section 1 Subsection B: Classes of Membership';

CREATE TABLE IF NOT EXISTS `members:elected` (
 `id` tinyint(3) unsigned NOT NULL auto_increment COMMENT 'Unique value',
 `name_prefix` varchar(8) collate utf8_bin NOT NULL COMMENT 'Prefix Added to Members Name',
 `hours_extra` decimal(4,2) NOT NULL COMMENT 'Hours Given as Bonus for Holding this Position.',
 `position` varchar(40) collate utf8_bin NOT NULL COMMENT 'Name of the Posisiton',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='Article II';

CREATE TABLE IF NOT EXISTS `members:status` (
 `id` tinyint(3) unsigned NOT NULL auto_increment COMMENT 'Bit''s Place',
 `status` varchar(16) collate utf8_bin NOT NULL COMMENT 'Categorie''s Name',
 PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='Article I, Section 1, Subsection A: Categories of Membership';

CREATE TABLE IF NOT EXISTS `members_history` (
 `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique Id',
 `mid` tinyint(3) unsigned NOT NULL COMMENT 'Members Unique Id.',
 `table` enum('class','elected','appointed','status') NOT NULL COMMENT 'Name of Table that was Edited.',
 `value` tinyint(3) unsigned NOT NULL COMMENT 'Value',
 `start` date NOT NULL COMMENT 'Value''s Effect Date',
 `end` date default NULL COMMENT 'Value''s Expiration Date',
 PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 COMMENT='Member History';

members_history.mid - это FK для идентификатора в таблице участников, не каждый участник будет иметь историю на них (но в конечном итоге они все будут, поскольку каждый участник должен будет иметь класс и статус). members_history.value - это ФК для members:{members_history.table}.id;

INSERT INTO `members`
(`id`, `username`, `password`, `salt`, `name_first`, `name_last`, `date_join`, `date_join`) VALUES
(   1,   'Dygear',MD5('pass'), 's417',       'Mark',    'Tomlin',      DATE(), NULL),
(   2,  'uberusr',MD5('p455'), '235f',     'Howard',    'Singer',      DATE(), NULL),
(   3,'kingchief',MD5('leet'), '32fs','Christopher',   'Buckham',      DATE(), NULL);

INSERT INTO `members:apointed`
(`id`, `name_prefix`, `hours_extra`, `posisiton`) VALUES
(   1,            '',          0.00, 'Crew Chief'),
(   2,            '',         20.00, 'Engineer'),
(   3,         'Lt.',         40.00, 'Lieutenant'),
(   4,       'Capt.',         60.00, 'Captin'),
(   5,      'Chief.',         80.00, '3rd Assistant Chief of Operation');

INSERT INTO `members:class`
(`id`, `class`) VALUES
(   1, 'Class I'),
(   2, 'Class II');

INSERT INTO `members:elected`
(`id`, `name_prefix`, `hours_extra`, `posisiton`) VALUES
(   1,            '',         40.00, 'Trustee'),
(   2,            '',         40.00, 'Chairman of the Board'),
(   3,       'Prez.',         40.00, 'President'),
(   4,      'VPrez.',         40.00, 'Vice-President'),
(   5,            '',         40.00, 'Recording Secretary'),
(   6,            '',         40.00, 'Service Secretary'),
(   7,            '',         40.00, 'Corresponding Secretary'),
(   8,            '',         40.00, 'Financial Secretary Treasuer'),
(   9,            '',         40.00, 'Assistant Financial Secretary Treasuer'),
(  10,      'Chief.',         80.00, 'Chief of Operations'),
(  11,      'Chief.',         80.00, 'First Deputy Chief of Operations'),
(  12,      'Chief.',         80.00, 'Second Deputy Chief of Operation');

INSERT INTO `members:status`
(`id`, `status`) VALUES
(   1, 'Active'),
(   2, 'Inactive'),
(   3, 'Student'),
(   4, 'Probationary'),
(   5, 'Lifetime'),
(   6, 'Cadet'),
(   7, 'Honorary'),
(   8, 'Medical'),
(   9, 'Military'),
(  10, 'Resigned'),
(  11, 'Disvowed');


INSERT INTO `members_history`
(`id`, `mid`,    `table`, `value`, `start`, `end`) VALUES
(NULL,     1, 'apointed',       3,  DATE(), NULL),
(NULL,     1,    'class',       1,  DATE(), NULL),
(NULL,     1,   'status',       1,  DATE(), NULL),
(NULL,     2,  'elected',       4,  DATE(), NULL),
(NULL,     2,    'class',       1,  DATE(), NULL),
(NULL,     2,   'status',       1,  DATE(), NULL),
(NULL,     3, 'apointed',      10,  DATE(), '2010-05-01'),
(NULL,     3,    'class',       1,  DATE(), NULL),
(NULL,     3,   'status',       1,  DATE(), NULL);

Ответы [ 2 ]

11 голосов
/ 03 ноября 2009

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

CREATE TABLE IF NOT EXISTS `members:abstract` (
 `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
 `type` enum('class','elected','appointed','status') NOT NULL,
  UNIQUE KEY (`id`, `type`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

Эта таблица служит родительской таблицей для всех ваших таблиц атрибутов участников. Каждая из этих таблиц меняет свой первичный ключ на , а не генерирует значения идентификаторов автоматически, а вместо этого ссылается на первичный ключ members:abstract. Я покажу только members:appointed, но остальные будут похожи.

CREATE TABLE IF NOT EXISTS `members:appointed` (
 `id` INT UNSIGNED NOT NULL PRIMARY KEY, -- not auto_increment
 `name_prefix` varchar(8) collate utf8_bin NOT NULL COMMENT 'Prefix Added to Members Name',
 `hours_extra` decimal(4,2) NOT NULL COMMENT 'Hours Given as Bonus for Holding this Position.',
 `position` varchar(40) collate utf8_bin NOT NULL COMMENT 'Name of the Posisiton',
 FOREIGN KEY (`id`) REFERENCES `members:abstract` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='Undefined within the SOP or By-Laws.';

Вы можете заставить эту таблицу автоматически получать сгенерированные значения с помощью триггера:

DELIMITER //
DROP TRIGGER IF EXISTS ins_appointed//
CREATE TRIGGER ins_appointed BEFORE INSERT ON `members:appointed`
FOR EACH ROW BEGIN
  INSERT INTO `members:abstract` (`type`) VALUES ('appointed');
  SET NEW.id = LAST_INSERT_ID();
END; //
DELIMITER ;

Сделайте то же самое для каждой из других таблиц атрибутов.

Обратите внимание, что значения идентификаторов теперь уникальны для всех ваших таблиц атрибутов.

Затем вы делаете members:abstract целью для внешнего ключа в members_history.

CREATE TABLE IF NOT EXISTS `members_history` (
 `id` INT unsigned NOT NULL auto_increment COMMENT 'Unique Id',
 `mid` INT unsigned NOT NULL COMMENT 'Members Unique Id.',
 `value` INT UNSIGNED NOT NULL,
 `table` enum('class','elected','appointed','status') NOT NULL,
 `start` date NOT NULL COMMENT 'Value''s Effect Date',
 `end` date default NULL COMMENT 'Value''s Expiration Date',
 PRIMARY KEY (`id`),
 FOREIGN KEY (`mid`) REFERENCES `members` (`id`) ON DELETE CASCADE,
 FOREIGN KEY (`value`, `table`) REFERENCES `members:abstract` (`id`, `type`) ON DELETE CASCADE
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 COMMENT='Member History';

Обратите внимание, что эта таблица определяет внешний ключ, так что вы не можете ссылаться на идентификатор неверного типа атрибута в members:abstract.

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

Вот запрос, который возвращает описанный вами результат (проверено на MySQL 5.1.40):

SELECT m.username,
  m.password, m.salt, m.name_first, m.name_last,
  MAX(a.name_prefix) AS name_prefix,
  COALESCE(MAX(a.hours_extra), MAX(e.hours_extra)) AS hours_extra,
  MAX(m.date_join) AS date_join,
  MAX(m.date_leave) AS date_leave,
  MAX(a.position) AS appointed,
  MAX(c.class) AS class,
  MAX(e.position) AS elected,
  MAX(s.status) AS status
FROM `members` m 
JOIN `members_history` h ON (h.mid = m.id)
LEFT OUTER JOIN `members:appointed` a ON (h.table = 'appointed' AND h.value = a.id)
LEFT OUTER JOIN `members:class` c ON (h.table = 'class' AND h.value = c.id)
LEFT OUTER JOIN `members:elected` e ON (h.table = 'elected' AND h.value = e.id)
LEFT OUTER JOIN `members:status` s ON (h.table = 'status' AND h.value = s.id)
GROUP BY m.id;
1 голос
/ 03 ноября 2009

все, что вам нужно, это левое внешнее соединение для каждого из типов истории и любой логики, необходимой для выбора «текущей» строки.

Ваша структура таблицы не имеет для меня достаточного смысла, чтобы собрать образец для вас. возможно, если вы предоставите ОДИН пример члена и пару строк истории для ОДНОГО атрибута, я могу вам помочь.

...