Нечувствительный к регистру уникальный индекс в Rails/ActiveRecord?

Мне нужно создать индекс без учета регистра в столбце в рельсах. Я сделал это через SQL:

execute(
   "CREATE UNIQUE INDEX index_users_on_lower_email_index 
    ON users (lower(email))"
 )

Это отлично работает, но в моем файле schema.rb у меня есть:

add_index "users", [nil], 
  :name => "index_users_on_lower_email_index", 
  :unique => true

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

Спасибо за помощь.

Ответ 1

Поскольку индексы MySQL уже не учитывают регистр, я предполагаю, что вы имеете дело с PostgreSQL, который по умолчанию создает индексы с учетом регистра. Я отвечаю здесь на основе Rails 3.2.3 и PostgreSQL 8.4.

Кажется, функциональные индексы являются еще одним примером того, что ActiveRecord не может сгенерировать. Внешние ключи и столбцы UUID - еще два, которые приходят на ум. Таким образом, нет выбора (кроме использования ActiveRecord для обезьян), но для использования операторов execute.

Это означает, что для точного дампа вашей базы данных вам нужно отказаться от DB-agnostic schema.rb в пользу специфичной для БД структуры .sql. См. Руководство Rails по миграции, раздел 6.2 Типы дампов схемы.. Это устанавливается следующим образом:

конфигурации /application.rb

config.active_record.schema_format = :sql

db/structure.sql следует обновлять автоматически при выполнении миграции. Вы можете сгенерировать его вручную с помощью этой команды:

rake db:structure:dump

Файл является чистым Postgres SQL. Несмотря на то, что вы не указали при использовании rake -T для списка задач рейка, кажется, что вы можете использовать эту команду для загрузки базы данных из файла struct.sql:

rake db:structure:load

Здесь нет ничего волшебного: исходный код просто вызывает psql на struct.sql.

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

class FixEmailUniqueIndexOnUsers < ActiveRecord::Migration
  def up
    remove_index :users, :email
    execute "CREATE UNIQUE INDEX index_users_on_lowercase_email 
             ON users USING btree (lower(email));"
  end

  def down
    execute "DROP INDEX index_users_on_lowercase_email;"
    add_index :users, :email, :unique => true
  end
end


Обновление 4 февраля 2014 года

Исправить неработающие ссылки, заблокировать Rails 3.2.16.

Ответ 2

Если вы используете PostgreSQL, вы можете изменить тип столбца на citext - строку без учета регистра. Он также делает поиск независимым от регистра.

def change
  enable_extension :citext
  change_column :users, :email, :citext
  add_index :users, :email, unique: true
end

Ответ 3

Я бы упростил это...

В вашей модели:

before_validation :downcase_email

def downcase_email
  self.email = email.downcase
end

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

Ответ 4

Считаете ли вы использование schema_plus (https://github.com/lomba/schema_plus)? Среди прочего (поддержка принудительного использования внешних ключей в базе данных и для представлений), он поддерживает установку индексов без учета регистра для баз данных PostgreSQL и обрабатывает их сброс в схеме. Из Readme: "Если вы используете Postgresql, SchemaPlus обеспечивает поддержку условий, выражений, индексных методов и индексов без учета регистра".

Ответ 5

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

def add_index(table_name, column_name, options = {})
  index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
  execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})"
end

Итак, если ваша база данных quote_column_name является реализацией по умолчанию (которая ничего не делает вообще), это может сработать:

add_index "users", ['lower(email)'], :name => "index_users_on_lower_email_index", :unique => true

Вы заметили, что вы пробовали этот, но он не работал (добавив, что к вашему вопросу может быть хорошей идеей). Похоже, ActiveRecord просто не понимает индексы по вычисленному значению. Я могу придумать уродливый хак, который будет сделан, но это уродливо:

  • Добавьте столбец email_lc.
  • Добавьте крючок before_validation или before_save, чтобы поместить нижнюю версию email в email_lc.
  • Поместите свой уникальный индекс на email_lc.

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

Ответ 6

Я бы предложил (как возможность рассмотреть среди других) использовать два отдельных поля:

  • электронная почта
  • email_original

Первый из них всегда сбрасывается и, следовательно, может быть однозначно индексирован в агностической базе данных, а второе поле хранится дословно, что пользователь вводил и может иметь верхние символы.

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

Ответ 7

Мне кажется, вам нужны имена столбцов, как показано ниже

   add_index "users", [email], {:name => "index_users_on_lower_email_index", :unique => true }

И вам нужно создать поле электронной почты в базе данных с правильной сортировкой без учета регистра, таким образом, ваш индекс будет также нечувствительным к регистру.

в зависимости от механизма db, который вы используете, синтаксис может отличаться, но

alter table [users] alter column [email] varchar(250) collate utf8_general_ci ...

и когда вы добавите индекс в этот столбец, он будет нечувствителен к регистру.

Ответ 8

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

Создайте новый файл миграции с пустым методом изменения:

$ rails generate migration add_index_in_users_on_name

Добавить вызов метода add_index для пустого метода изменения:

add_index :users, 'lower(name)', name: 'index_users_on_lower_name', unique: true

Run Rake db: выполнить миграцию задачи:

$ rake db:migrate

В результате индекс будет добавлен правильно, а файл db/schema.rb содержит правильный add_index:

add_index "users", ["LOWER(\"NAME\")"], name: "index_users_on_lower_name", unique: true

Это проверено только с RDB Oracle.