Python Flask SQLAlchemy: "Невозможно определить направление отношений ... для вложенных отношений - PullRequest
0 голосов
/ 21 января 2020

Я пытаюсь создать двойную динамическую c страницу формы, где элементы формы и опции выбора (в раскрывающихся меню) читаются из базы данных в зависимости от выбранной организации и языкового предпочтения пользователь.

В коде user_data_types представляет элементы формы, а response_options представляет альтернативы раскрывающегося меню.

Полученное сообщение об ошибке: «Не удается определить направление отношения для отношения» UserDataType.response_option_names '- столбцы внешнего ключа не присутствуют ни в родительской, ни в дочерних отображенных таблицах

Что подразумевается под этим сообщением об ошибке и что я пропускаю / как его исправить? Кажется, проблема в определении класса UserDataType (?)

Python кодовая часть:

org = Org.query.filter_by(slug=slug).first() #works fine

language_pref = logged_in_user.language_pref_id #works fine

user_data_types = OrgDataType.query.filter_by(org_id=org.id).with_entities(OrgDataType.user_data_type_id) #works fine

user_data_type_details = UserDataType.query.filter(UserDataType.id.in_(user_data_types)).\
filter_by(language_id=language_pref).\
join(UserDataTypeResponseOption, UserDataType.id == UserDataTypeResponseOption.user_data_type_id).\
join(ResponseOption, UserDataTypeResponseOption.response_option_id == ResponseOption.id).\
filter_by(language_id=language_pref)\
#I think this works, see equivalent(?) SQL query further below

if request.method == "GET":
    return render_template("myform/pref_edit.html", user_data_type_details=user_data_type_details)

HTML кодовая часть:

{% for user_data_type_detail in user_data_type_details %}
  <div>
    <select name="user_data_type_detail.user_data_type_name">
    {% for response_option in user_data_type_detail.reponse_option_names %}
      <option value="{{ response_option.response_option_name }}">{{ response_option.response_option_name }}</option>
    {% endfor %}
    </select>
  </div>
{% endfor %}

База данных Определения классов (с UserDataTypeResponseOption, связывающими два других вместе):

class UserDataType(db.Model):
    __tablename__ = 'user_data_type'
    id = db.Column(db.Integer, primary_key=True, nullable=False)
    language_id = db.Column(db.Integer, primary_key=True, nullable=False)
    user_data_type_name = db.Column(db.Text)
    response_option_names = db.relationship("ResponseOption", primaryjoin="UserDataType.id==UserDataTypeResponseOption.user_data_type_id", viewonly=True)

class UserDataTypeResponseOption(db.Model):
    __tablename__ = 'user_data_type_response_option'
    id = db.Column(db.Integer, primary_key=True, nullable=False, unique=True)
    user_data_type_id = db.Column(db.Integer, db.ForeignKey('user_data_type.id'))
    response_option_id = db.Column(db.Integer, db.ForeignKey('response_option.id'))

class ResponseOption(db.Model):
    __tablename__ = 'response_option'
    id = db.Column(db.Integer, primary_key=True, nullable=False)
    language_id = db.Column(db.Integer, primary_key=True, nullable=False)
    response_option_name = db.Column(db.Text)

Таблицы, созданные в Браузере БД SQLite, соответствуют:

CREATE TABLE "user_data_type" (
    "id"    INTEGER NOT NULL,
    "language_id"   INTEGER NOT NULL,
    "user_data_type_name"   TEXT,
    "user_data_type_description"    TEXT,
    PRIMARY KEY("id","language_id")
);

CREATE TABLE "user_data_type_response_option" (
    "id"    INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
    "user_data_type_id" INTEGER,
    "response_option_id"    INTEGER,
    FOREIGN KEY("response_option_id") REFERENCES "response_option"("id"),
    FOREIGN KEY("user_data_type_id") REFERENCES "user_data_type"("id")
);

CREATE TABLE "response_option" (
    "id"    INTEGER NOT NULL,
    "language_id"   INTEGER NOT NULL,
    "response_option_name"  TEXT,
    PRIMARY KEY("id","language_id")
);

В Браузере БД SQLite я могу запустить это SQL query ...


select user_data_type.id, user_data_type_name, response_option_id, response_option_name
   from user_data_type
   join user_data_type_response_option
      on user_data_type.id = user_data_type_response_option.user_data_type_id
   join response_option
      on user_data_type_response_option.response_option_id = response_option.id
   where user_data_type.id = 1 and user_data_type.language_id = 2 and response_option.language_id = 2

... который дает мне следующий результат, который, как я думаю, мне нужен для отображения страницы формы.

id | user_data_type | response_option_id | response_option_name
1 E-mail 1 Пожалуйста, не пишите
1 E-mail 2 Только забавные письма
1 E-mail 3 Все письма пишите

Ответы [ 2 ]

1 голос
/ 26 января 2020

Ошибка

Can't determine relationship direction for relationship 'UserDataType.response_option_names' - foreign key columns are present in neither the parent nor the child's mapped tables

говорит о том, что отношение внешнего ключа от ResponseOption до UserDataType ожидалось и не было найдено.

Этот внешний ключ ожидается, так как вы определили столбец response_option_names = db.relationship("ResponseOption", ...) в таблице UserDataType. В этом сценарии «многие ко многим» правильно не иметь такого внешнего ключа, который вы будете связывать UserDataType и ResponseOption, используя таблицу ассоциации UserDataTypeResponseOption.

Следуя документации здесь , вы можете исправить это, изменив

response_option_names = db.relationship("ResponseOption", primaryjoin="UserDataType.id==UserDataTypeResponseOption.user_data_type_id", viewonly=True)

на

associations = db.relationship("UserDataTypeResponseOption", primaryjoin="UserDataType.id==UserDataTypeResponseOption.user_data_type_id", viewonly=True)

Вам также необходимо добавить новое отношение с UserDataTypeResponseOption на ResponseOption, например так:

response_option = db.relationship("ResponseOption")

Теперь ваш запрос

user_data_type_details = UserDataType.query.filter(UserDataType.id.in_(user_data_types)).\
filter_by(language_id=language_pref).\
join(UserDataTypeResponseOption, UserDataType.id == UserDataTypeResponseOption.user_data_type_id).\
join(ResponseOption, UserDataTypeResponseOption.response_option_id == ResponseOption.id).\
filter_by(language_id=language_pref)\

будет работать нормально, но обратите внимание, что у вас нет прямого доступа к ResponseOption с каждого элемента UserDataType. Вам нужно будет с go по UserDataTypeResponseOption:

for udt in user_data_type_details:
    print('user data type: ', udt)
    for assoc in udt.associations:
        print('response option: ', assoc.response_option.response_option_name)

ВЫХОД:

user data type:  <UserDataType 1, 1>
response option:  No e-mails please
response option:  Only fun e-mails
response option:  All e-mails please

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

Одним из решений будет изменение структуры ваших таблиц, чтобы таблица ассоциаций также знала идентификаторы языка как для UserDataType, так и ResponseOption. Для этого вам нужно добавить два ForeignKeyConstraint ограничения к UserDataTypeResponseOption, так как это способ создания внешних ключей с несколькими столбцами в SQLAlchemy. Я также удалил столбец id из таблицы сопоставлений и отметил остальные столбцы как первичные ключи:

class UserDataType(db.Model):
    __tablename__ = 'user_data_type'
    id = db.Column(db.Integer, primary_key=True, nullable=False)
    language_id = db.Column(db.Integer, primary_key=True, nullable=False)
    user_data_type_name = db.Column(db.Text)
    associations = db.relationship("UserDataTypeResponseOption", viewonly=True)


class ResponseOption(db.Model):
    __tablename__ = 'response_option'
    id = db.Column(db.Integer, primary_key=True, nullable=False)
    language_id = db.Column(db.Integer, primary_key=True, nullable=False)
    response_option_name = db.Column(db.Text)


class UserDataTypeResponseOption(db.Model):
    __tablename__ = 'user_data_type_response_option'
    user_data_type_id = db.Column(db.Integer, primary_key=True)
    user_data_type_language_id = db.Column(db.Integer, primary_key=True)
    response_option_id = db.Column(db.Integer, primary_key=True)
    response_option_language_id = db.Column(db.Integer, primary_key=True)
    response_option = db.relationship("ResponseOption")
    __table_args__ = (ForeignKeyConstraint([user_data_type_id, user_data_type_language_id],
                                           [UserDataType.id, UserDataType.language_id]),
                      ForeignKeyConstraint([response_option_id, response_option_language_id],
                                           [ResponseOption.id, ResponseOption.language_id]),
                      {})

И теперь вы можете использовать несколько языков.

Прямой доступ к ResponseOption s из UserDataType объектов.

SQLAlchemy предоставляет способ прямого доступа к ResponseOption s с помощью расширения прокси-сервера ассоциации (см. здесь ):

Это расширение позволяет настраивать атрибуты, которые будут иметь доступ к двум «прыжкам» с одним доступом, одному «прыжку» к связанному объекту и второму к целевому атрибуту.

Но это сопровождается предупреждением:

Предупреждение Шаблон объекта ассоциации не координирует изменения с отдельным отношением, которое отображает таблицу ассоциации как «вторичную».

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

class UserDataType(db.Model):
    __tablename__ = 'user_data_type'
    id = db.Column(db.Integer, primary_key=True, nullable=False)
    language_id = db.Column(db.Integer, primary_key=True, nullable=False)
    user_data_type_name = db.Column(db.Text)
    response_options = db.relationship("ResponseOption", secondary="user_data_type_response_option", viewonly=True)  # added secondary


class ResponseOption(db.Model):
    __tablename__ = 'response_option'
    id = db.Column(db.Integer, primary_key=True, nullable=False)
    language_id = db.Column(db.Integer, primary_key=True, nullable=False)
    response_option_name = db.Column(db.Text)


class UserDataTypeResponseOption(db.Model):
    __tablename__ = 'user_data_type_response_option'
    user_data_type_id = db.Column(db.Integer, primary_key=True)
    user_data_type_language_id = db.Column(db.Integer, primary_key=True)
    response_option_id = db.Column(db.Integer, primary_key=True)
    response_option_language_id = db.Column(db.Integer, primary_key=True)
    response_option = db.relationship("ResponseOption", backref="parent_associations")  # added backref
    __table_args__ = (ForeignKeyConstraint([user_data_type_id, user_data_type_language_id],
                                           [UserDataType.id, UserDataType.language_id]),
                      ForeignKeyConstraint([response_option_id, response_option_language_id],
                                           [ResponseOption.id, ResponseOption.language_id]),
                      {})
for udt in user_data_type_details:
    print('user data type: ', udt)
    for ro in udt.response_options:
        print('response option: ', ro.response_option_name)

ВЫХОД:

user data type:  <UserDataType 1, 1>
response option:  No e-mails please
response option:  Only fun e-mails
response option:  All e-mails please

Полный код

app.py

* 1 091 *

pref_edit.py

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>pref_edit</title>
</head>
<body>
{% for user_data_type_detail in user_data_type_details %}
    <div>
        <label for="{{ user_data_type_detail.user_data_type_name }}">{{ user_data_type_detail.user_data_type_name }}</label>
        <select name="{{ user_data_type_detail.user_data_type_name }}">
            {% for ro in user_data_type_detail.response_options %}
                <option value="{{ ro.response_option_name }}">{{ ro.response_option_name }}</option>
            {% endfor %}
        </select>
    </div>
{% endfor %}
</body>
</html>

first language

second language

Другие возможные решения

Существуют и другие решения, если вы хотите изменить дизайн базы данных, чтобы переместить language_id из UserDataType и ResponseOption таблиц в новые таблицы. Проверьте эту интересную статью для разных подходов.

0 голосов
/ 29 января 2020

При испытании подхода № 4 для поддержки мультиязычности в предложенной статье у меня возникают проблемы с фильтрацией языков, которые вошедший в систему пользователь установил в качестве предпочтительного языка (также хранящегося в базе данных). Я все еще получаю все UserDataTypes и ResponseOptions (на всех языках), поэтому я упускаю что-то фундаментальное.

Определения классов базы данных теперь:

class UserDataType(db.Model):
    __tablename__ = 'user_data_type'
    id = db.Column(db.Integer, primary_key=True, nullable=False)
    user_data_type_name = db.Column(db.Text) #but not intending to use
    user_data_type_description = db.Column(db.Text) #but not intending to use
    udt_phrases = db.relationship('UdtTranslation', viewonly=True)
    response_options = db.relationship('ResponseOption', secondary="user_data_type_response_option", viewonly=True)

class UdtTranslation(db.Model):
    __tablename__ = 'udt_translation'
    udt_trans_id = db.Column(db.Integer, db.ForeignKey('user_data_type.id'), primary_key=True, nullable=False)
    udt_language_id = db.Column(db.Integer, db.ForeignKey('language.id'), primary_key=True, nullable=False)
    udt_name = db.Column(db.Text) #the actual name in all languages
    udt_description = db.Column(db.Text) #the actual description in all languages

class ResponseOption(db.Model):
    __tablename__ = 'response_option'
    id = db.Column(db.Integer, primary_key=True, nullable=False)
    response_option_name = db.Column(db.Text) #but not intending to use
    response_option_description = db.Column(db.Text) #but not intending to use
    ro_phrases = db.relationship('RoTranslation', viewonly=True)
    ro_languages = db.relationship('Language', secondary="ro_translation", viewonly=True)

class RoTranslation(db.Model):
    __tablename__ = 'ro_translation'
    ro_trans_id = db.Column(db.Integer, db.ForeignKey('response_option.id'), primary_key=True, nullable=False)
    ro_language_id = db.Column(db.Integer, db.ForeignKey('language.id'), primary_key=True, nullable=False)
    ro_name = db.Column(db.Text)  #the actual name in all languages
    ro_description = db.Column(db.Text) #the actual description in all languages

class Language(db.Model):
    __tablename__ = 'language'
    id = db.Column(db.Integer, primary_key=True, nullable=False, unique=True)
    language_name = db.Column(db.Text)

class UserDataTypeResponseOption(db.Model):
    __tablename__ = 'user_data_type_response_option'
    id = db.Column(db.Integer, primary_key=True, nullable=False, unique=True)
    user_data_type_id = db.Column(db.Integer, db.ForeignKey('user_data_type.id'))
    response_option_id = db.Column(db.Integer, db.ForeignKey('response_option.id'))
    response_option = db.relationship("ResponseOption", backref="parent_associations")

И запрос, который я пытаюсь с is:

user_data_type_details = UserDataType.query.filter(UserDataType.id.in_(user_data_types)). \
    join(UserDataTypeResponseOption, UserDataType.id == UserDataTypeResponseOption.user_data_type_id). \
    join(UdtTranslation, UdtTranslation.udt_trans_id == UserDataType.id).filter_by(udt_language_id = language_pref). \
    join(ResponseOption, ResponseOption.id == UserDataTypeResponseOption.response_option_id). \
    join(RoTranslation, RoTranslation.ro_trans_id == ResponseOption.id).filter_by(ro_language_id = language_pref).all()

И с помощью приведенной ниже процедуры печати я получаю UserDataTypes на всех языках, а для каждого экземпляра UserDataType я получаю ResponseOptions на всех языках. Ясно, что способ, которым я отфильтровываю язык, не работает. Идеи?

for user_data_type_detail in user_data_type_details:
   for udt_phrase in user_data_type_detail.udt_phrases:
       print(udt_phrase.udt_name)
        for ro in user_data_type_detail.response_options:
            for ro_phrase in ro.ro_phrases:
                print(ro_phrase.ro_name)
...