Wed 23 Apr 2008
Для задачи хранения состояний множества элементов иногда используются битовые маски.
Например, о прочтении комментариев к статьям для каждого пользователя. И хотя в этом случае лучше хранить время последнего захода в конкретную тему, но могут возникнуть и другие задачи, в которых необходимо запоминать бинарные данные для элементов, не содержащих дату.
Можно использовать M2M связь между User и объектом. Главный недостаток - это большие издержки на индекс и количество записей, если пользователей и объектов наблюдения очень много. Но можно использовать битовые маски, лишенные этого недостатка.
Идея такова, что для каждого пользователя существует только одна запись в базе, а номер байта в этой записи и номер бита в этом байте определяют состояние определенного элемента. В стандартной поставке django нет модели, поддерживающей blob-элементы в базах (битовые данные), поэтому я использовал обычное текстовое поле, один символ которого определяет состояние для четырех элементов. То есть запись в 250 байт дает нам данные о состоянии для 1000 элементов.
Например, имеем 10.000 анекдотов (чтобы этот метод был правомочен, их должно быть много, значит скорее всего статьи мелкие :)
И имеем 100.000 пользователей. Тогда модель связей будет выглядеть примерно так:
А размер readed_flags максимум до 2500 байт. (10.000/4)
С данным решением можно хранить не все 2500 байт, а только до самого большого прочтенного ID, деленного на 4.
Флаг прочитанности и его установка происходит с помощью двух функций внутри модели: is_readed(id) и set_readed(id)
Только после set_readed() надо не забыть записать в базу через save()
Я специально не сделал запись внутри функции, чтобы можно было безболезненно для скорости устанавливать флаги для большого количества анекдотов и потом одним махом (запросом в базу) сохранять.
Вот полный код модели:
Можно использовать M2M связь между User и объектом. Главный недостаток - это большие издержки на индекс и количество записей, если пользователей и объектов наблюдения очень много. Но можно использовать битовые маски, лишенные этого недостатка.
Идея такова, что для каждого пользователя существует только одна запись в базе, а номер байта в этой записи и номер бита в этом байте определяют состояние определенного элемента. В стандартной поставке django нет модели, поддерживающей blob-элементы в базах (битовые данные), поэтому я использовал обычное текстовое поле, один символ которого определяет состояние для четырех элементов. То есть запись в 250 байт дает нам данные о состоянии для 1000 элементов.
Например, имеем 10.000 анекдотов (чтобы этот метод был правомочен, их должно быть много, значит скорее всего статьи мелкие :)
И имеем 100.000 пользователей. Тогда модель связей будет выглядеть примерно так:
class Readed(models.Model): user = models.ForeignKey(User, related_name='readed') readed_flags = models.TextField(blank=True)Количество записей равно количеству пользователей, то есть 100.000
А размер readed_flags максимум до 2500 байт. (10.000/4)
С данным решением можно хранить не все 2500 байт, а только до самого большого прочтенного ID, деленного на 4.
Флаг прочитанности и его установка происходит с помощью двух функций внутри модели: is_readed(id) и set_readed(id)
Только после set_readed() надо не забыть записать в базу через save()
Я специально не сделал запись внутри функции, чтобы можно было безболезненно для скорости устанавливать флаги для большого количества анекдотов и потом одним махом (запросом в базу) сохранять.
Вот полный код модели:
READED_FLAG = [1,2,4,8,0]
class Readed(models.Model):
user = models.ForeignKey(User, related_name='readed')
readed_flags = models.TextField(blank=True)
def dec2hex(self,n):
return "%X" % n
def hex2dec(self,s):
return int(s, 16)
def is_readed(self,num):
if num < 1:
raise ValueError("Must be positiv and not null")
byte_position, bit_position = divmod(num-1,4)
notpresent = byte_position + 1 - len(self.readed_flags)
if notpresent:
return False
four_status = self.hex2dec(self.readed_flags[byte_position])
return (four_status & READED_FLAG[bit_position]) != READED_FLAG[4]
def set_readed(self,num):
if num < 1:
raise ValueError("Must be positiv and not null")
byte_position, bit_position = divmod(num-1,4)
notpresent = byte_position + 1 - len(self.readed_flags)
for i in xrange(notpresent):
self.readed_flags += "0"
if notpresent:
four_status = 0
else:
four_status = self.hex2dec(self.readed_flags[byte_position])
four_status_new = self.dec2hex(four_status|READED_FLAG[bit_position])
readed_flags_new = self.readed_flags[:byte_position]+ \
four_status_new+self.readed_flags[byte_position+1:]
self.readed_flags = readed_flags_new
Применение этого метода шире, чем просто замена M2M поля, у кого насколько хватит фантазии ;)
English
Deutsch
Русский

April 25th, 2008 at 1:32 p.m.
Отличное решение, взял на заметку
April 25th, 2008 at 3:56 p.m.
Небольшое язвительное замечание :)
Правилнее бы все это дело запихнуть в отделное поле (например унаследованное от django.models.fields.TextFields к примеру) и назвать BitMaskField()
April 25th, 2008 at 4:56 p.m.
Не сказал бы, "правильнее", это только другой стиль программирования, каждый выбирает решение, на свой вкус ;)
July 5th, 2008 at 7:28 p.m.
Решение интересное, только мне кажется проблемка тут зарыта. Основное, что будут читать пользователи - это как правило последние анекдоты, таким образом мы просто глупо будем дописывать к каждому пользователю 2500 нулей в начало, и с каждым новым анекдотом это число будет только расти.
Мне кажется было бы _чуть_ логичнее для такой системы хранить 2 числа типа так: "9483;200000A03C" Что значит 9483 нуля, потом 200...3С. Это и сэкономит место и скорость (БД не нужно будет передавать лишние тысячи байт на каждого пользователя. А чтобы избежать оверхэда на split(';') можно хранить это как "00009483 " и затем int(field[:8]) - количество нулей, а str(field[9:]) - переменная.
Сохранять разумеется как "%08d;%s" % (..)
Но решение, как я и сказал - интересное.