имена на динамични таблици за модели на Active Record

Имам интересен проблем с Active Record и не съм съвсем сигурен кое е най-чистото решение. Наследената база данни, с която се интегрирам, има странна гънка в схемата си, където една логическа таблица е „разделена“ на няколко физически таблици. Всяка таблица има еднаква структура, но съдържа данни за различни елементи.

Не съм добър в обясняването на това ясно (както можете да разберете!). Нека се опитам да обясня с конкретен пример. Да кажем, че имаме кола, която има едно или повече колела. Обикновено бихме представили това с маса с кола и маса с колела така:

CREATE TABLE cars (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(255),
  ;etc
)

CREATE TABLE wheels (
  `id` int(11) NOT NULL auto_increment,
  `car_id` int(11) NOT NULL,
  `color` varchar(255),
  ;etc
)

Дотук добре. Но със стратегията за „разделяне“, която е в моята наследена база данни, ще изглежда по-скоро като:

CREATE TABLE cars (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(255),
  ;etc
)

CREATE TABLE car_to_wheel_table_map (
  `car_id` int(11) NOT NULL,
  `wheel_table` varchar(255)
)

CREATE TABLE wheels_for_fords (
  `id` int(11) NOT NULL auto_increment,
  `car_id` int(11) NOT NULL,
  `color` varchar(255)
)

CREATE TABLE wheels_for_buicks (
  `id` int(11) NOT NULL auto_increment,
  `car_id` int(11) NOT NULL,
  `color` varchar(255)
)

CREATE TABLE wheels_for_toyotas (
  `id` int(11) NOT NULL auto_increment,
  `car_id` int(11) NOT NULL,
  `color` varchar(255)
)

И така, тук имаме набор от таблици wheels_for_x и таблица car_to_wheel_table_map, която съдържа преобразуване от car_id към конкретния wheels_for_x, който съдържа колелата за конкретна кола. Ако искам да намеря комплекта колела за кола, първо трябва да открия коя таблица с колела да използвам чрез таблицата car_to_wheel_table_map и след това да потърся записи в таблицата с колела, посочени в car_to_wheel_table_map.

Първо, може ли някой да ме просветли дали има стандартно име за тази техника?

Второ, някой има ли насоки как мога да накарам това да работи в Active Record по хубав чист начин. Начинът, по който го виждам, мога или да имам модел на колело, където името на таблицата може да бъде дефинирано за екземпляр, или мога динамично да създавам класове на модела по време на изпълнение с правилното име на таблицата, както е посочено в таблицата за съпоставяне.

РЕДАКТИРАНЕ: Имайте предвид, че промяната на схемата, за да бъде по-близо до това, което AR иска, не е опция. Различни наследени кодови бази разчитат на тази схема и реалистично не могат да бъдат модифицирани.


person Pete Hodgson    schedule 13.05.2009    source източник
comment
В случай, че някой трябва да играе с нещо подобно в бъдеще, вижте тези въпроси и отговори: stackoverflow.com/questions/52537951/   -  person Nick M    schedule 27.09.2018


Отговори (7)


Разделянето на DB таблица наистина е доста често срещана практика. Ще се изненадам, ако някой не е правил това преди. Какво ще кажете за ActsAsPartitionable? http://revolutiononrails.blogspot.com/2007/04/plugin-release-actsaspartitionable.html

Друга възможност: може ли вашата СУБД да се преструва, че дяловете са една голяма маса? Мисля, че MySQL поддържа това.

person Simon Woodside    schedule 13.05.2009
comment
Благодаря, ще го разгледам. Не съм сигурен, че ще отговори напълно на изискванията ми, но може да ми даде някои идеи как да разреша това сам. - person Pete Hodgson; 13.05.2009

Ето начин, по който можете да го направите. Основите (преди 70-те реда код) са:

  • създайте has_many за всеки тип кола
  • дефинирайте метод "wheels", който използва името на таблицата в асоциацията, за да получи правилните колела

Уведомете ме, ако имате въпроси

#!/usr/bin/env ruby
%w|rubygems active_record irb|.each {|lib| require lib}
ActiveSupport::Inflector.inflections.singular("toyota", "toyota")
CAR_TYPES = %w|ford buick toyota|

ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.establish_connection(
  :adapter => "sqlite3",
  :database => ":memory:"
)

ActiveRecord::Schema.define do
  create_table :cars do |t|
    t.string :name
  end

  create_table :car_to_wheel_table_map, :id => false do |t|
    t.integer :car_id
    t.string :wheel_table
  end

  CAR_TYPES.each do |car_type|
    create_table "wheels_for_#{car_type.pluralize}" do |t|
      t.integer :car_id
      t.string :color
    end
  end
end

CAR_TYPES.each do |car_type|
  eval <<-END
    class #{car_type.classify}Wheel < ActiveRecord::Base
      set_table_name "wheels_for_#{car_type.pluralize}"
      belongs_to :car
    end
  END
end

class Car < ActiveRecord::Base
  has_one :car_wheel_map

  CAR_TYPES.each do |car_type|
    has_many "#{car_type}_wheels"
  end

  delegate :wheel_table, :to => :car_wheel_map

  def wheels
    send("#{wheel_table}_wheels")
  end
end

class CarWheelMap < ActiveRecord::Base
  set_table_name "car_to_wheel_table_map"
  belongs_to :car
end


rav4 = Car.create(:name => "Rav4")
rav4.create_car_wheel_map(:wheel_table => "toyota")
rav4.wheels.create(:color => "red")

fiesta = Car.create(:name => "Fiesta")
fiesta.create_car_wheel_map(:wheel_table => "ford")
fiesta.wheels.create(:color => "green")

IRB.start if __FILE__ == $0
person Erik Kastner    schedule 13.05.2009
comment
Благодаря Ерик, но за съжаление не знаем имената на масите по време на проектиране. Всъщност трябва да поддържаме добавянето на нови таблици към системата, докато моето приложение Rails работи. Мисля, че всяко решение трябва динамично да открива имената на таблиците по време на изпълнение. - person Pete Hodgson; 13.05.2009
comment
Харесва ми отговора ти :). @Pete: може би бихте могли да създадете CAR_TYPES динамично и след това да презаредите тази част от кода? Не знам как да го направя, но мисля, че е възможно с ruby. Btw тази схема е наистина луда. - person klew; 14.05.2009
comment
klew.. всъщност схемата е доста често срещана в системи, където размерът на таблицата е ограничен (мобилни устройства) и където данните от задния край трябва да поддържат споменатите устройства. Освен това намалява времето за заявка и натоварването на db, за да направите това.. направете групиране на таблица с 1 милион записа.. след това направете същата заявка на таблица с 10 милиона. - person baash05; 01.12.2011

Какво ще кажете за това вместо това? (ето основното: http://gist.github.com/111041)

#!/usr/bin/env ruby
%w|rubygems active_record irb|.each {|lib| require lib}
ActiveSupport::Inflector.inflections.singular("toyota", "toyota")

ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.establish_connection(
  :adapter => "sqlite3",
  :database => ":memory:"
)

ActiveRecord::Schema.define do
  create_table :cars do |t|
    t.string :name
  end

  create_table :car_to_wheel_table_map, :id => false do |t|
    t.integer :car_id
    t.string :wheel_table
  end

  create_table :wheels_for_fords do |t|
    t.integer :car_id
    t.string :color
  end

  create_table :wheels_for_toyotas do |t|
    t.integer :car_id
    t.string :color
  end
end

class Wheel < ActiveRecord::Base
  set_table_name nil
  belongs_to :car
end

class CarWheelMap < ActiveRecord::Base
  set_table_name "car_to_wheel_table_map"
  belongs_to :car
end

class Car < ActiveRecord::Base
  has_one :car_wheel_map
  delegate :wheel_table, :to => :car_wheel_map

  def wheels
    @wheels ||= begin
      the_klass = "#{wheel_table.classify}Wheel"
      eval <<-END
        class #{the_klass} < ActiveRecord::Base
          set_table_name "wheels_for_#{wheel_table.pluralize}"
          belongs_to :car
        end
      END

      self.class.send(:has_many, "#{wheel_table}_wheels")
      send "#{wheel_table}_wheels"
    end
  end
end

rav4 = Car.create(:name => "Rav4")
rav4.create_car_wheel_map(:wheel_table => "toyota")

fiesta = Car.create(:name => "Fiesta")
fiesta.create_car_wheel_map(:wheel_table => "ford")

rav4.wheels.create(:color => "red")
fiesta.wheels.create(:color => "green")

# IRB.start if __FILE__ == $0
person Erik Kastner    schedule 13.05.2009

Бих направил това свързване с персонализирана функция в модела:

has_one :cat_to_wheel_table_map

def wheels
  Wheel.find_by_sql("SELECT * FROM #{cat_to_wheel_table_map.wheel_table} WHERE car_id == #{id}")
end

Може би можете да го направите, като използвате асоцииране с :finder_sql, но не съм сигурен как да му предам аргументи. Използвах модел Wheel, който трябва да дефинирате, ако искате вашите данни да бъдат картографирани от ActiveRecord. Вероятно можете да направите този модел от някоя от вашите съществуващи маси с колела.

И не съм го тествал ;).

person klew    schedule 13.05.2009
comment
Изглежда малко крехко да има твърд кодиран SQL там. Освен това това ме възпрепятства да използвам някоя от хубавите магии за асоцииране на AR (напр. my_car.wheels.create, my_car.wheels ‹‹ some_wheel и т.н. - person Pete Hodgson; 13.05.2009
comment
Да.. Първото ми обаче беше как да редактирам записи с този метод. - person baash05; 01.12.2011

За съжаление познавам вашите проблеми. Който се е сетил да раздели таблиците във вашата база данни, сигурно е бил уволнен от вашия шеф и нает от моя ;-)

Както и да е, решението (чрез RailsForum): http://railsforum.com/viewtopic.php?id=674

е да използвате магическите модели на д-р Ник.

наздраве

person Community    schedule 28.06.2009

Предположение 1: знаете какъв тип кола гледате, така че можете да разберете дали е форд или додж.

Нека поставим марката на колата в атрибут, наречен (от всички неща) марка. Трябва да нормализирате това по-късно, но засега нека бъде просто.

CREATE TABLE cars (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(255),
  'make' varchar(255),
  #ect
)


class Wheel < ActiveRecord::Base
   def find_by_make(iMake)
       select("wheels_for_#{iMake}.*").from("wheels_for_#{iMake}");
   end
 #...
end

Можете да добавите някаква защита там, за да потвърдите и намалите вашия iMake. Можете също така да направите нещо, за да гарантирате, че вашата маса съществува.

Сега пиша на масата.. Не съм сигурен как ще работи това. Само от него съм чел. Може би също е нещо просто.

person baash05    schedule 30.05.2012
comment
Може да помислите за добавяне на атрибут make в обхвата на класа. Все пак не сте сигурни как ще се играе това с нишки? - person baash05; 30.05.2012

Защо просто не поставите всички колела в една маса и не използвате стандартен :has_many? Можете да го направите в миграция:

  1. създайте нова маса с колела
  2. мигриране на данни от други таблици към новосъздадената таблица
  3. изтрийте старите таблици
person Jon Smock    schedule 13.05.2009
comment
Наследство. Съществуващ, страшен, ужасяващ за промяна код също използва тези таблици. Освен това възможността да разделяте някои много големи таблици в DB по този начин улеснява някои оперативни задачи (промяна на индекси, изтриване на стари данни и т.н.). - person Pete Hodgson; 13.05.2009
comment
Можете ли да ми дадете пример или два за това как кодовата база в момента осъществява достъп до него? Може да успеем да напишем някои методи/named_scopes/нещо, за да симулираме какво прави текущата кодова база. Тогава можете да преработите бавно, вместо да разбивате цялата система с актуализация. - person Jon Smock; 13.05.2009
comment
В играта има различни наследени кодови бази, написани на различни езици по различно време, които може или не могат да използват тези таблици. Никой наистина не знае, оригиналният разработчик(и) отдавна е изчезнал и т.н. и т.н. Наистина не е възможно да се реши този проблем чрез промяна на схемата. И отново, има оперативни причини, поради които може да има смисъл да запазим схемата в игра. - person Pete Hodgson; 13.05.2009