Порядок объявления классов в SqlAlchemy - PullRequest
0 голосов
/ 12 января 2019

Мне жаль этот новый вопрос, но я не могу разобраться с ним пару дней, и это сводит меня с ума.

Я строю небольшой проект, чтобы изучить колбу, SqlAlchemy и Postrges. У меня есть серьезные проблемы с объявлением классов в SqlAlchemy. Я уже упростил модели, удалив все отношения «многие ко многим». Однако теперь у меня появились новые проблемы даже с отношениями один-ко-многим, хотя я думаю, что перепробовал все возможные варианты. Может быть, есть опечатка, которую я продолжаю игнорировать, или я не понимаю что-то фундаментальное. Пожалуйста, дайте мне знать ...

Итак, у меня есть классы, объявленные следующим образом в моих models.py:

 @login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))


class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key = True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password = db.Column(db.String(60), nullable=False)
    date_registered = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    role = db.Column(db.Integer, nullable=False)
    role_exp_date = db.Column(db.DateTime)
    #o2o
    personal_datas = db.relationship('PersonalData', uselist=False, backref='user', lazy=True)  
    persons = db.relationship('Person', uselist=False, backref='user', lazy=True)       
    #o2m
    posts = db.relationship('Post', backref='author', lazy=True)
    comments = db.relationship('PostComment', backref='author', lazy=True)
    projects_owned = db.relationship('ConstrProject', backref='owner', lazy=True)
    attachments = db.relationship('Attachment', backref='author', lazy=True)

    def __repr__(self):
        return f"{self.username} ({self.email})"



class PersonalData(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    date_birth = db.Column(db.DateTime)
    image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
    interests =  db.Column(db.Text)
    experties =  db.Column(db.Text)                     #Потом сделать отдельную таблицу...
    #o2o
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    #o2m


class Person(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    first_name = db.Column(db.String(30), nullable=False)
    middle_name = db.Column(db.String(40), nullable=False)
    last_name = db.Column(db.String(60), nullable=False)
    email = db.Column(db.String(120))
    license = db.Column(db.String(120))
    address = db.Column(db.String(240))
    telephone = db.Column(db.String(30))
    #o2o
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    #o2m
    signers = db.relationship('Signer', backref='person', lazy=True)

    def __repr__(self):
        return f"{self.last_name.Capitalize} {self.first_name[0].Upper}. {self.middle_name[0].Upper}."


class ConstrProject(db.Model):
    __tablename__ = 'constrproject'
    id = db.Column(db.Integer, primary_key = True)
    name = db.Column(db.String(120), nullable=False, default='New Project')
    full_title = db.Column(db.Text, default='New Project')
    notes = db.Column(db.Text)
    public = db.Column(db.Boolean, default=True)                                    #? check expamples
    date_created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 
    date_last_edit = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    document_template = db.Column(db.Integer, nullable=False, default=1)                        #later to m2m
    print_settings = db.Column(db.Integer, nullable=False, default=1)                           #later to m2m
    address = db.Column(db.String(240))
    #o2m
    documents = db.relationship('Document', backref='project', lazy=True)
    #m2o
    owner_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)          #+      #default = CurrentUser
    developer_id = db.Column(db.Integer, db.ForeignKey('company.id'))
    main_contractor_id = db.Column(db.Integer, db.ForeignKey('company.id'))
    architect_id = db.Column(db.Integer, db.ForeignKey('company.id'))
    subcontractor_id = db.Column(db.Integer, db.ForeignKey('company.id'))
    other_id = db.Column(db.Integer, db.ForeignKey('company.id'))


    developer = db.relationship('Company', foreign_keys=[developer_id], back_populates='constr_projects_developed')
    main_contractor = db.relationship('Company', foreign_keys=[main_contractor_id], back_populates='constr_projects_main_contracts')
    architect = db.relationship('Company', foreign_keys=[architect_id], back_populates='constr_projects_architect')
    subcontractor = db.relationship('Company', foreign_keys=[subcontractor_id], back_populates='constr_projects_subcontracts')
    other = db.relationship('Company', foreign_keys=[other_id], back_populates='constr_projects_other')


    tech_control_reps_id = db.Column(db.Integer, db.ForeignKey('signer.id'), nullable=False)
    main_contractor_reps_id = db.Column(db.Integer, db.ForeignKey('signer.id'), nullable=False)
    architect_reps_id = db.Column(db.Integer, db.ForeignKey('signer.id'), nullable=False)
    subcontractor_reps_id = db.Column(db.Integer, db.ForeignKey('signer.id'), nullable=False)
    other_reps_id = db.Column(db.Integer, db.ForeignKey('signer.id'), nullable=False)

    tech_control_reps = db.relationship('Signer', foreign_keys=[tech_control_reps_id], back_populates='tech_control_projects')
    main_contractor_reps = db.relationship('Signer', foreign_keys=[main_contractor_reps_id], back_populates='main_contractor_projects')
    architect_reps = db.relationship('Signer', foreign_keys=[architect_reps_id], back_populates='architect_projects')
    subcontractor_reps = db.relationship('Signer', foreign_keys=[subcontractor_reps_id], back_populates='subcontractor_projects')
    other_reps = db.relationship('Signer', foreign_keys=[other_reps_id], back_populates='others_projects')

    def __repr__(self):
        return f"Site: {self.name},  (id{self.id})"





class Signer(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    decree = db.Column(db.String(120))
    job_title = db.Column(db.String(120))
    date_duty_start = db.Column(db.DateTime)
    date_duty_end = db.Column(db.DateTime)
    #o2m
    person_id = db.Column(db.Integer, db.ForeignKey('person.id'), nullable=False)
    company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=False)
    #m2o
    tech_control_projects = db.relationship('ConstrProject', back_populates='tech_control_reps')
    main_contractor_projects = db.relationship('ConstrProject', back_populates='main_contractor_reps')
    architect_projects = db.relationship('ConstrProject', back_populates='architect_reps')
    subcontractor_projects = db.relationship('ConstrProject', back_populates='subcontractor_reps')
    others_projects = db.relationship('ConstrProject', back_populates='other_reps')

    def __repr__(self):
        return f"{self.job_title} as per {self.decree}."       #название компании как подтянуть



class Company(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    name = db.Column(db.String(60))
    full_title = db.Column(db.String(240))
    tin = db.Column(db.Integer)
    kpp = db.Column(db.Integer)
    ogrn = db.Column(db.Integer)
    email = db.Column(db.String(120))
    address = db.Column(db.String(240))
    telephone = db.Column(db.String(30))
    #o2m
    license_number = db.Column(db.String(40), nullable = False)
    license_date_issued = db.Column(db.DateTime) 
    license_category = db.Column(db.String(120), default = '2nd')
    license_issued_by = db.Column(db.String(120))
    license_issued_by_tin = db.Column(db.Integer)
    license_issued_by_kpp = db.Column(db.Integer)
    license_issued_by_ogrn = db.Column(db.Integer)

    signers = db.relationship('Signer', backref='company', lazy=True)
    constr_projects_developed = db.relationship('ConstrProject', back_populates='developer') 
    constr_projects_main_contracts = db.relationship('ConstrProject', back_populates='main_contractor')
    constr_projects_architect = db.relationship('ConstrProject', back_populates='architect')
    constr_projects_subcontracts = db.relationship('ConstrProject', back_populates='subcontractor')
    constr_projects_other = db.relationship('ConstrProject', back_populates='other')

    def __repr__(self):
        return f"{self.name}"



class Post(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    title = db.Column(db.String(100), nullable=False)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    content = db.Column(db.Text, nullable=False)
    #o2m
    comments = db.relationship('PostComment', backref='Post', lazy=True)
    #m2o
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)


    def __repr__(self):
        return f"Post('{self.title}', '{self.date_posted}')"



class PostComment(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    content = db.Column(db.Text, nullable=False)
    #m2o
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)

    def __repr__(self):
        return f"Comment('{self.id}', '{self.date_posted}')"




class Document(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    type = db.Column(db.String(60), nullable=False, default='АОСР')
    date_last_edit = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    notes = db.Column(db.Text)
    public = db.Column(db.Boolean, default=True)
    number = db.Column(db.String(20), nullable=False)
    date = db.Column(db.DateTime, default=datetime.utcnow)
    job_name = db.Column(db.Text)                               #? обязательный? на каком этапе делать проверку?
    job_place = db.Column(db.String(200))
    date_job_start = db.Column(db.DateTime, default=datetime.utcnow)
    date_job_end = db.Column(db.DateTime, default=datetime.utcnow)
    regulations = db.Column(db.Text)
    next_job_allowed = db.Column(db.String(240))
    attachments_user_defined = db.Column(db.Text)
    #o2m
    attachments = db.relationship('Attachment', backref='document', lazy=True)
    #m2o
    project_id = db.Column(db.Integer, db.ForeignKey('constrproject.id'), nullable=False)
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    #m2m

    arch_docs  = db.Column(db.Text)
    building_materials  = db.Column(db.Text)
    work_drawings  = db.Column(db.Text)

    def __repr__(self):
        return f"АОСР ('{self.number}', '{self.job_name} {self.job_place}', '{self.project}' )"



class Attachment(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    type_of_document = db.Column(db.String(60), nullable=False, default="QAC")
    number = db.Column(db.String(50), nullable=False)
    date = db.Column(db.DateTime)
    date_valid_start = db.Column(db.DateTime)
    date_valid_end = db.Column(db.DateTime)
    contents = db.Column(db.Text)
    type_of_file = db.Column(db.String(10), nullable=False, default = 'jpg')
    image_file = db.Column(db.String(20), nullable=False)
    #m2o
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    document_id = db.Column(db.Integer, db.ForeignKey('document.id'), nullable=False)

    def __repr__(self):
        if self.text:
            return f'{self.text}'
        return f'+{self.type_of_document} id{self.id}  ({self.type_of_file})'

Я не могу понять почему, когда я пытаюсь создать экземпляр «Документа» Я получаю такие ошибки:

sqlalchemy.exc.AmbiguousForeignKeysError: Не удалось определить соединение условие между родительскими / дочерними таблицами в отношениях Signer.tech_control_projects - существует несколько путей внешнего ключа связывание таблиц. Укажите аргумент foreign_keys, предоставив список тех столбцов, которые должны учитываться как содержащие ключевая ссылка на родительскую таблицу.

или

sqlalchemy.exc.InvalidRequestError: При инициализации маппера Mapper | ConstrProject | constrproject, выражение 'tech_control_reps_id' не удалось найти имя ("имя" tech_control_reps_id "не определены "). Если это имя класса, рассмотрите возможность добавления этого отношение () к классу после того, как оба зависимых класса были определены.

Я даже не пытался создавать эти классы. Добавление foreign_keys, похоже, тоже не помогает. все отношения объявлены как строки. Я пытался использовать лямбда также безуспешно.

Тем не менее, перемещение порядка выражений классов изменяет сообщения об ошибках, которые я получаю ...

Я не могу найти хороших примеров (ресурсов) более сложных баз данных с несколькими отношениями «многие ко многим» и «один ко многим» в каждом классе. Обычно примеры очень простые и очевидные. Поэтому я буду очень признателен, если вы разместите ссылки на такие проекты или учебные пособия.

1 Ответ

0 голосов
/ 13 января 2019

Я хотел изучить некоторые из них сам, поэтому я создал рабочий пример с однозначными и множественными взаимосвязями один ко многим на основе вашего кода (User, Person, Post и Comment занятия). Я надеюсь, что это будет хороший (простой, но не тривиальный) пример для вас.

'''SQLAlchemy one-to-one and one-to-many SSCCE'''

import sqlalchemy
import sqlalchemy.ext.declarative
from passlib.hash import pbkdf2_sha256

Base = sqlalchemy.ext.declarative.declarative_base()


class User(Base):
    __tablename__ = 'users'
    user_id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
    username = sqlalchemy.Column(sqlalchemy.String(20), unique=True, nullable=False)
    email = sqlalchemy.Column(sqlalchemy.String(120), unique=True, nullable=False)
    password = sqlalchemy.Column(sqlalchemy.String(100), nullable=False)
    registered = sqlalchemy.Column(
        sqlalchemy.DateTime(timezone=True),
        nullable=False,
        server_default=sqlalchemy.func.now()
    )
    #o2o
    person = sqlalchemy.orm.relationship(
        'Person',
        uselist=False,
        back_populates='user',
        lazy='joined'
    )
    #o2m
    posts = sqlalchemy.orm.relationship('Post', back_populates='user')
    comments = sqlalchemy.orm.relationship('Comment', back_populates='user')

    def __repr__(self):
        return f'{self.username} ({self.email})'


class Person(Base):
    __tablename__ = 'persons'
    person_id = sqlalchemy.Column(
        sqlalchemy.Integer,
        sqlalchemy.ForeignKey('users.user_id'),
        primary_key=True
    )
    first_name = sqlalchemy.Column(sqlalchemy.String(30), nullable=False)
    middle_name = sqlalchemy.Column(sqlalchemy.String(40), nullable=False)
    last_name = sqlalchemy.Column(sqlalchemy.String(60), nullable=False)
    #o2o
    user = sqlalchemy.orm.relationship('User', back_populates='person', lazy='joined')

    def __repr__(self):
        return (
            f'{self.last_name.upper()}'
            f' {self.first_name[:1].upper()}.'
            f' {self.middle_name[:1].upper()}.'
        )


class Post(Base):
    __tablename__ = 'posts'
    post_id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
    title = sqlalchemy.Column(sqlalchemy.String(100), nullable=False)
    posted = sqlalchemy.Column(
        sqlalchemy.DateTime(timezone=True),
        nullable=False,
        server_default=sqlalchemy.func.now()
    )
    content = sqlalchemy.Column(sqlalchemy.Text, nullable=False)
    #o2m
    comments = sqlalchemy.orm.relationship('Comment', back_populates='post')
    #m2o
    user_id = sqlalchemy.Column(
        sqlalchemy.Integer,
        sqlalchemy.ForeignKey('users.user_id'),
        nullable=False
    )
    user = sqlalchemy.orm.relationship('User', uselist=False, back_populates='posts', lazy='joined')

    def __repr__(self):
        return f'Post({self.title!r}, {self.posted!r})'


class Comment(Base):
    __tablename__ = 'comments'
    comment_id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
    posted = sqlalchemy.Column(
        sqlalchemy.DateTime(timezone=True),
        nullable=False,
        server_default=sqlalchemy.func.now()
    )
    content = sqlalchemy.Column(sqlalchemy.Text, nullable=False)
    #m2o
    user_id = sqlalchemy.Column(
        sqlalchemy.Integer,
        sqlalchemy.ForeignKey('users.user_id'),
        nullable=False
    )
    user = sqlalchemy.orm.relationship(
        'User',
        uselist=False,
        back_populates='comments',
        lazy='joined'
    )
    post_id = sqlalchemy.Column(
        sqlalchemy.Integer,
        sqlalchemy.ForeignKey('posts.post_id'),
        nullable=False
    )
    post = sqlalchemy.orm.relationship(
        'Post',
        uselist=False,
        back_populates='comments',
        lazy='joined'
    )

    def __repr__(self):
        return f'Comment({self.comment_id!r}, {self.posted!r})'


def main():
    engine = sqlalchemy.create_engine(
        'postgresql+psycopg2:///stack',
        echo=True,
        server_side_cursors=True,
        use_batch_mode=True
    )

    Base.metadata.create_all(engine)
    Session = sqlalchemy.orm.sessionmaker(bind=engine)

    session = Session()
    session.commit()

    try:
        the_user = session.query(User).filter(User.username == 'example').one()
    except sqlalchemy.orm.exc.NoResultFound:
        the_user = User(
            username='example',
            email='example@example.com',
            password=pbkdf2_sha256.hash('correct horse battery staple')
        )
        the_user.person = Person(first_name='Ex', middle_name='', last_name='Ample')
        session.add(the_user)

    print(the_user)
    print(the_user.person)

    if not the_user.posts:
        the_user.posts.append(Post(title='First post', content='Lorem ipsum'))
        session.commit()
    print(the_user.posts[0])

    if not the_user.posts[0].comments:
        the_user.posts[0].comments.append(Comment(content='Me too', user=the_user))
        session.commit()
    print(the_user.posts[0].comments[0])

if __name__ == '__main__':
    main()

Несколько комментариев:

  • Я не думаю, что легко иметь внешние ключи в обоих случаях для обязательных отношений один к одному.
  • Я сделал так, чтобы и Пользователь, и Персона использовали одни и те же идентификационные номера, поскольку они однозначные.
  • Я сделал PostgreSQL now () в качестве значения по умолчанию для временных меток вместо utcnow на стороне клиента.
  • Я заставил временные метки использовать тип "timestamp with timezone" - тип "timestamp" (без часового пояса) является мерзостью .
  • «пользователь» - это недопустимое имя для таблицы, так как это также ключевое слово в PostgreSQL, поэтому я изменил его на «пользователи». Другие таблицы также изменили форму множественного числа для согласованности.
  • Необходимо надежное хранение паролей.
  • Я использовал back_populates последовательно, поскольку он более явный и лучше работает со статическими анализаторами кода, чем backref.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...