Scrapy - SQLalchemy Foreign Key tidak dibuat di SQLite

Aug 22 2020

Saya mencoba menjalankan Scrapy menggunakan itemLoader untuk mengumpulkan semua data dan memasukkannya ke dalam SQLite 3. Saya berhasil mengumpulkan semua info yang saya inginkan tetapi saya tidak bisa mendapatkan kunci asing untuk dibuat di tabel ThreadInfo dan PostInfo saya menggunakan back_populateskunci asing. Saya sudah mencoba back_reftetapi juga tidak berhasil. Semua info lainnya dimasukkan ke database SQLite setelah Scrapy saya selesai.

Tujuan saya adalah memiliki empat tabel, boardInfo, threadInfo, postInfo, dan authorInfo yang ditautkan satu sama lain.

  • boardInfo akan memiliki hubungan satu-ke-banyak dengan threadInfo
  • threadInfo akan memiliki hubungan satu-ke-banyak dengan postInfo
  • authorInfo akan memiliki hubungan satu-ke-banyak dengan threadInfo dan
    postInfo.

Saya menggunakan DB Browser untuk SQLite dan menemukan bahwa nilai kunci asing saya adalah Null. Saya mencoba kueri untuk nilai (threadInfo.boardInfos_id), dan itu ditampilkan None. Saya mencoba memperbaiki ini selama beberapa hari dan membaca dokumen tetapi tidak dapat menyelesaikan masalah.

Bagaimana caranya agar kunci luar dibuat di tabel threadInfo dan postInfo saya?

Terima kasih atas semua bimbingan dan komentarnya.

Inilah my 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

Ini pipelines.py saya

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

Jawaban

kerasbaz Aug 22 2020 at 08:43

Dari kode yang saya lihat, bagi saya tidak terlihat seperti Anda sedang mengatur ThreadInfo.authorInfosLinkatau di ThreadInfo.authorInfos_idmana pun (hal yang sama berlaku untuk semua FK / hubungan Anda).

Agar objek terkait dapat dilampirkan ke instance ThreadInfo, Anda perlu membuatnya lalu melampirkannya seperti:

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

Anda mungkin tidak ingin session.add () setiap objek jika itu terkait melalui FK. Anda ingin:

  1. membuat instance BoardInfoobjekbi
  2. lalu buat instance lampirkan ThreadInfoobjek terkait Andati
  3. lampirkan objek terkait Anda misalnya bi.threadInfosLink = ti
  4. Di akhir semua hubungan yang dirantai, Anda cukup menambahkan bike sesi menggunakan session.add(bi)- semua objek terkait akan ditambahkan melalui hubungannya dan FK akan benar.
kerasbaz Aug 22 2020 at 10:27

Berdasarkan diskusi di komentar jawaban saya yang lain, di bawah ini adalah bagaimana saya akan merasionalisasi model Anda agar lebih masuk akal bagi saya.

Memperhatikan:

  1. Saya telah menghapus "Info" yang tidak perlu di mana-mana
  2. Saya telah menghapus nama kolom eksplisit dari definisi model Anda dan sebagai gantinya akan mengandalkan kemampuan SQLAlchemy untuk menyimpulkannya untuk saya berdasarkan nama atribut saya
  3. Dalam objek "Post", saya tidak menamai atribut PostContent, ini tersirat bahwa konten terkait dengan Post karena begitulah cara kita mengaksesnya - sebagai gantinya cukup panggil atribut "Post"
  4. Saya telah menghapus semua terminologi "Tautan" - di tempat di mana menurut saya Anda menginginkan referensi ke kumpulan objek terkait, saya telah memberikan atribut jamak dari objek itu sebagai hubungan.
  5. Saya telah meninggalkan baris di model Post untuk Anda hapus. Seperti yang Anda lihat, Anda tidak perlu "penulis" dua kali - sekali sebagai objek terkait dan sekali di Post, yang mengalahkan tujuan FK.

Dengan perubahan ini, saat Anda mencoba menggunakan model ini dari kode Anda yang lain, menjadi jelas di mana Anda perlu menggunakan .append () dan di mana Anda cukup menetapkan objek terkait. Untuk objek Board tertentu, Anda tahu bahwa 'utas' adalah koleksi hanya berdasarkan nama atribut, jadi Anda akan melakukan sesuatu sepertib.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')