はじめに
Django でデータベースバックエンドに MySQL を利用しているとき、CharField や TextField では大文字と小文字を区別しません。例えば「Value A」と「Value a」を区別して取得する場合、
result = Item.objects.filter(value='Value A')
のように取得すると、「Value A」の他に「Value a」や「value a」、「VALUE A」などがあればこれらも取得してしまいます。これは Django が悪いわけではなく、MySQL の挙動が原因です。MySQL で大文字と小文字を区別するためには、VARCHAR や TEXT のカラム定義に BINARY を付ける必要があります。今回、この BINARY を Django で自動的に付けるようにする方法を紹介します。
現象確認
まず、大文字と小文字を区別できない事を確認してみます。モデルとテストを下記の様に実装します。
models.py
from django.db import models
class Item(models.Model):
value = models.CharField(max_length=100)
tests.py
from django.test import TestCase
class ItemModelTests(TestCase):
def test_value_to_distinguish_between_uppercase_and_lowercase(self):
from .models import Item
item1 = Item(value='VALUE A')
item1.save()
item2 = Item(value='value a')
item2.save()
item3 = Item(value='Value A')
item3.save()
item = Item.objects.filter(value='Value A')
self.assertEqual(item.count(), 1)
self.assertNotEqual(item[0].value, 'VALUE A')
self.assertNotEqual(item[0].value, 'value a')
self.assertEqual(item[0].value, 'Value A')
テストを実行すると、期待通り Fail します。取得した結果が 3 件になっている事から、大文字と小文字を区別できていない、と解釈できます。
(venv35)$ python manage.py test
(中略)
F
======================================================================
FAIL: test_value_to_distinguish_between_uppercase_and_lowercase (main.tests.ItemModelTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/user1/work/django1.10/sample/main/tests.py", line 16, in test_value_to_distinguish_between_uppercase_and_lowercase
self.assertEqual(item.count(), 1)
AssertionError: 3 != 1
----------------------------------------------------------------------
Ran 1 test in 0.219s
FAILED (failures=1)
Destroying test database for alias 'default'...
カスタムフィールドの定義
MySQL で実行される、カラム定義に関する SQL は models.Model で定義されたフィールド (CharField や TextField) が元になります。Django がデフォルトで提供している CharField や TextField を拡張して、BINARY キーワードを加えた自前のフィールドクラスを定義します。
models.py
from django.db import models
class CharBinaryField(models.CharField):
def db_type(self, connection):
return super(CharBinaryField, self).db_type(connection) + ' binary'
class Item(models.Model):
value = CharBinaryField(max_length=100)
テストを実行すると、今度は期待通り Pass します。取得した結果が 1 件だけになっている事、Python からみて大文字と小文字の区別がついている事が確認できました。
(venv35)$ python manage.py test
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.204s
OK
Destroying test database for alias 'default'...
おわりに
今回の内容は私が Stack Overflow で質問した内容の解説になります。How to set “BINARY” to VARCHAR column definition for MySQL with django.models?