MySQLでバージョンナンバーをソートする

2.6.4のようなバージョンを表す文字列でソートしたい。

普通にorder byすると「2.0.0」よりも「11.0.0」が小さいと判定されてしまう。

mysql> SELECT version FROM app ORDER BY version DESC;

                    • +
version
                    • +
2.0
11.0
1.1.1
1.0.1
1.0.0
1.0
1
                    • +

7 rows in set (0.00 sec)

各バージョン番号を.でsplitして別々のカラムに保存するのがよいのだけど、そういったカラム追加ができない場合はどうすればよいだろうか


調べた結果、INET_ATONを使用する方法があることがわかった。

INET_ATONはIPv4IPアドレスを数値で返す関数である。

参考:MySQL :: MySQL 5.1 リファレンスマニュアル (オンラインヘルプ) :: 7.11.4 その他の関数

mysql> SELECT version, FROM app ORDER BY INET_ATON(SUBSTRING_INDEX(CONCAT(version,'.0.0.0'),'.',4)) DESC;

                    • +
version
                    • +
11.0
2.0
1.1.1
1.0.1
1.0.0
1.0
1
                    • +

7 rows in set (0.01 sec)


CONCAT、SUBSTRING_INDEX、INET_ATONで値は以下のように変化する。

mysql> SELECT version, CONCAT(version,'.0.0.0'), SUBSTRING_INDEX(CONCAT(version,'.0.0.0'),'.',4), INET_ATON(SUBSTRING_INDEX(CONCAT(version,'.0.0.0'),'.',4)) FROM app ORDER BY INET_ATON(SUBSTRING_INDEX(CONCAT(version,'.0.0.0'),'.',4)) DESC;

                                                                                                                                                                                                                                                                                                        • +
version CONCAT(version,'.0.0.0') SUBSTRING_INDEX(CONCAT(version,'.0.0.0'),'.',4) INET_ATON(SUBSTRING_INDEX(CONCAT(version,'.0.0.0'),'.',4))
                                                                                                                                                                                                                                                                                                        • +
11.0 11.0.0.0.0 11.0.0.0 184549376
2.0 2.0.0.0.0 2.0.0.0 33554432
1.1.1 1.1.1.0.0.0 1.1.1.0 16843008
1.0.1 1.0.1.0.0.0 1.0.1.0 16777472
1.0.0 1.0.0.0.0.0 1.0.0.0 16777216
1.0 1.0.0.0.0 1.0.0.0 16777216
1 1.0.0.0 1.0.0.0 16777216
                                                                                                                                                                                                                                                                                                        • +

7 rows in set (0.00 sec)

本来IPアドレス表現をは変換するための関数なので、各バージョン番号が255を超える場合は、正しい結果にならないことに注意


参考:mysql sorting of version numbers - Stack Overflow

unittest.TestCaseの出力で期待値と実際の値の差分を省略しない

unittest.TestCaseのassertEqualsなどで、期待値と実際の値の差分が大きい場合80*8文字まで表示してあとは省略される

# coding=utf-8
from unittest import TestCase


class DiffTest(TestCase):
    def test_diff(self):
        self.assertEquals(range(0, 1000), range(0, 1001))

実行結果

AssertionError: Lists differ: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,... != [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,...

Second list contains 1 additional elements.
First extra element 1000:
1000

Diff is 7925 characters long. Set self.maxDiff to None to see it.


メッセージにある通りself.maxDiffをNoneに設定することで、省略させないようにすることができる

# coding=utf-8
from unittest import TestCase


class DiffTest(TestCase):

    maxDiff = None

    def test_diff(self):
        self.assertEquals(range(0, 1000), range(0, 1001))

ファイルアップロードでファイル名が重複したときの挙動

最近djangoでファイルアップロードが絡む処理を実装したとき、同一ファイル名に対する挙動が以前と変わっていることに気づいた。


以前はファイル名が重複したときxxxx_1.txt、xxxx_2.txtといった感じで連番が振られていたが、現在はxxxx_8kkVNQf.txtのようにランダムな文字列が振られるようになっている。


そういえばちょっと前にファイルアップロード関連でセキュリティアップデートがでていたけど、ちゃんと読んでなかったので確認する。


JVNDB-2014-003951 - JVN iPedia - 脆弱性対策情報データベース
Security releases issued | Weblog | Django



攻撃者が同名のファイル名を持つファイルを連続してアップロードし続けることによって、サービス運用妨害されるおそれがある。


つまり同名のファイルを10000回アップロードされたら、次のアップロードでは10000回のループが走り、FileSystemStorageを使ってる場合はそのたびにos.statが呼ばれるってことですね。


カスタムストレージを使って、外部のクラウドサービスにファイルを保存している場合は、外部サービスへのアクセスが10000回走ると。


パッチ


django.utils.crypto import get_random_stringを使って7文字のランダム文字列を作成するように変わったわけですね。

get_random_stringはセッションのキーやcsrf_tokenを作成するのにも使われている。

DjangoからAzureストレージにBLOBを保存したときのメモ

django-storagesにAzure用Storageクラスが用意されているので使ってみる。

参考:Azure Storage ― django-storages 1.1.8 documentation

DEFAULT_FILE_STORAGE = 'storages.backends.azure_storage.AzureStorage'
AZURE_ACCOUNT_NAME = ""
AZURE_ACCOUNT_KEY =  ""
AZURE_CONTAINER = ""

適当にImageFieldを持つモデルを用意

class Item(models.Model):
    name = models.CharField(max_length=255)
    photo = models.ImageField(upload_to='item_photo',
                              width_field='width',
                              height_field='height')
    width = models.IntegerField()
    height = models.IntegerField()

ファイル保存のテストクラス

class FileSaveTest(TestCase):
    def test_save(self):
        from items.models import Item

        item = Item(name='dummy')
        image_file = ImageFile(
            open("/path/to/dummy.png", "r"),
            "dummy.png")
        item.photo.save('dummy.png', image_file, save=False)
        item.save()

        self.assertEquals(Item.objects.count(), 1)
        self.assertTrue(Item.objects.get().photo.storage.exists("item_photo/dummy.png"))


実行するとこのようなエラーになった

TypeError: 'NoneType' object has no attribute '__getitem__'

スタックトレースを見ると、保存したファイルのwidth、heightを取得する処理でエラーになっている模様


azureの管理画面を見てみると、BLOBの保存には成功しているが、サイズが9Bなので明らかにおかしい。
'dummy.png'の9文字で9 byteっぽい



ソースを追っかけて調べたところ、azureのクライアントライブラリはBLOBとしてbyte列を受け取る仕様になっているのに対し、django-storageのAzureStorageはFileを渡している。

azureのクライアントライブラリはbyte列でない、byte列にencodeできない入力は無理やりstr(request_body)で評価した結果を保存するのでこのような結果になったようだ。


これじゃ使えないだろ、と思ってリポジトリ調べたらやはりFileからreadしたデータを使うようにプルリクエストがでていた。
david / django-storages / Pull request #76: Fixed Azure backend. url is absolute. Content is stored correctly. ― Bitbucket


プルリクエストを参考にして修正したStorageクラスを使ったら、BLOBが保存できたのでとりあえずめでたしめでたし。

AppRegistryNotReady: Models aren't loaded yet.

Pycharmの「Run manage.py Task」から「shell」を起動してmodelsをimportしたら以下のエラーになった

AppRegistryNotReady: Models aren't loaded yet.

Djangoのバージョンは1.7。
runserverしてアプリを動かす分にはエラーにならないし、Pycharmを通さずTerminalからshellを起動しても再現しない。

とりあえずStackOverFlowを参考にして、以下を実行すればshellを使えるようになった

import django
django.setup()


参考:python - MODELS AREN'T LOADED YET error while populating in django1.8 and python2.7.8 - Stack Overflow