Tìm kiếm không phân biệt chữ hoa chữ thường trong mô hình Rails


211

Mô hình sản phẩm của tôi có chứa một số mặt hàng

 Product.first
 => #<Product id: 10, name: "Blue jeans" >

Tôi hiện đang nhập một số thông số sản phẩm từ một tập dữ liệu khác, nhưng có sự không nhất quán trong cách đánh vần tên. Ví dụ, trong bộ dữ liệu khác, Blue jeanscó thể được đánh vần Blue Jeans.

Tôi muốn Product.find_or_create_by_name("Blue Jeans"), nhưng điều này sẽ tạo ra một sản phẩm mới, gần giống với sản phẩm đầu tiên. Lựa chọn của tôi là gì nếu tôi muốn tìm và so sánh tên dưới.

Vấn đề về hiệu năng không thực sự quan trọng ở đây: Chỉ có 100-200 sản phẩm và tôi muốn chạy nó dưới dạng di chuyển nhập dữ liệu.

Có ý kiến ​​gì không?

Câu trả lời:


368

Có lẽ bạn sẽ phải dài dòng hơn ở đây

name = "Blue Jeans"
model = Product.where('lower(name) = ?', name.downcase).first 
model ||= Product.create(:name => name)

5
Nhận xét của @ botbot không áp dụng cho chuỗi từ đầu vào của người dùng. "# $$" là một phím tắt ít được biết đến để thoát các biến toàn cục bằng phép nội suy chuỗi Ruby. Nó tương đương với "# {$$}". Nhưng nội suy chuỗi không xảy ra với chuỗi đầu vào của người dùng. Hãy thử những điều này trong Irb để thấy sự khác biệt: "$##"'$##'. Đầu tiên là nội suy (dấu ngoặc kép). Thứ hai là không. Đầu vào của người dùng không bao giờ được nội suy.
Brian Morearty

5
Chỉ cần lưu ý rằng find(:first)không được dùng nữa, và tùy chọn bây giờ là sử dụng #first. Do đó,Product.first(conditions: [ "lower(name) = ?", name.downcase ])
Luís Ramalho

2
Bạn không cần phải làm tất cả công việc này. Sử dụng thư viện Arel tích hợp hoặc Squeel
Dogweather

17
Trong Rails 4 bây giờ bạn có thể làmmodel = Product.where('lower(name) = ?', name.downcase).first_or_create
Derek Lucas

1
@DerekLucas mặc dù có thể làm như vậy trong Rails 4, phương pháp này có thể gây ra hành vi không mong muốn. Giả sử chúng ta có after_createcuộc gọi lại trong Productmô hình và bên trong cuộc gọi lại, chúng ta có wheremệnh đề, vd products = Product.where(country: 'us'). Trong trường hợp này, các wheremệnh đề được xâu chuỗi khi các cuộc gọi lại thực thi trong bối cảnh của phạm vi. Chỉ cần FYI.
elquimista

100

Đây là một thiết lập hoàn chỉnh trong Rails, để tôi tham khảo. Tôi rất vui nếu nó cũng giúp bạn.

truy vấn:

Product.where("lower(name) = ?", name.downcase).first

trình xác nhận:

validates :name, presence: true, uniqueness: {case_sensitive: false}

chỉ mục (câu trả lời từ chỉ mục duy nhất không phân biệt chữ hoa chữ thường trong Rails / ActiveRecord? ):

execute "CREATE UNIQUE INDEX index_products_on_lower_name ON products USING btree (lower(name));"

Tôi ước có một cách hay hơn để làm đầu tiên và cuối cùng, nhưng sau đó, Rails và ActiveRecord là nguồn mở, chúng tôi không nên phàn nàn - chúng tôi có thể tự thực hiện và gửi yêu cầu kéo.


6
Cảm ơn bạn đã tin tưởng vào việc tạo chỉ mục không phân biệt chữ hoa chữ thường trong PostgreSQL. Tín dụng lại cho bạn để hiển thị cách sử dụng nó trong Rails! Thêm một lưu ý: nếu bạn sử dụng một công cụ tìm tiêu chuẩn, ví dụ find_by_name, thì nó vẫn khớp chính xác. Bạn phải viết các công cụ tìm tùy chỉnh, tương tự như dòng "truy vấn" của bạn ở trên, nếu bạn muốn tìm kiếm của mình không phân biệt chữ hoa chữ thường.
Đánh dấu Berry

Xem xét rằng find(:first, ...)bây giờ không được chấp nhận, tôi nghĩ rằng đây là câu trả lời thích hợp nhất.
người dùng

là tên.downcase cần thiết? Nó dường như hoạt động vớiProduct.where("lower(name) = ?", name).first
Jordan

1
@Jordan bạn đã thử điều đó với tên có chữ in hoa chưa?
oma

1
@Jordan, có lẽ không quá quan trọng, nhưng chúng ta nên cố gắng vì sự chính xác trên SO vì chúng ta đang giúp đỡ người khác :)
oma

28

Nếu bạn đang sử dụng Postegres và Rails 4+, thì bạn có tùy chọn sử dụng loại cột CITEXT, điều này sẽ cho phép các truy vấn không phân biệt chữ hoa chữ thường mà không phải viết ra logic truy vấn.

Di chuyển:

def change
  enable_extension :citext
  change_column :products, :name, :citext
  add_index :products, :name, unique: true # If you want to index the product names
end

Và để kiểm tra nó, bạn nên mong đợi như sau:

Product.create! name: 'jOgGers'
=> #<Product id: 1, name: "jOgGers">

Product.find_by(name: 'joggers')
=> #<Product id: 1, name: "jOgGers">

Product.find_by(name: 'JOGGERS')
=> #<Product id: 1, name: "jOgGers">

21

Bạn có thể muốn sử dụng như sau:

validates_uniqueness_of :name, :case_sensitive => false

Xin lưu ý rằng theo mặc định, cài đặt là: case_sensitive => false, do đó bạn thậm chí không cần phải viết tùy chọn này nếu bạn không thay đổi các cách khác.

Tìm thêm tại: http://api.rubyonrails.org/groupes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniquety_of


5
Theo kinh nghiệm của tôi, trái ngược với tài liệu, case_sensitive là đúng theo mặc định. Tôi đã thấy rằng hành vi trong postgresql và những người khác đã báo cáo tương tự trong mysql.
Troy

1
Vì vậy, tôi đang thử điều này với postgres, và nó không hoạt động. find_by_x phân biệt chữ hoa chữ thường bất kể ...
Louis Sayers

Xác nhận này chỉ khi tạo mô hình. Vì vậy, nếu bạn có 'HAML' trong cơ sở dữ liệu của mình và bạn cố gắng thêm 'haml', nó sẽ không vượt qua được xác nhận.
Dudo

14

Trong postgres:

 user = User.find(:first, :conditions => ['username ~* ?', "regedarek"])

1
Rails trên Heroku, vì vậy sử dụng Postgres 'ILIKE là tuyệt vời. Cảm ơn bạn!
FeifanZ

Chắc chắn sử dụng ILIKE trên PostgreSQL.
Dom

12

Một số ý kiến ​​đề cập đến Arel, mà không cung cấp một ví dụ.

Dưới đây là một ví dụ Arel về tìm kiếm không phân biệt chữ hoa chữ thường:

Product.where(Product.arel_table[:name].matches('Blue Jeans'))

Ưu điểm của loại giải pháp này là không dựa trên cơ sở dữ liệu - nó sẽ sử dụng các lệnh SQL chính xác cho bộ điều hợp hiện tại của bạn ( matchessẽ sử dụng ILIKEcho Postgres và LIKEcho mọi thứ khác).


9

Trích dẫn từ tài liệu SQLite :

Bất kỳ ký tự nào khác khớp với chính nó hoặc tương đương chữ thường / chữ thường (nghĩa là khớp không phân biệt chữ hoa chữ thường)

... mà tôi không biết. Nhưng nó hoạt động:

sqlite> create table products (name string);
sqlite> insert into products values ("Blue jeans");
sqlite> select * from products where name = 'Blue Jeans';
sqlite> select * from products where name like 'Blue Jeans';
Blue jeans

Vì vậy, bạn có thể làm một cái gì đó như thế này:

name = 'Blue jeans'
if prod = Product.find(:conditions => ['name LIKE ?', name])
    # update product or whatever
else
    prod = Product.create(:name => name)
end

Không #find_or_create, tôi biết, và nó có thể không thân thiện với cơ sở dữ liệu chéo, nhưng đáng xem?


1
giống như trường hợp nhạy cảm trong mysql nhưng không phải trong postgresql. Tôi không chắc chắn về Oracle hay DB2. Vấn đề là, bạn không thể tin vào điều đó và nếu bạn sử dụng nó và ông chủ của bạn thay đổi db cơ bản của bạn, bạn sẽ bắt đầu có các bản ghi "thiếu" mà không có lý do rõ ràng tại sao. Đề xuất (tên) thấp hơn của @ neutrino có lẽ là cách tốt nhất để giải quyết vấn đề này.
masukomi

6

Một cách tiếp cận khác mà không ai đề cập đến là thêm các công cụ tìm không phân biệt chữ hoa chữ thường vào ActiveRecord :: Base. Chi tiết có thể được tìm thấy ở đây . Ưu điểm của phương pháp này là bạn không phải sửa đổi mọi mô hình và bạn không phải thêm lower()mệnh đề vào tất cả các truy vấn không nhạy cảm trong trường hợp của mình, thay vào đó bạn chỉ sử dụng một phương pháp tìm khác.


khi trang bạn liên kết chết, câu trả lời của bạn cũng vậy.
Anthony

Như @Anthony đã tiên tri, vì vậy nó đã đi qua. Liên kết chết.
XP84

3
@ XP84 Tôi không biết điều này có liên quan như thế nào nữa, nhưng tôi đã sửa liên kết.
Alex Korban

6

Chữ in hoa và in thường chỉ khác nhau một bit. Cách hiệu quả nhất để tìm kiếm chúng là bỏ qua bit này, không chuyển đổi thấp hơn hoặc cao hơn, v.v ... Xem từ khóa COLLATIONcho MSSQL, xem NLS_SORT=BINARY_CInếu sử dụng Oracle, v.v.


4

Find_or_create hiện không được dùng nữa, bạn nên sử dụng AR Relation thay vì First_or_create, như vậy:

TombolaEntry.where("lower(name) = ?", self.name.downcase).first_or_create(name: self.name)

Điều này sẽ trả về đối tượng phù hợp đầu tiên hoặc tạo một đối tượng cho bạn nếu không tồn tại.



2

Có rất nhiều câu trả lời tuyệt vời ở đây, đặc biệt là @ oma's. Nhưng một điều khác bạn có thể thử là sử dụng tuần tự hóa cột tùy chỉnh. Nếu bạn không nhớ mọi thứ được lưu trữ bằng chữ thường trong db của mình thì bạn có thể tạo:

# lib/serializers/downcasing_string_serializer.rb
module Serializers
  class DowncasingStringSerializer
    def self.load(value)
      value
    end

    def self.dump(value)
      value.downcase
    end
  end
end

Sau đó, trong mô hình của bạn:

# app/models/my_model.rb
serialize :name, Serializers::DowncasingStringSerializer
validates_uniqueness_of :name, :case_sensitive => false

Lợi ích của phương pháp này là bạn vẫn có thể sử dụng tất cả các công cụ tìm thông thường (bao gồm find_or_create_by) mà không cần sử dụng phạm vi tùy chỉnh, chức năng hoặc có lower(name) = ?trong các truy vấn của mình.

Nhược điểm là bạn mất thông tin vỏ trong cơ sở dữ liệu.


2

Tương tự như Andrew là # 1:

Một cái gì đó làm việc cho tôi là:

name = "Blue Jeans"
Product.find_by("lower(name) = ?", name.downcase)

Điều này loại bỏ sự cần thiết phải làm một #where#firsttrong cùng một truy vấn. Hi vọng điêu nay co ich!


1

Bạn cũng có thể sử dụng phạm vi như thế này bên dưới và đặt chúng vào mối quan tâm và bao gồm trong các mô hình bạn có thể cần chúng:

scope :ci_find, lambda { |column, value| where("lower(#{column}) = ?", value.downcase).first }

Sau đó sử dụng như thế này: Model.ci_find('column', 'value')



0
user = Product.where(email: /^#{email}$/i).first

TypeError: Cannot visit Regexp
Dorian

@shilovk cảm ơn. Điều này thật đúng với gì mà tôi đã tìm kiếm. Và nó có vẻ tốt hơn câu trả lời được chấp nhận stackoverflow.com/a/2220595/1380867
MZaragoza

Tôi thích giải pháp này, nhưng làm thế nào bạn vượt qua được lỗi "Không thể truy cập Regapi"? Tôi cũng đang thấy điều đó
Gayle

0

Một số người hiển thị bằng cách sử dụng THÍCH hoặc ILIKE, nhưng những người đó cho phép tìm kiếm regex. Ngoài ra, bạn không cần phải viết hoa trong Ruby. Bạn có thể để cơ sở dữ liệu làm điều đó cho bạn. Tôi nghĩ rằng nó có thể nhanh hơn. Cũng first_or_createcó thể được sử dụng sau where.

# app/models/product.rb
class Product < ActiveRecord::Base

  # case insensitive name
  def self.ci_name(text)
    where("lower(name) = lower(?)", text)
  end
end

# first_or_create can be used after a where clause
Product.ci_name("Blue Jeans").first_or_create
# Product Load (1.2ms)  SELECT  "products".* FROM "products"  WHERE (lower(name) = lower('Blue Jeans'))  ORDER BY "products"."id" ASC LIMIT 1
# => #<Product id: 1, name: "Blue jeans", created_at: "2016-03-27 01:41:45", updated_at: "2016-03-27 01:41:45"> 


-9

Cho đến nay, tôi đã thực hiện một giải pháp bằng Ruby. Đặt cái này bên trong mô hình Sản phẩm:

  #return first of matching products (id only to minimize memory consumption)
  def self.custom_find_by_name(product_name)
    @@product_names ||= Product.all(:select=>'id, name')
    @@product_names.select{|p| p.name.downcase == product_name.downcase}.first
  end

  #remember a way to flush finder cache in case you run this from console
  def self.flush_custom_finder_cache!
    @@product_names = nil
  end

Điều này sẽ cho tôi sản phẩm đầu tiên mà tên phù hợp. Hoặc không.

>> Product.create(:name => "Blue jeans")
=> #<Product id: 303, name: "Blue jeans">

>> Product.custom_find_by_name("Blue Jeans")
=> nil

>> Product.flush_custom_finder_cache!
=> nil

>> Product.custom_find_by_name("Blue Jeans")
=> #<Product id: 303, name: "Blue jeans">
>>
>> #SUCCESS! I found you :)

2
Điều đó cực kỳ không hiệu quả đối với một tập dữ liệu lớn hơn, vì nó phải tải toàn bộ vào bộ nhớ. Mặc dù không phải là vấn đề với bạn chỉ với vài trăm mục, đây không phải là cách thực hành tốt.
lambshaanxy
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.