Scrapy - Chiave esterna SQLalchemy non creata in SQLite

Aug 22 2020

Ho provato a eseguire Scrapy utilizzando itemLoader per raccogliere tutti i dati e inserirli in SQLite 3. Sono riuscito a raccogliere tutte le informazioni che volevo ma non riesco a generare le chiavi esterne nelle mie tabelle ThreadInfo e PostInfo utilizzando back_populatesuna chiave esterna. Ho provato back_refma non ha funzionato. Tutte le altre informazioni sono state inserite nel database SQLite al termine del mio Scrapy.

Il mio obiettivo è avere quattro tabelle, boardInfo, threadInfo, postInfo e authorInfo collegati tra loro.

  • boardInfo avrà una relazione uno-a-molti con threadInfo
  • threadInfo avrà una relazione uno-a-molti con postInfo
  • authorInfo avrà una relazione uno-a-molti con threadInfo e
    postInfo.

Ho usato DB Browser per SQLite e ho scoperto che i valori delle mie chiavi esterne sono Null. Ho provato la query per il valore (threadInfo.boardInfos_id) e viene visualizzato None. Provo a risolvere questo problema per molti giorni e leggo il documento ma non riesco a risolvere il problema.

Come posso generare le chiavi foriegn nelle mie tabelle threadInfo e postInfo?

Grazie per tutte le guide e i commenti.

Ecco il mio models.py

from sqlalchemy import create_engine, Column, Table, ForeignKey, MetaData
from sqlalchemy import Integer, String, Date, DateTime, Float, Boolean, Text
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
from scrapy.utils.project import get_project_settings

Base = declarative_base()

def db_connect():
    '''
    Performs database connection using database settings from settings.py.
    Returns sqlalchemy engine instance
    '''
    return create_engine(get_project_settings().get('CONNECTION_STRING'))

def create_table(engine):
    Base.metadata.create_all(engine)

class BoardInfo(Base): 
    __tablename__ = 'boardInfos'
    id = Column(Integer, primary_key=True)
    boardName = Column('boardName', String(100)) 
    threadInfosLink = relationship('ThreadInfo', back_populates='boardInfosLink') # One-to-Many with threadInfo

class ThreadInfo(Base):
    __tablename__ = 'threadInfos'
    id = Column(Integer, primary_key=True)
    threadTitle = Column('threadTitle', String())
    threadLink = Column('threadLink', String())
    threadAuthor = Column('threadAuthor', String())
    threadPost = Column('threadPost', Text())
    replyCount = Column('replyCount', Integer)
    readCount = Column('readCount', Integer)

    boardInfos_id = Column(Integer, ForeignKey('boardInfos.id')) # Many-to-One with boardInfo
    boardInfosLink = relationship('BoardInfo', back_populates='threadInfosLink') # Many-to-One with boardInfo

    postInfosLink = relationship('PostInfo', back_populates='threadInfosLink') # One-to-Many with postInfo
    
    authorInfos_id = Column(Integer, ForeignKey('authorInfos.id')) # Many-to-One with authorInfo
    authorInfosLink = relationship('AuthorInfo', back_populates='threadInfosLink') # Many-to-One with authorInfo

class PostInfo(Base):
    __tablename__ = 'postInfos'
    id = Column(Integer, primary_key=True)
    postOrder = Column('postOrder', Integer, nullable=True)
    postAuthor = Column('postAuthor', Text(), nullable=True)
    postContent = Column('postContent', Text(), nullable=True)
    postTimestamp = Column('postTimestamp', Text(), nullable=True)

    threadInfos_id = Column(Integer, ForeignKey('threadInfos.id')) # Many-to-One with threadInfo 
    threadInfosLink = relationship('ThreadInfo', back_populates='postInfosLink') # Many-to-One with threadInfo 
    
    authorInfos_id = Column(Integer, ForeignKey('authorInfos.id')) # Many-to-One with authorInfo
    authorInfosLink = relationship('AuthorInfo', back_populates='postInfosLink') # Many-to-One with authorInfo

class AuthorInfo(Base):
    __tablename__ = 'authorInfos'
    id = Column(Integer, primary_key=True)
    threadAuthor = Column('threadAuthor', String())

    postInfosLink = relationship('PostInfo', back_populates='authorInfosLink') # One-to-Many with postInfo
    threadInfosLink = relationship('ThreadInfo', back_populates='authorInfosLink') # One-to-Many with threadInfo

Ecco il mio pipelines.py

from sqlalchemy import exists, event
from sqlalchemy.orm import sessionmaker
from scrapy.exceptions import DropItem
from .models import db_connect, create_table, BoardInfo, ThreadInfo, PostInfo, AuthorInfo
from sqlalchemy.engine import Engine
from sqlite3 import Connection as SQLite3Connection
import logging

@event.listens_for(Engine, "connect")
def _set_sqlite_pragma(dbapi_connection, connection_record):
    if isinstance(dbapi_connection, SQLite3Connection):
        cursor = dbapi_connection.cursor()
        cursor.execute("PRAGMA foreign_keys=ON;")
        # print("@@@@@@@ PRAGMA prog is running!! @@@@@@")
        cursor.close()

class DuplicatesPipeline(object):

    def __init__(self):
        '''
        Initializes database connection and sessionmaker.
        Creates tables.
        '''
        engine = db_connect()
        create_table(engine)
        self.Session = sessionmaker(bind=engine)
        logging.info('****DuplicatesPipeline: database connected****')

    def process_item(self, item, spider):

        session = self.Session()
        
        exist_threadLink = session.query(exists().where(ThreadInfo.threadLink == item['threadLink'])).scalar()
        exist_thread_replyCount = session.query(ThreadInfo.replyCount).filter_by(threadLink = item['threadLink']).scalar()
        if exist_threadLink is True: # threadLink is in DB
            if exist_thread_replyCount < item['replyCount']: # check if replyCount is more?
                return item
                session.close()
            else:
                raise DropItem('Duplicated item found and replyCount is not changed')
                session.close()
        else: # New threadLink to be added to BoardPipeline
            return item
            session.close()

class BoardPipeline(object):
    def __init__(self):
        '''
        Initializes database connection and sessionmaker
        Creates tables
        '''
        engine = db_connect()
        create_table(engine)
        self.Session = sessionmaker(bind=engine)

    def process_item(self, item, spider):
        '''
        Save scraped info in the database
        This method is called for every item pipeline component
        '''

        session = self.Session()

        # Input info to boardInfos
        boardInfo = BoardInfo()
        boardInfo.boardName = item['boardName']
        
        # Input info to threadInfos
        threadInfo = ThreadInfo()
        threadInfo.threadTitle = item['threadTitle']
        threadInfo.threadLink = item['threadLink']
        threadInfo.threadAuthor = item['threadAuthor']
        threadInfo.threadPost = item['threadPost']
        threadInfo.replyCount = item['replyCount']
        threadInfo.readCount = item['readCount']

        # Input info to postInfos
        # Due to info is in list, so we have to loop and add it.
        for num in range(len(item['postOrder'])):
            postInfoNum = 'postInfo' + str(num)
            postInfoNum = PostInfo()
            postInfoNum.postOrder = item['postOrder'][num]
            postInfoNum.postAuthor = item['postAuthor'][num]
            postInfoNum.postContent = item['postContent'][num]
            postInfoNum.postTimestamp = item['postTimestamp'][num]
            session.add(postInfoNum)
        
        # Input info to authorInfo
        authorInfo = AuthorInfo()
        authorInfo.threadAuthor = item['threadAuthor'] 

        # check whether the boardName exists
        exist_boardName = session.query(exists().where(BoardInfo.boardName == item['boardName'])).scalar()
        if exist_boardName is False:  # the current boardName does not exists
            session.add(boardInfo)

        # check whether the threadAuthor exists
        exist_threadAuthor = session.query(exists().where(AuthorInfo.threadAuthor == item['threadAuthor'])).scalar()
        if exist_threadAuthor is False:  # the current threadAuthor does not exists
            session.add(authorInfo)

        try:
            session.add(threadInfo)
            session.commit()

        except:
            session.rollback()
            raise

        finally:
            session.close()

        return item

Risposte

kerasbaz Aug 22 2020 at 08:43

Dal codice che posso vedere, non mi sembra che tu stia ambientando ThreadInfo.authorInfosLinko da ThreadInfo.authorInfos_idnessuna parte (lo stesso vale per tutte le tue relazioni FK /).

Affinché gli oggetti correlati siano collegati a un'istanza ThreadInfo, è necessario crearli e quindi allegarli qualcosa come:

        # Input info to authorInfo
        authorInfo = AuthorInfo()
        authorInfo.threadAuthor = item['threadAuthor'] 
        
        threadInfo.authorInfosLink = authorInfo

Probabilmente non vuoi session.add () ogni oggetto se è correlato tramite FK. Ti consigliamo di:

  1. istanziare un BoardInfooggettobi
  2. quindi istanzia allegare il tuo ThreadInfooggetto correlatoti
  3. allega il tuo oggetto correlato es bi.threadInfosLink = ti
  4. Alla fine di tutte le tue relazioni concatenate, puoi semplicemente aggiungere bialla sessione usando session.add(bi): tutti gli oggetti correlati verranno aggiunti attraverso le loro relazioni e gli FK saranno corretti.
kerasbaz Aug 22 2020 at 10:27

Secondo la discussione nei commenti dell'altra mia risposta, di seguito è riportato come razionalizzerei i tuoi modelli per renderli più sensati per me.

Avviso:

  1. Ho rimosso le "Info" non necessarie ovunque
  2. Ho rimosso i nomi di colonna espliciti dalle definizioni del modello e mi baserò invece sulla capacità di SQLAlchemy di dedurli per me in base ai nomi degli attributi
  3. In un oggetto "Post" non nomino l'attributo PostContent, è implicito che il contenuto sia correlato al Post perché è così che vi accediamo - invece chiama semplicemente l'attributo "Post"
  4. Ho rimosso tutta la terminologia "Link" - nei punti in cui penso che tu voglia un riferimento a una raccolta di oggetti correlati, ho fornito un attributo plurale di quell'oggetto come relazione.
  5. Ho lasciato una riga nel modello Post da rimuovere. Come puoi vedere, non hai bisogno di "autore" due volte - una volta come oggetto correlato e una volta sul Post, questo vanifica lo scopo degli FK.

Con queste modifiche, quando si tenta di utilizzare questi modelli dall'altro codice, diventa ovvio dove è necessario utilizzare .append () e dove si assegna semplicemente l'oggetto correlato. Per un dato oggetto Board sai che 'thread' è una raccolta basata solo sul nome dell'attributo, quindi farai qualcosa comeb.threads.append(thread)

from sqlalchemy import create_engine, Column, Table, ForeignKey, MetaData
from sqlalchemy import Integer, String, Date, DateTime, Float, Boolean, Text
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base

class Board(Base): 
    __tablename__ = 'board'
    id = Column(Integer, primary_key=True)
    name = Column(String(100)) 
    threads = relationship(back_populates='board')

class Thread(Base):
    __tablename__ = 'thread'
    id = Column(Integer, primary_key=True)
    title = Column(String())
    link = Column(String())
    author = Column(String())
    post = Column(Text())
    reply_count = Column(Integer)
    read_count = Column(Integer)

    board_id = Column(Integer, ForeignKey('Board.id'))
    board = relationship('Board', back_populates='threads')

    posts = relationship('Post', back_populates='threads')
    
    author_id = Column(Integer, ForeignKey('Author.id'))
    author = relationship('Author', back_populates='threads')

class Post(Base):
    __tablename__ = 'post'
    id = Column(Integer, primary_key=True)
    order = Column(Integer, nullable=True)
    author = Column(Text(), nullable=True)    # remove this line and instead use the relationship below
    content = Column(Text(), nullable=True)
    timestamp = Column(Text(), nullable=True)

    thread_id = Column(Integer, ForeignKey('Thread.id'))
    thread = relationship('Thread', back_populates='posts')
    
    author_id = Column(Integer, ForeignKey('Author.id')) 
    author = relationship('Author', back_populates='posts')

class AuthorInfo(Base):
    __tablename__ = 'author'
    id = Column(Integer, primary_key=True)
    name = Column(String())

    posts = relationship('Post', back_populates='author') 
    threads = relationship('Thread', back_populates='author')