Django - Хранение пользовательской логики запросов в модели

Я создаю школьное приложение с использованием DRF. Учителя могут создавать упражнения и связывать их с тегами. Упражнение, таким образом, находится в m2m-отношении с тегом:

class Exercise(models.Model):
    MULTIPLE_CHOICE_SINGLE_POSSIBLE = 0
    MULTIPLE_CHOICE_MULTIPLE_POSSIBLE = 1
    OPEN_ANSWER = 2
    JS = 3
    C = 4

    EXERCISE_TYPES = (
        # ...
    )

    DRAFT = 0
    PRIVATE = 1
    PUBLIC = 2

    EXERCISE_STATES = (
        # ...
    )
    
    exercise_type = models.PositiveSmallIntegerField(choices=EXERCISE_TYPES)
    state = models.PositiveSmallIntegerField(choices=EXERCISE_STATES, default=DRAFT)
    text = models.TextField(blank=True)
    tags = models.ManyToManyField(Tag, blank=True)


class Tag(models.Model):
    name = models.TextField()

Преподаватели могут использовать теги для создания тестов, которые случайным образом выбирают упражнения с определенными тегами. Для этого существует модель Rule, с которой связаны одна или несколько Clauses. Clause находится в отношениях m2m с Tag.

Допустим, правило имеет два связанных пункта; первый пункт связан с тегами t1 и t2, а второй - с t3 и t4.

Логика правила выберет упражнение, которое имеет такие теги: (t1 ИЛИ t2) И (t3 ИЛИ t4)

class EventTemplateRule(models.Model):
    pass

class EventTemplateRuleClause(models.Model):
    rule = models.ForeignKey(
        EventTemplateRule,
        related_name="clauses",
        on_delete=models.CASCADE,
    )
    tags = models.ManyToManyField(Tag, blank=True)

Фактический запрос строится во время выполнения с использованием повторяющихся filter приложений и Q объектов.

Это позволяет учителям выдвигать такие условия, как:

  • выберите упражнение с тегом "двоичное дерево поиска" и тегом "легко"
  • выберите упражнение с тегом "graphs" и тегом "DAG"
  • выберите упражнение с тегом "hard" и тегом "tree" или "linked list"
  • ...

На данный момент я хочу сделать эту систему более выразительной. Было бы неплохо иметь больше параметров поиска, например:

  • выберите упражнение, которое имеет тег "DAG" или "graph", И имеет тег "easy", И чей state не PRIVATE, И чей текст содержит Let A be ... ИЛИ чей exercise_type является OPEN_ANSWER.

Вы поняли идею: произвольные условия AND'd/OR'd на любом из полей и отношений упражнения.

Мой вопрос: как бы вы хранили такое условие в модели правил?

Первое, что я подумал, это использовать JSONField для хранения условия примерно так:

{
    "and": [
         "or": [
            "has_tag": "1", // PK of the tag
            "has_tag": "22"
         ],
        "or": [
             "is_state": "2",
             "text_contains": "Let A be *", // valid regex
        ]
}

Одна проблема, которую я вижу в этом, - ссылочная целостность: нет способа проверить на уровне СУБД, что теги 1 и 22 действительно существуют. То же самое касается допустимых значений для полей типа state. Я, конечно, могу проверить эти вещи в валидаторе, но это кажется немного халтурным.

Другой возможной проблемой является безопасность: может ли пользователь сконструировать условия так, чтобы каким-то образом внедрить произвольный SQL в результирующие запросы? Этого не должно произойти, если я создаю запрос с помощью ORM, а не генерирую свой собственный необработанный SQL, но я спрашиваю на всякий случай.

Есть ли лучший способ сделать это?

Я не вижу лучшего способа, чем тот, который вы описали, но тогда я не думаю, что это настолько серьезная проблема, если это правильно протестировано. Предварительная загрузка всех действительных ID, фильтров и т.д. один раз при запуске приложения, а затем проверка действительности на лету - это просто и не потребует дополнительных запросов к базе данных, так что я не вижу в этом большой проблемы.

Я не думаю, что JSON - это плохой способ хранения, потому что кажется, что есть много возможных конфигураций для обработки. Кроме того, глядя на необработанный JSON, сразу становится более очевидно, что происходит, чем при использовании моделей.

Однако, альтернативой, которая больше соответствует django-принципам, было бы использование RuleCriteria типа модели и наборов форм. Я собрал воедино то, как может выглядеть модель, но вам придется подумать над ней гораздо больше, чем я:

class RuleCriteria(models.Model):
   rule = models.ForeignKey(Rule, ... # link to Rule
   parent_criteria = models.ForeignKey("self", ... # connect to the criteria before

   join = models.CharField(... # AND or OR or null

   comparison = models.CharField(... # your has_tag, is_state, etc.
   value = models.CharField(... # your '1', '22', etc.

Затем вы можете использовать наборы форм для управления тем, как они создаются и редактируются. Валидация также может находиться на модели, когда она очищается.

Что касается вашего вопроса об инъекциях, это не риск, если вы все еще используете ORM для обработки данных, которые собрал конечный пользователь. Это было бы риском, если бы вы использовали необработанный SQL и не санировали вводимые данные.

Вернуться на верх