Смешанный контент (float || unicode) для столбца базы данных - PullRequest
2 голосов
/ 27 мая 2011

Ради простоты, скажем, у меня есть вопрос.

Каждый ответ набирает балл.

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

Q: какой твой любимый питомец?

  1. кошка [1 балл]
  2. собака [2 балла]
  3. кайман [3 балла]

Отвечая Собака Я получаю 2 очка.

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

Сколько литров пива вы пьете в день?

  1. 0 [0 баллов]
  2. 1 [1 балл]
  3. 3 [5 баллов]

Если я отвечу 2 литра Я получу 3 балла .

Теперь я использую sqlalchemy и у меня есть таблица с ответом в каждой строке:

questions
    id PK
    name String
    quantitative Bool

answers
    id Integer PK
    id_question Integer FK
    value String

и приведение answers.value к плавающим значениям каждый раз, когда мне приходится иметь дело с ним как числом для интерполяции и т. Д.

  1. Я мог бы изменить имя столбца value на _value и сделать функции получения и установки для answer.value, которые каждый раз приводят к плавающему значению answer._value, если вопрос числовой (answer.question.quantitative равен True)

  2. У меня могут быть отдельные столбцы в ответе для текстовых и числовых значений (например, value и text, у меня все равно не будет миллионов записей)

  3. Или ...

Что должно быть более эффективным и простым в использовании?

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


Редактировать

Поскольку пример пива может вводить в заблуждение, я интегрируюсь с другим:

В: Сколько денег вы даете на благотворительность в долларах?

  1. 0 [0 баллов]
  2. 10 [1 балл]
  3. 100 [2 балла]

Как и для вопрос о домашних животных и пиве У меня есть значения ответа "0", "10", "100", хранящиеся в базе данных в виде строк в столбце answers.value, чтобы интерполировать значения, чтобы получить оценку для ответа 50 У меня есть все время, чтобы разыграть answers.value для плавания.

Здесь я смешал тип контента в одном столбце БД.

Ответы [ 2 ]

3 голосов
/ 27 мая 2011

Что делает это излишне сложным, так это попытка оптимизировать количественные ответы.

Это множественный выбор.Относитесь к количественному ответу как к качественному.Отнесите «точки» в качестве отдельного атрибута каждого ответа.

И да, в базе данных будет ("3 литра", 3).И да, мыслящему человеку это может показаться излишним.

Но для целей программного обеспечения хорошо работать, чтобы считать все ответы качественными и сохранить любое количественное отображение полностью разделенным.


Редактировать.Не храните ответ в виде числа.Это просто неправильно.

Как и в случае с домашними животными и пивом, у меня есть значения ответов "0", "10", "100", хранящиеся в базе данных в виде строк в столбце answers.value.

Верно.

для интерполяции значений, чтобы получить оценку за ответ 50 У меня есть все время разыгрывать answers.value для float.

Неправильно.

Посмотрите ихтак же, как вы обращаетесь с домашними животными.Это простое соединение.Делайте все так, как вы делаете домашние животные.Относитесь ко всем данным как к «качественным».Одно простое правило;не два правила.Это правильное и стандартное решение.

1 голос
/ 15 июня 2011

Для быстрого и грязного решения я бы предложил по крайней мере использовать два разных столбца для хранения разных ответов.Вы также можете добавить ограничение CHECK в базу данных, чтобы гарантировать, что ровно один из них используется для любой строки, а другой - NULL.Чем выполнить быстрый-грязный код для вычисления общего Test балла.

Альтернатива

Идея состоит в том, чтобы построить правильную объектную модель, сопоставить ее с RDMBS и вопрос не нуженспросить.Также я ожидаю, что при использовании Single Table Inheritance результирующая схема БД будет практически идентична текущей реализации (вы можете увидеть модель при запуске сценария с параметром echo=True):

CREATE TABLE questions (
    id INTEGER NOT NULL, 
    text VARCHAR NOT NULL, 
    type VARCHAR(10) NOT NULL, 
    PRIMARY KEY (id)
)

CREATE TABLE answer_options (
    id INTEGER NOT NULL, 
    question_id INTEGER NOT NULL, 
    value INTEGER NOT NULL, 
    type VARCHAR(10) NOT NULL, 
    text VARCHAR, 
    input INTEGER, 
    PRIMARY KEY (id), 
    FOREIGN KEY(question_id) REFERENCES questions (id)
)

CREATE TABLE answers (
    id INTEGER NOT NULL, 
    type VARCHAR(10) NOT NULL, 
    question_id INTEGER, 
    test_id INTEGER, 
    answer_option_id INTEGER, 
    answer_input INTEGER, 
    PRIMARY KEY (id), 
    FOREIGN KEY(question_id) REFERENCES questions (id), 
    FOREIGN KEY(answer_option_id) REFERENCES answer_options (id), 
    --FOREIGN KEY(test_id) REFERENCES tests (id)
)

Приведенный ниже код представляет собой полный рабочий скрипт, который отображает как объектную модель, ее отображение в базе данных, так и сценарии использования.Поскольку она разработана, модель легко расширяется с помощью других типов вопросов / ответов без какого-либо влияния на существующие классы.В основном вы получаете менее хакерский и более гибкий код просто потому, что у вас есть объектная модель, которая правильно отражает ваш случай.Код ниже:

from sqlalchemy import create_engine, Column, Integer, SmallInteger, String, ForeignKey, Table, Index
from sqlalchemy.orm import relationship, scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base

# Configure test data SA
engine = create_engine('sqlite:///:memory:', echo=True)
session = scoped_session(sessionmaker(bind=engine))
Base = declarative_base()
Base.query = session.query_property()

class _BaseMixin(object):
    """ Just a helper mixin class to set properties on object creation.  
    Also provides a convenient default __repr__() function, but be aware that 
    also relationships are printed, which might result in loading relations.
    """
    def __init__(self, **kwargs):
        for k,v in kwargs.items():
            setattr(self, k, v)

    def __repr__(self):
        return "<%s(%s)>" % (self.__class__.__name__, 
            ', '.join('%s=%r' % (k, self.__dict__[k]) 
                for k in sorted(self.__dict__) if '_sa_' != k[:4] and '_backref_' != k[:9])
            )

### AnswerOption hierarchy
class AnswerOption(Base, _BaseMixin):
    """ Possible answer options (choice or any other configuration).  """
    __tablename__ = u'answer_options'
    id = Column(Integer, primary_key=True)
    question_id = Column(Integer, ForeignKey('questions.id'), nullable=False)
    value = Column(Integer, nullable=False)
    type = Column(String(10), nullable=False)
    __mapper_args__ = {'polymorphic_on': type}

class AnswerOptionChoice(AnswerOption):
    """ A possible answer choice for the question.  """
    text = Column(String, nullable=True) # when mapped to single-table, must be NULL in the DB
    __mapper_args__ = {'polymorphic_identity': 'choice'}

class AnswerOptionInput(AnswerOption):
    """ A configuration entry for the input-type of questions.  """
    input = Column(Integer, nullable=True) # when mapped to single-table, must be NULL in the DB
    __mapper_args__ = {'polymorphic_identity': 'input'}

### Question hierarchy
class Question(Base, _BaseMixin):
    """ Base class for all types of questions.  """
    __tablename__ = u'questions'
    id = Column(Integer, primary_key=True)
    text = Column(String, nullable=False)
    type = Column(String(10), nullable=False)
    answer_options = relationship(AnswerOption, backref='question')
    __mapper_args__ = {'polymorphic_on': type}

    def get_answer_value(self, answer):
        """ function to get a value of the answer to the question.  """
        raise Exception('must be implemented in a subclass')

class QuestionChoice(Question):
    """ Single-choice question.  """
    __mapper_args__ = {'polymorphic_identity': 'choice'}

    def get_answer_value(self, answer):
        assert isinstance(answer, AnswerChoice)
        assert answer.answer_option in self.answer_options, "Incorrect choice"
        return answer.answer_option.value

class QuestionInput(Question):
    """ Input type question.  """
    __mapper_args__ = {'polymorphic_identity': 'input'}

    def get_answer_value(self, answer):
        assert isinstance(answer, AnswerInput)
        value_list = sorted([(_i.input, _i.value) for _i in self.answer_options])
        if not value_list:
            raise Exception("no input is specified for the question {0}".format(self))
        if answer.answer_input <= value_list[0][0]:
            return value_list[0][1]
        elif answer.answer_input >= value_list[-1][0]:
            return value_list[-1][1]
        else: # interpolate in the range:
            for _pos in range(len(value_list)-1):
                if answer.answer_input == value_list[_pos+1][0]:
                    return value_list[_pos+1][1]
                elif answer.answer_input < value_list[_pos+1][0]:
                    # interpolate between (_pos, _pos+1)
                    assert (value_list[_pos][0] != value_list[_pos+1][0])
                    return value_list[_pos][1] + (value_list[_pos+1][1] - value_list[_pos][1]) * (answer.answer_input - value_list[_pos][0]) / (value_list[_pos+1][0] - value_list[_pos][0])
        assert False, "should never reach here"

### Answer hierarchy
class Answer(Base, _BaseMixin):
    """ Represents an answer to the question.  """
    __tablename__ = u'answers'
    id = Column(Integer, primary_key=True)
    type = Column(String(10), nullable=False)
    question_id = Column(Integer, ForeignKey('questions.id'), nullable=True) # when mapped to single-table, must be NULL in the DB
    question = relationship(Question)
    test_id = Column(Integer, ForeignKey('tests.id'), nullable=True) # @todo: decide if allow answers without a Test
    __mapper_args__ = {'polymorphic_on': type}

    def get_value(self):
        return self.question.get_answer_value(self)

class AnswerChoice(Answer):
    """ Represents an answer to the *Choice* question.  """
    __mapper_args__ = {'polymorphic_identity': 'choice'}
    answer_option_id = Column(Integer, ForeignKey('answer_options.id'), nullable=True) 
    answer_option = relationship(AnswerOption, single_parent=True)

class AnswerInput(Answer):
    """ Represents an answer to the *Choice* question.  """
    __mapper_args__ = {'polymorphic_identity': 'input'}
    answer_input = Column(Integer, nullable=True) # when mapped to single-table, must be NULL in the DB

### other classes (Questionnaire, Test) and helper tables
association_table = Table('questionnaire_question', Base.metadata,
    Column('id', Integer, primary_key=True),
    Column('questionnaire_id', Integer, ForeignKey('questions.id')),
    Column('question_id', Integer, ForeignKey('questionnaires.id'))
)
_idx = Index('questionnaire_question_u_nci', 
            association_table.c.questionnaire_id, 
            association_table.c.question_id, 
            unique=True)

class Questionnaire(Base, _BaseMixin):
    """ Questionnaire is a compilation of questions.  """
    __tablename__ = u'questionnaires'
    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False)
    # @note: could use relationship with order or even add question number
    questions = relationship(Question, secondary=association_table)

class Test(Base, _BaseMixin):
    """ Test is a 'test' - set of answers for a given questionnaire. """
    __tablename__ = u'tests'
    id = Column(Integer, primary_key=True)
    # @todo: add user name or reference
    questionnaire_id = Column(Integer, ForeignKey('questionnaires.id'), nullable=False)
    questionnaire = relationship(Questionnaire, single_parent=True)
    answers = relationship(Answer, backref='test')
    def total_points(self):
        return sum(ans.get_value() for ans in self.answers)

# -- end of model definition --

Base.metadata.create_all(engine)

# -- insert test data --
print '-' * 20 + ' Insert TEST DATA ...'
q1 =  QuestionChoice(text="What is your fav pet?")
q1c1 = AnswerOptionChoice(text="cat", value=1, question=q1)
q1c2 = AnswerOptionChoice(text="dog", value=2, question=q1)
q1c3 = AnswerOptionChoice(text="caiman", value=3)
q1.answer_options.append(q1c3)
a1 = AnswerChoice(question=q1, answer_option=q1c2)
assert a1.get_value() == 2
session.add(a1)
session.flush()

q2 =  QuestionInput(text="How many liters of beer do you drink a day?")
q2i1 = AnswerOptionInput(input=0, value=0, question=q2)
q2i2 = AnswerOptionInput(input=1, value=1, question=q2)
q2i3 = AnswerOptionInput(input=3, value=5)
q2.answer_options.append(q2i3)

# test interpolation routine
_test_ip = ((-100, 0),
            (0, 0),
            (0.5, 0.5),
            (1, 1),
            (2, 3),
            (3, 5),
            (100, 5)
)
a2 = AnswerInput(question=q2, answer_input=None)
for _inp, _exp in _test_ip:
    a2.answer_input = _inp
    _res = a2.get_value()
    assert _res == _exp, "{0}: {1} != {2}".format(_inp, _res, _exp)
a2.answer_input = 2
session.add(a2)
session.flush()

# create a Questionnaire and a Test
qn = Questionnaire(name='test questionnaire')
qn.questions.append(q1)
qn.questions.append(q2)
session.add(qn)
te = Test(questionnaire=qn)
te.answers.append(a1)
te.answers.append(a2)
assert te.total_points() == 5
session.add(te)
session.flush()

# -- other tests --
print '-' * 20 + ' TEST QUERIES ...'
session.expunge_all() # clear the session cache
a1 = session.query(Answer).get(1)
assert a1.get_value() == 2 # @note: will load all dependant objects (question and answer_options) automatically to compute the value
a2 = session.query(Answer).get(2)
assert a2.get_value() == 3 # @note: will load all dependant objects (question and answer_options) automatically to compute the value
te = session.query(Test).get(1)
assert te.total_points() == 5

Я надеюсь, что эта версия кода отвечает на все вопросы, заданные в комментариях.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...