การค้นหาแบบตรงตามตัวพิมพ์ใหญ่ - เล็กในโมเดล Rails


211

รุ่นผลิตภัณฑ์ของฉันมีบางรายการ

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

ตอนนี้ฉันกำลังนำเข้าพารามิเตอร์ผลิตภัณฑ์จากชุดข้อมูลอื่น แต่มีความไม่สอดคล้องกันในการสะกดชื่อ ตัวอย่างเช่นในชุดข้อมูลอื่นBlue jeansอาจสะกดBlue Jeansได้

ฉันต้องการProduct.find_or_create_by_name("Blue Jeans")แต่สิ่งนี้จะสร้างผลิตภัณฑ์ใหม่เกือบเหมือนผลิตภัณฑ์แรก ตัวเลือกของฉันคืออะไรหากฉันต้องการค้นหาและเปรียบเทียบชื่อที่ลดระดับลง

ปัญหาด้านประสิทธิภาพไม่ใช่เรื่องสำคัญจริง ๆ ที่นี่: มีผลิตภัณฑ์เพียง 100-200 รายการและฉันต้องการเรียกใช้สิ่งนี้เป็นการย้ายข้อมูลที่นำเข้าข้อมูล

ความคิดใด ๆ

คำตอบ:


368

คุณอาจจะต้อง verbose เพิ่มเติมที่นี่

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

5
ความคิดเห็นของ @ botbot ไม่ได้ใช้กับสตริงจากอินพุตของผู้ใช้ "# $$" เป็นช็อตคัตที่รู้จักกันน้อยในการหลบหนีตัวแปรกลางด้วยการแก้ไขสตริงของรูบี มันเทียบเท่ากับ "# {$$}" แต่การแก้ไขสตริงไม่ได้เกิดขึ้นกับสตริงที่ผู้ใช้ป้อน ลองเหล่านี้ใน Irb เห็นความแตกต่าง: และ"$##" '$##'ตัวแรกจะถูกสอดแทรก (เครื่องหมายคำพูดคู่) ที่สองไม่ได้ การป้อนข้อมูลของผู้ใช้จะไม่ได้รับการแก้ไข
Brian Morearty

5
เพียงเพื่อให้ทราบว่าจะเลิกและตัวเลือกตอนนี้คือการใช้งานfind(:first) #firstดังนั้นProduct.first(conditions: [ "lower(name) = ?", name.downcase ])
Luís Ramalho

2
คุณไม่จำเป็นต้องทำงานทั้งหมดนี้ ใช้ห้องสมุด Arel ในตัวหรือ Squeel
Dogweather

17
ใน Rails 4 คุณสามารถทำได้model = Product.where('lower(name) = ?', name.downcase).first_or_create
Derek Lucas

1
@DerekLucas แม้ว่าจะเป็นไปได้ที่จะทำใน Rails 4 วิธีนี้อาจทำให้เกิดพฤติกรรมที่ไม่คาดคิด สมมติว่าเราได้after_createโทรกลับในProductรูปแบบและภายในโทรกลับเรามีข้อเช่นwhere products = Product.where(country: 'us')ในกรณีนี้ส่วนwhereคำสั่งจะถูกล่ามโซ่เนื่องจากการเรียกกลับดำเนินการภายในบริบทของขอบเขต เพียงแค่ FYI
elquimista

100

นี่เป็นการตั้งค่าที่สมบูรณ์ใน Rails สำหรับการอ้างอิงของฉันเอง ฉันดีใจที่มันช่วยคุณเช่นกัน

แบบสอบถาม:

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

เครื่องมือตรวจสอบ:

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

ดัชนี (คำตอบจากดัชนีเฉพาะกรณีเล็ก ๆ น้อย ๆ ใน Rails / ActiveRecord? ):

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

ฉันหวังว่าจะมีวิธีที่สวยงามกว่าในการทำครั้งแรกและครั้งสุดท้าย แต่จากนั้นอีกครั้ง Rails และ ActiveRecord เป็นโอเพ่นซอร์สเราไม่ควรบ่น - เราสามารถดำเนินการด้วยตนเองและส่งคำขอดึง


6
ขอขอบคุณสำหรับเครดิตในการสร้างดัชนีตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ใน PostgreSQL เครดิตกลับไปหาคุณเพื่อแสดงวิธีใช้ใน Rails! หมายเหตุเพิ่มเติมอีกประการหนึ่ง: หากคุณใช้ตัวค้นหามาตรฐานเช่น find_by_name มันจะยังคงเป็นการจับคู่ที่ตรงกัน คุณต้องเขียนตัวค้นหาที่กำหนดเองซึ่งคล้ายกับบรรทัด "คิวรี" ของคุณด้านบนหากคุณต้องการให้การค้นหาของคุณไม่ตรงตามตัวพิมพ์ใหญ่ - เล็ก
Mark Berry

เมื่อพิจารณาแล้วว่า find(:first, ...)เลิกใช้แล้วฉันคิดว่านี่เป็นคำตอบที่เหมาะสมที่สุด
ผู้ใช้

จำเป็นต้องใช้ name.downcase หรือไม่ ดูเหมือนว่าจะทำงานกับProduct.where("lower(name) = ?", name).first
จอร์แดน

1
@Jordan คุณลองด้วยชื่อที่มีอักษรตัวใหญ่หรือไม่?
oma

1
@Jordan อาจจะไม่สำคัญมากเกินไป แต่เราควรมุ่งมั่นเพื่อความถูกต้องในดังนั้นในขณะที่เรามีการช่วยเหลือผู้อื่น :)
OMA

28

หากคุณใช้ Postegres และ Rails 4+ คุณมีตัวเลือกในการใช้คอลัมน์ประเภท CITEXT ซึ่งจะอนุญาตให้มีการค้นหาแบบตัวพิมพ์เล็กและตัวพิมพ์ใหญ่โดยไม่ต้องเขียนตรรกะการสืบค้นออก

การโยกย้าย:

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

และเพื่อทดสอบคุณควรคาดหวังสิ่งต่อไปนี้:

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

คุณอาจต้องการใช้สิ่งต่อไปนี้:

validates_uniqueness_of :name, :case_sensitive => false

โปรดทราบว่าโดยค่าเริ่มต้นการตั้งค่าคือ: case_sensitive => false ดังนั้นคุณไม่จำเป็นต้องเขียนตัวเลือกนี้หากคุณไม่ได้เปลี่ยนวิธีอื่น

ค้นหาเพิ่มเติมได้ที่: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of


5
จากประสบการณ์ของฉันซึ่งแตกต่างจากเอกสารคู่มือ case_sensitive เป็นจริงโดยค่าเริ่มต้น ฉันเห็นว่าพฤติกรรมใน postgresql และอื่น ๆ ได้รายงานเหมือนกันใน mysql
ทรอย

1
ดังนั้นฉันจึงลองกับ postgres และใช้ไม่ได้ find_by_x คำนึงถึงตัวพิมพ์เล็กและตัวพิมพ์ใหญ่โดยไม่คำนึงว่า ...
Louis Sayers

การตรวจสอบความถูกต้องนี้จะเกิดขึ้นเฉพาะเมื่อสร้างโมเดล ดังนั้นหากคุณมี 'HAML' ในฐานข้อมูลของคุณและคุณลองเพิ่ม 'haml' มันจะไม่ผ่านการตรวจสอบ
Dudo

14

ในภายหลัง:

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

1
Rails บน Heroku ดังนั้นการใช้ Postgres ... ILIKE นั้นยอดเยี่ยม ขอบคุณ!
FeifanZ

ใช้ ILIKE อย่างแน่นอนบน PostgreSQL
Dom

12

ความคิดเห็นหลายข้ออ้างถึง Arel โดยไม่แสดงตัวอย่าง

นี่คือตัวอย่าง Arel ของการค้นหาแบบคำนึงถึงขนาดตัวพิมพ์:

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

ข้อดีของการแก้ปัญหาประเภทนี้คือมันไม่เชื่อเรื่องฐานข้อมูล - มันจะใช้คำสั่ง SQL ที่ถูกต้องสำหรับอะแดปเตอร์ปัจจุบันของคุณ ( matchesจะใช้ILIKEสำหรับ Postgres และLIKEทุกอย่างอื่น)


9

การอ้างอิงจากเอกสารคู่มือSQLite :

อักขระอื่นใดที่ตรงกับตัวเองหรือเทียบเท่ากับตัวพิมพ์เล็ก / ใหญ่ (เช่นการจับคู่ตัวพิมพ์เล็กและตัวพิมพ์ใหญ่)

... ที่ฉันไม่รู้ แต่ใช้งานได้:

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

ดังนั้นคุณสามารถทำสิ่งนี้:

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

ไม่#find_or_createฉันรู้และอาจไม่เป็นมิตรกับฐานข้อมูลมาก แต่ควรดูใช่ไหม


1
ชอบเป็นกรณี ๆ ไปใน mysql แต่ไม่ได้อยู่ใน postgresql ฉันไม่แน่ใจเกี่ยวกับ Oracle หรือ DB2 ประเด็นคือคุณไม่สามารถนับมันได้และถ้าคุณใช้มันและเจ้านายของคุณเปลี่ยนฐานข้อมูลพื้นฐานของคุณคุณจะเริ่มบันทึก "ขาดหายไป" โดยไม่มีเหตุผลชัดเจน คำแนะนำ (ชื่อ) ที่ต่ำกว่าของ @ neutrino น่าจะเป็นวิธีที่ดีที่สุดในการแก้ไขปัญหานี้
masukomi

6

อีกวิธีที่ไม่มีใครพูดถึงคือการเพิ่มตัวค้นหาที่ไม่สนใจขนาดตัวพิมพ์ใน ActiveRecord :: Base รายละเอียดสามารถพบได้ที่นี่ ข้อดีของวิธีนี้คือคุณไม่ต้องแก้ไขทุกรุ่นและคุณไม่จำเป็นต้องเพิ่มส่วนlower()คำสั่งลงในแบบสอบถามที่ไม่ตอบสนองต่อกรณีทั้งหมดของคุณคุณเพียงแค่ใช้วิธีการค้นหาแบบต่างๆแทน


เมื่อหน้าที่คุณลิงก์ตายไปคำตอบของคุณก็เช่นกัน
Anthony

อย่างที่ @Anony ได้พยากรณ์ไว้ดังนั้นมันจะเกิดขึ้น ลิงค์ตาย
XP84

3
@ XP84 ฉันไม่รู้ว่ามันเกี่ยวข้องกันแค่ไหน แต่ฉันได้แก้ไขลิงก์แล้ว
Alex Korban

6

ตัวอักษรพิมพ์ใหญ่และตัวพิมพ์เล็กแตกต่างกันเพียงเล็กน้อย วิธีที่มีประสิทธิภาพมากที่สุดในการค้นหาคือละเว้นบิตนี้ไม่ใช่แปลงต่ำหรือสูงเป็นต้นดูคำหลักCOLLATIONสำหรับ MSSQL ดูNLS_SORT=BINARY_CIว่าใช้ Oracle หรือไม่เป็นต้น


4

ตอนนี้ Find_or_create เลิกใช้แล้วคุณควรใช้ AR Relation แทนพร้อมกับ first_or_create เช่น:

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

สิ่งนี้จะคืนค่าวัตถุแรกที่จับคู่หรือสร้างวัตถุให้คุณหากไม่มี



2

มีคำตอบที่ดีมากมายที่นี่โดยเฉพาะ @ oma แต่สิ่งหนึ่งที่คุณลองได้ก็คือใช้การจัดลำดับคอลัมน์แบบกำหนดเอง หากคุณไม่คิดว่าทุกอย่างจะถูกจัดเก็บไว้ในฐานข้อมูลของคุณเล็กคุณสามารถสร้าง:

# 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

จากนั้นในรูปแบบของคุณ:

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

ประโยชน์ของวิธีนี้คือคุณยังสามารถใช้งาน Finders ปกติทั้งหมด (รวมถึงfind_or_create_by) โดยไม่ต้องใช้ขอบเขตฟังก์ชั่นที่กำหนดเองหรือมีlower(name) = ?ในแบบสอบถามของคุณ

ข้อเสียคือการที่คุณสูญเสียข้อมูลเคสในฐานข้อมูล


2

คล้ายกับแอนดรูซึ่งเป็น # 1:

สิ่งที่เหมาะกับฉันคือ:

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

สิ่งนี้ไม่จำเป็นต้องทำ#whereและ#firstในแบบสอบถามเดียวกัน หวังว่านี่จะช่วยได้!


1

คุณยังสามารถใช้ขอบเขตเช่นนี้ด้านล่างและวางไว้ในข้อกังวลและรวมไว้ในโมเดลที่คุณอาจต้องการ:

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

จากนั้นใช้ดังนี้: Model.ci_find('column', 'value')



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

TypeError: Cannot visit Regexp
Dorian

@ shilovk ขอบคุณ นี่คือสิ่งที่ฉันกำลังมองหา และมันดูดีกว่าคำตอบที่ได้รับการยอมรับstackoverflow.com/a/2220595/1380867
MZaragoza

ฉันชอบโซลูชันนี้ แต่คุณได้รับข้อผิดพลาด "ไม่สามารถไปที่ Regexp" ได้อย่างไร ฉันก็เห็นเช่นกัน
แกรี

0

บางคนแสดงโดยใช้ LIKE หรือ ILIKE แต่อนุญาตให้ทำการค้นหา regex นอกจากนี้คุณไม่จำเป็นต้อง downcase ใน Ruby คุณสามารถปล่อยให้ฐานข้อมูลทำเพื่อคุณ ฉันคิดว่ามันอาจจะเร็วกว่า นอกจากนี้ยังสามารถนำมาใช้หลังจากที่first_or_createwhere

# 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

จนถึงตอนนี้ฉันได้แก้ปัญหาโดยใช้ Ruby วางสิ่งนี้ไว้ในรูปแบบผลิตภัณฑ์:

  #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

นี่จะให้ผลิตภัณฑ์แรกที่ชื่อตรงกับฉัน หรือไม่มี

>> 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
นั่นเป็นสิ่งที่ไม่มีประสิทธิภาพอย่างยิ่งสำหรับชุดข้อมูลที่มีขนาดใหญ่เนื่องจากจะต้องโหลดทุกสิ่งลงในหน่วยความจำ แม้ว่าจะไม่ใช่ปัญหาสำหรับคุณที่มีเพียงไม่กี่ร้อยรายการ แต่นี่ไม่ใช่วิธีปฏิบัติที่ดี
lambshaanxy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.