SQLAlchemy: создайте ассоциацию «многие ко многим» и заполните ее

#python #sql #sqlalchemy

#python #sql #sqlalchemy

Вопрос:

Моя идея заключается в следующем:

  • Существует основная таблица ( Documents ), которая содержит некоторые тексты, например, сообщения в блоге. Каждый документ имеет уникальный идентификатор text_id .
  • Существует вторичная таблица ( Links ), в которой хранятся уникальные URL-адреса, которые появляются в этих сообщениях. Каждый URL-адрес имеет уникальный идентификатор url_id .
  • Они связаны таблицей ассоциаций ( Association ), которая сопоставляет идентификатор текста с идентификатором домена.

Я хочу иметь возможность получать сообщения, собирать из них URL-адреса, а затем:

  • создайте новую запись в Documents
  • если он содержит новые URL-адреса — добавьте их в Links документ и свяжите с ним через Association
  • если документ содержит уже существующие URL-адреса — создайте только ассоциацию между новым документом и этими.

Для начала я создал три класса, как здесь:

 class Association(Base):
    __tablename__ = 'association'
    text_id = Column('text_id', Integer, ForeignKey('left.text_id'), primary_key=True)
    url_id = Column('url_id', Integer, ForeignKey('right.url_id'), primary_key = True)
    child = relationship("Links", back_populates='parents')
    parent = relationship("Documents", back_populates='children')

class Documents(Base):
    __tablename__ = 'left'
    text_id = Column(Integer, primary_key=True, unique=True)
    text = Column(Text)
    children = relationship("Association", back_populates='parent')

class Links(Base):
    __tablename__ = 'right'
    url_id = Column(Integer, primary_key=True, autoincrement=True, unique=True)
    url = Column(Text, unique=True)
    parents = relationship('Association', back_populates = 'child')

Base.metadata.create_all(engine)
 

Затем я пытаюсь загрузить данные:

 data = [
    {'id':1, 'text':'sometext', 'url':'facebook.com'},
    {'id':2, 'text':'sometext', 'url':'twitter.com'},
    {'id':3, 'text':'sometext', 'url':'twitter.com'}
]

for row in data:
    d = Document(text_id = row['id'])
    a = Association()
    a.child = Links(url = row['url'])
    d.children.append(a)
    session.add(d)
session.commit()
 

Что приводит к ошибке:

 Traceback (most recent call last):
  File "/home/user/.pyenv/versions/3.7.12/envs/myenv/lib/python3.7/site-packages/IPython/core/interactiveshell.py", line 3444, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-13-325b1cd57576>", line 5, in <module>
    p.children.append(a)
  File "/home/user/.pyenv/versions/3.7.12/envs/myenv/lib/python3.7/site-packages/sqlalchemy/util/langhelpers.py", line 1240, in __getattr__
    return self._fallback_getattr(key)
  File "/home/user/.pyenv/versions/3.7.12/envs/myenv/lib/python3.7/site-packages/sqlalchemy/util/langhelpers.py", line 1214, in _fallback_getattr
    raise AttributeError(key)
AttributeError: append
 

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

С другой стороны, даже если это сработает, я подозреваю, что добавление уже существующего URL-адреса через p.children.append(a) может привести к ошибке, поскольку он, по сути, попытается создать дубликат и Links не допускает этого.

Я использую MySQL и MariaDB, если это имеет значение.

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

UPD: я не смог вставить, потому что я создал экземпляр базы с automap_base() помощью вместо declarative_base() . Однако теперь я могу добавлять повторяющиеся записи, которые действительно являются проблемой:

 sqlalchemy.exc.IntegrityError: (pymysql.err.IntegrityError) (1062, "Duplicate entry 'twitter.com' for key 'url'")
[SQL: INSERT INTO `right` (url) VALUES (%(url)s)]
[parameters: {'url': 'twitter.com'}]
(Background on this error at: https://sqlalche.me/e/14/gkpj)
 

Ответ №1:

Во-первых, будет легче отлаживать, если вы используете правильные доменные имена вместо: right , left , child , children . Я знаю, что это копия из документов, но документы являются общими, в то время как ваш случай специфичен. Ваш код будет намного более читабельным.

Чтобы избежать дубликатов, вы должны проверить перед вставкой, что запись уже существует ( Documents есть уникальная text_id , Links есть уникальная url ).

 for row in data:
    d = session.query(Document).filter_by(text_id=row['id']).first()
    if not d:
        d = Document(text_id=row['id'])
    link = session.query(Links).filter_by(url=row['url']).first():
    if not link:
        link = Links(url=row['url'])
    a = Association(child=link)
    d.children.append(a)
    session.add(d)
    session.flush()
session.commit()
 

Комментарии:

1. Большое спасибо за решение! И да, в следующий раз я уделю больше внимания именованию 🙂