Добавление логики обновления в ваши миграции

Пару раз я был в ситуации, когда хотел реорганизовать дизайн какой-то модели и в итоге добавлял логику обновления в миграцию. Однако, насколько я понял, это не очень хорошая практика (тем более, что вам рекомендуется использовать файл схемы для развертывания, а не ваши миграции). Как вы справляетесь с такими проблемами?

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

Однако после того, как я подумал, что мне нужен какой-то третий тип пользователя, возможно, модератор или что-то подобное. В этом случае я добавляю модель UserType (и соответствующую миграцию) и вторую миграцию для удаления флага «admin» из пользовательской таблицы. И тут возникает проблема. В миграции «add_user_type_to_users» мне нужно сопоставить значение флага администратора с типом пользователя. Кроме того, для этого должны существовать типы пользователей, то есть я не могу использовать файл seed, а скорее создаю типы пользователей при миграции (это также считается плохой практикой). Вот какой-то вымышленный код, представляющий ситуацию:

class CreateUserTypes < ActiveRecord::Migration
    def self.up
        create_table :user_types do |t|
            t.string :name, :nil => false, :unique => true
        end

        #Create basic types (can not put in seed, because of future migration dependency)
        UserType.create!(:name => "BASIC")
        UserType.create!(:name => "MODERATOR")
        UserType.create!(:name => "ADMINISTRATOR")
    end

    def self.down
        drop_table :user_types
    end
end

class AddTypeIdToUsers < ActiveRecord::Migration
    def self.up
        add_column :users, :type_id, :integer

        #Determine type via the admin flag
        basic = UserType.find_by_name("BASIC")
        admin = UserType.find_by_name("ADMINISTRATOR")
        User.all.each {|u| u.update_attribute(:type_id, (u.admin?) ? admin.id : basic.id)}

        #Remove the admin flag
        remove_column :users, :admin

        #Add foreign key
        execute "alter table users add constraint fk_user_type_id
            foreign key (type_id) references user_types (id)"
    end

    def self.down
        #Re-add the admin flag
        add_column :users, :admin, :boolean, :default => false

        #Reset the admin flag (this is the problematic update code)
        admin = UserType.find_by_name("ADMINISTRATOR")

        execute "update users set admin=true where type_id=#{admin.id}"

        #Remove foreign key constraint
        execute "alter table users drop foreign key fk_user_type_id"

        #Drop the type_id column
        remove_column :users, :type_id
    end
end

Как видите, есть две проблемные части. Сначала часть создания строки в первой модели, которая необходима, если я хочу запустить все миграции подряд, затем часть «обновления» во второй миграции, которая сопоставляет столбец «admin» со столбцом «type_id».

Любой совет?


person Daniel Abrahamsson    schedule 22.05.2010    source источник


Ответы (2)


Я нахожу более «нетрадиционным» то, что вы используете fk, чем то, что вы загружаете UserType со старым User.admin, что, я думаю, происходит довольно часто.

Если вы используете fk, вы получаете уродливые ошибки mysql, которые сбивают пользователя с толку. Если в противном случае вы используете проверки и перехватчики AR для обеспечения ссылочной целостности, вы получаете красивые и хорошо интегрированные сообщения об ошибках, которые не нарушают поток взаимодействия с пользователем вашего приложения.

Не беспокойтесь о миграции, которая будет выполняться один раз, и подумайте о бизнес-логике, которую вы размещаете вне своего кода.

Это все вопрос мнения/соглашения, но я надеюсь, что вы найдете мои идеи полезными.

person Oinak    schedule 22.05.2010
comment
Когда дело доходит до внешних ключей, я считаю их весьма полезными, особенно при работе с важными для бизнеса данными. Это помогает мне избежать висячих данных и определить, не пропустил ли я применение хуков и т.п. со стороны Rails. Хотя вы правы, они могут выдавать вам уродливые сообщения об ошибках. - person Daniel Abrahamsson; 22.05.2010
comment
Я вижу, это вопрос выбора. если целостность данных за пределами резервных копий выше UX, fk — это то, что нужно. В этом случае у вас есть плагин для управления fk из ваших миграций: agilewebdevelopment.com/plugins/foreign_key_migrations - person Oinak; 22.05.2010

Для этой цели обычно используется файл db/seeds.rb — размещенные в нем записи будут загружены как часть rake db:setup

Тем не менее, я всегда находил, что рельсы падают из-за этой проблемы. Я думал о написании плагина, который дает вам папку db/seeds, имеет начальные файлы с отметками даты для добавления записей (возможно, .yml) и отслеживает начальные данные в системной таблице, чтобы их можно было отменить/обновить.

person Daniel Heath    schedule 23.05.2010
comment
Как видно по коду, я не могу использовать seed, так как хочу применить сразу несколько миграций, а последняя миграция зависит от того, есть ли данные в базе. Было бы неплохо, если бы у вас был один начальный файл для каждой миграции, и чтобы начальные файлы применялись между миграциями. Итак, сначала запускается 20100524...do_something.rb, а затем 20100524...seed_something.rb выполняет необходимое заполнение для этой миграции. - person Daniel Abrahamsson; 24.05.2010