Нечувствительный к регистру уникальный индекс в 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.