Scrapy - SQLalchemy Foreign Key ไม่ได้สร้างใน SQLite

Aug 22 2020

ฉันพยายามเรียกใช้ Scrapy โดยใช้ itemLoader เพื่อรวบรวมข้อมูลทั้งหมดและใส่ลงใน SQLite 3 ฉันประสบความสำเร็จในการรวบรวมข้อมูลทั้งหมดที่ฉันต้องการ แต่ฉันไม่สามารถสร้างคีย์ต่างประเทศในตาราง ThreadInfo และ PostInfo ของฉันโดยใช้back_populatesกับ Foreign Key ฉันลองแล้วback_refแต่ก็ไม่ได้ผล ข้อมูลอื่น ๆ ทั้งหมดถูกแทรกลงในฐานข้อมูล SQLite หลังจาก Scrapy ของฉันเสร็จสิ้น

เป้าหมายของฉันคือมีสี่ตาราง boardInfo, threadInfo, postInfo และ authorInfo ที่เชื่อมโยงกัน

  • boardInfo จะมีความสัมพันธ์แบบหนึ่งต่อกลุ่มกับ threadInfo
  • threadInfo จะมีความสัมพันธ์แบบหนึ่งต่อกลุ่มกับ postInfo
  • authorInfo จะมีความสัมพันธ์แบบหนึ่งต่อกลุ่มกับ threadInfo และ
    postInfo

ผมใช้ DB เบราว์เซอร์สำหรับ SQLite Nullและพบว่าค่าของคีย์ต่างประเทศของฉันที่มี ฉันลองค้นหาค่า (threadInfo.boardInfos_id) แล้วมันก็ปรากฏNoneขึ้น ฉันพยายามแก้ไขปัญหานี้มาหลายวันและอ่านเอกสาร แต่ไม่สามารถแก้ปัญหาได้

ฉันจะสร้างคีย์ foriegn ในตาราง threadInfo และ postInfo ได้อย่างไร

ขอบคุณสำหรับแนวทางและความคิดเห็นทั้งหมด

นี่คือ 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

นี่คือ 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

คำตอบ

kerasbaz Aug 22 2020 at 08:43

จากรหัสที่ฉันเห็นมันไม่ได้ดูเหมือนกับฉันว่าคุณกำลังตั้งค่าThreadInfo.authorInfosLinkหรือThreadInfo.authorInfos_idที่ใด ๆ (เช่นเดียวกันกับ FK / ความสัมพันธ์ทั้งหมดของคุณ)

สำหรับอ็อบเจ็กต์ที่เกี่ยวข้องที่จะแนบกับอินสแตนซ์ ThreadInfo คุณต้องสร้างขึ้นมาจากนั้นแนบสิ่งเหล่านี้เช่น:

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

คุณอาจไม่ต้องการ session.add () แต่ละออบเจ็กต์หากเกี่ยวข้องกันผ่าน FK คุณจะต้อง:

  1. สร้างอินสแตนซ์BoardInfoวัตถุbi
  2. จากนั้นสร้างอินสแตนซ์แนบThreadInfoวัตถุที่เกี่ยวข้องของคุณti
  3. แนบวัตถุที่เกี่ยวข้องของคุณเช่น bi.threadInfosLink = ti
  4. ในตอนท้ายของความสัมพันธ์ที่ถูกล่ามโซ่ของคุณคุณสามารถเพิ่มลงbiในเซสชันโดยใช้session.add(bi)- วัตถุที่เกี่ยวข้องทั้งหมดจะถูกเพิ่มผ่านความสัมพันธ์และ FK จะถูกต้อง
kerasbaz Aug 22 2020 at 10:27

ตามการอภิปรายในความคิดเห็นของคำตอบอื่น ๆ ของฉันด้านล่างนี้คือวิธีที่ฉันจะหาเหตุผลเข้าข้างตนเองแบบจำลองของคุณเพื่อให้เหมาะสมกับฉันมากขึ้น

ข้อสังเกต:

  1. ฉันได้ลบ "ข้อมูล" ที่ไม่จำเป็นออกทุกที่
  2. ฉันได้ลบชื่อคอลัมน์ที่ชัดเจนออกจากคำจำกัดความของโมเดลของคุณแล้วและจะใช้ความสามารถของ SQLAlchemy ในการอนุมานสิ่งเหล่านั้นให้ฉันตามชื่อแอตทริบิวต์
  3. ในออบเจ็กต์ "โพสต์" ฉันไม่ได้ตั้งชื่อแอตทริบิวต์ PostContent แต่โดยนัยว่าเนื้อหาเกี่ยวข้องกับโพสต์เพราะนั่นคือวิธีที่เรากำลังเข้าถึง - เพียงแค่เรียกแอตทริบิวต์ว่า "Post" แทน
  4. ฉันได้ลบคำศัพท์ "Link" ทั้งหมดแล้ว - ในที่ที่ฉันคิดว่าคุณต้องการอ้างอิงถึงคอลเล็กชันของอ็อบเจ็กต์ที่เกี่ยวข้องซึ่งฉันได้ระบุแอตทริบิวต์พหูพจน์ของวัตถุนั้นเป็นความสัมพันธ์
  5. ฉันได้ทิ้งบรรทัดไว้ในโมเดลโพสต์เพื่อให้คุณลบ อย่างที่คุณเห็นคุณไม่จำเป็นต้องมี "ผู้เขียน" สองครั้ง - ครั้งหนึ่งเป็นวัตถุที่เกี่ยวข้องและอีกครั้งในโพสต์ที่เอาชนะวัตถุประสงค์ของ FKs

ด้วยการเปลี่ยนแปลงเหล่านี้เมื่อคุณพยายามใช้โมเดลเหล่านี้จากโค้ดอื่น ๆ ของคุณจะเห็นได้ชัดว่าคุณต้องใช้. append () ที่ไหนและคุณกำหนดวัตถุที่เกี่ยวข้องที่ไหน สำหรับอ็อบเจ็กต์ Board ที่ระบุคุณรู้ว่า 'threads' เป็นคอลเล็กชันตามชื่อแอตทริบิวต์ดังนั้นคุณจะทำสิ่งต่างๆเช่นb.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')