2013-07-17 2 views
12

sqlalchemy에는 django의 GenericForeignKey와 같은 것이 있습니까? 일반 외래 필드를 사용하는 것이 옳습니다.sqlalchemy 일반 외래 키 (django ORM과 같습니다)

내 문제는 다음과 같습니다. 여러 모델 (예 : 게시, 프로젝트, 공석, 특별한 것은 없음)이 있으며 각 모델에 주석을 추가하고 싶습니다. 그리고 하나의 Comment 모델 만 사용하고 싶습니다. 가치가 있니? 아니면 PostComment, ProjectComment 등을 사용해야합니까? 찬반 양론 모두?

감사합니다.

답변

13

가장 자주 사용하는 가장 간단한 패턴은 실제로 각 관계에 대해 별도의 설명 테이블을 가지고 있다는 것입니다. 이것은 처음 엔 두려운 것처럼 보일 수 있지만, 다른 접근법을 사용하는 것보다 코드를 추가로 사용하지는 않습니다 - 테이블은 자동으로 생성되고 모델은 Post.Comment, Project.Comment 등의 패턴을 사용하여 참조됩니다. 주석의 정의는 다음에서 유지됩니다. 한 곳. 이러한 접근 방식은 가장 간단하고 효율적이며 DBA가 각기 다른 유형의 주석을 각자의 테이블에 보관하여 개별적으로 크기를 조정할 수 있기 때문에 가장 간단하고 효율적입니다.

사용할 또 다른 패턴은 하나의 주석 테이블이지만 별도의 연관 테이블입니다. 이 패턴은 한 번에 두 가지 이상의 객체 (예 : 게시물 및 프로젝트)에 연결된 Comment를 사용할 수있는 유스 케이스를 제공합니다. 이 패턴은 여전히 ​​상당히 효율적입니다.

셋째, 다형성 연관 테이블이 있습니다. 이 패턴은 참조 무결성을 희생하지 않고 컬렉션 및 관련 클래스를 나타 내기 위해 고정 된 수의 테이블을 사용합니다. 이 패턴은 참조 무결성을 유지하면서 Django 스타일의 "일반적인 외래 키"에 가장 근접한 것을 시도하지만 이전 두 가지 방법만큼 간단하지는 않습니다.

실제 외래 키가없고 응용 프로그램 논리를 사용하여 행이 일치하는 ROR/Django에서 사용하는 패턴을 모방하는 것도 가능합니다.

SQLAlchemy 배포판의 처음 세 패턴은 examples/generic_associations/아래에 현대 형식으로 표시됩니다.

ROR/Django 패턴은 매우 자주 묻기 때문에 SQLAlchemy 예제에 추가 할 예정입니다. 내가 사용하고있는 접근 방식은 Django가하는 일과 정확히 같지 않습니다. 유형을 추적하기 위해 "contenttypes"테이블을 사용하는 것처럼 보입니다. 그런 식으로 불필요하게 보이지만, 정수형 컬럼의 일반적인 개념은 discriminator 열을 기반으로 테이블의 수를 가리 킵니다. 여기에 있습니다 :

from sqlalchemy.ext.declarative import declarative_base, declared_attr 
from sqlalchemy import create_engine, Integer, Column, \ 
        String, and_ 
from sqlalchemy.orm import Session, relationship, foreign, remote, backref 
from sqlalchemy import event 


class Base(object): 
    """Base class which provides automated table name 
    and surrogate primary key column. 

    """ 
    @declared_attr 
    def __tablename__(cls): 
     return cls.__name__.lower() 
    id = Column(Integer, primary_key=True) 
Base = declarative_base(cls=Base) 

class Address(Base): 
    """The Address class. 

    This represents all address records in a 
    single table. 

    """ 
    street = Column(String) 
    city = Column(String) 
    zip = Column(String) 

    discriminator = Column(String) 
    """Refers to the type of parent.""" 

    parent_id = Column(Integer) 
    """Refers to the primary key of the parent. 

    This could refer to any table. 
    """ 

    @property 
    def parent(self): 
     """Provides in-Python access to the "parent" by choosing 
     the appropriate relationship. 

     """ 
     return getattr(self, "parent_%s" % self.discriminator) 

    def __repr__(self): 
     return "%s(street=%r, city=%r, zip=%r)" % \ 
      (self.__class__.__name__, self.street, 
      self.city, self.zip) 

class HasAddresses(object): 
    """HasAddresses mixin, creates a relationship to 
    the address_association table for each parent. 

    """ 

@event.listens_for(HasAddresses, "mapper_configured", propagate=True) 
def setup_listener(mapper, class_): 
    name = class_.__name__ 
    discriminator = name.lower() 
    class_.addresses = relationship(Address, 
         primaryjoin=and_(
             class_.id == foreign(remote(Address.parent_id)), 
             Address.discriminator == discriminator 
            ), 
         backref=backref(
           "parent_%s" % discriminator, 
           primaryjoin=remote(class_.id) == foreign(Address.parent_id) 
           ) 
         ) 
    @event.listens_for(class_.addresses, "append") 
    def append_address(target, value, initiator): 
     value.discriminator = discriminator 

class Customer(HasAddresses, Base): 
    name = Column(String) 

class Supplier(HasAddresses, Base): 
    company_name = Column(String) 

engine = create_engine('sqlite://', echo=True) 
Base.metadata.create_all(engine) 

session = Session(engine) 

session.add_all([ 
    Customer(
     name='customer 1', 
     addresses=[ 
      Address(
        street='123 anywhere street', 
        city="New York", 
        zip="10110"), 
      Address(
        street='40 main street', 
        city="San Francisco", 
        zip="95732") 
     ] 
    ), 
    Supplier(
     company_name="Ace Hammers", 
     addresses=[ 
      Address(
        street='2569 west elm', 
        city="Detroit", 
        zip="56785") 
     ] 
    ), 
]) 

session.commit() 

for customer in session.query(Customer): 
    for address in customer.addresses: 
     print(address) 
     print(address.parent) 
+0

고마워요! 나는 첫 번째 패턴을 좋아하지 않는다. 나는 그것이 건조하지 않다라고 생각한다. 각 코멘트 ('edited' 플래그일지도 모름)에 정보를 추가하려면 모든 모델/테이블에서해야합니다. 내'태그'모델의 두 번째 패턴을 생각하고있었습니다. 동시에'Project'와'Post'에 링크 될 수 있습니다. 그리고 제 3은 제 '코멘트'에 필요한 것 같습니다. ROR/Django는 그렇게 단순하지 않은 것처럼 보입니다. 그래서 저는 그것을 "연구"할 것입니다. – krasulya

+1

완전히 건조합니다. DRY는 "반복하지 말 것"을 의미합니다. 패턴이 어떻게 작동하는지 살펴 본다면 전혀 반복하지 않을 것입니다. DB에 유사한 테이블이 많이 있다고해서 자신을 반복한다는 것을 의미하지는 않습니다. 그것들의 생성은 "편집 된"(Alembic과 같은 도구를 사용하는 것)과 같은 새로운 칼럼의 추가와 마찬가지로 자동화되어 있습니다. 그것은 가장 공간/시간 효율적이고 DBA 친화적 인 접근 방식입니다 (다른 테이블에 대한 저장소를 독립적으로 구성 할 수 있으므로). 나는이 문제에 대해 전직 장고를 설득하는 데 너무 많은 어려움을 겪고있다. – zzzeek

+0

첫 번째 솔루션의 개념이 마음에 들지만 실제로 구현하는 방법이 명확하지 않습니다. 간단한 예를 들려 주시겠습니까? – aquavitae

0

나는 이것이 아마도 끔찍한 방법이라고 알고 있지만, 그것은 나를 위해 빠른 해결책이었습니다.

class GenericRelation(object): 
def __init__(self, object_id, object_type): 
    self.object_id = object_id 
    self.object_type = object_type 

def __composite_values__(self): 
    return (self.object_id, self.object_type) 


class Permission(AbstractBase): 

#__abstract__ = True 

_object = None 

_generic = composite(
    GenericRelation, 
    sql.Column('object_id', data_types.UUID, nullable=False), 
    sql.Column('object_type', sql.String, nullable=False), 
) 

permission_type = sql.Column(sql.Integer) 

@property 
def object(self): 
    session = object_session(self) 
    if self._object or not session: 
     return self._object 
    else: 
     object_class = eval(self.object_type) 
     self._object = session.query(object_class).filter(object_class.id == self.object_id).first() 
     return self._object 

@object.setter 
def object(self, value): 
    self._object = value 
    self.object_type = value.__class__.__name__ 
    self.object_id = value.id