Domanda

Sorry, if this is a newbie question but the documentation about the many-to-one relationship doesn't seems to cover this. I have been looking for something similar to this (under the "How to Insert / Add Data to Your Tables" section), however in the shown example this is always a unique insertion.

Basically, I want to populate my database with data located on my local machine. For the sake of simplicity I have constructed the below-shown example into a MWE that illustrates the problem. The problem consists of two tables called Price and Currency and the implementation is done in a declarative style.

model.py

from sqlalchemy import Column, Integer, String
from sqlalchemy import Float, BigInteger, ForeignKey
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Currency(Base):
    __tablename__ = 'Currency'

    id = Column(Integer, primary_key=True)
    unit = Column(String(16), unique=True)

    def __init__(self, unit):
        self.unit = unit

class Price(Base):
    __tablename__ = 'Price'

    id = Column(BigInteger, primary_key=True)

    currency_id = Column(Integer, ForeignKey("Currency.id"), nullable=False)
    currency = relationship("Currency", backref="Currency.id")

    hour1 = Column(Float)
    hour2 = Column(Float)

    def __init__(self, hour1, hour2):
        self.hour1 = hour1
        self.hour2 = hour2

Currently, I am populating the database using following code:

script.py

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

from model import *

engine = create_engine('sqlite:///example.db', echo=True)

db_session = scoped_session(sessionmaker(autocommit=False,
                                         autoflush=False,
                                         bind=engine))
session = db_session()

Base.metadata.create_all(engine)

oPrice = Price(2.5, 2.5)
oPrice.currency = Currency("EUR")
session.add(oPrice)

tPrice = Price(5.5, 1.5)
tPrice.currency = Currency("EUR")
session.add(tPrice)

session.commit()

This creates an error

sqlalchemy.exc.IntegrityError: (IntegrityError) column unit is not unique u'INSERT INTO "Currency" (unit) VALUES (?)' ('EUR',)

What is the best strategy for populating my database, such that I ensure that my Currency.id and Price.currency_id mapping is correct? Should I make the model-classes look for uniqueness before they are initialized, and do I do that in associated with the other table?

È stato utile?

Soluzione

I'd second what Antti has suggested since currencies have standard codes like 'INR', 'USD' etc, you can make currency_code as primary key.

Or in case you want to keep the numeric primary key then one of the options is: http://www.sqlalchemy.org/trac/wiki/UsageRecipes/UniqueObject

edit (adding example based on the recipe in the link above, the one with class decoartor)

database.py

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

engine = create_engine('sqlite:///example.db', echo=True)
db_session = scoped_session(sessionmaker(autocommit=False,
                                     autoflush=False,
                                     bind=engine))

model.py

from sqlalchemy import Column, Integer, String
from sqlalchemy import Float, BigInteger, ForeignKey
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.declarative import declarative_base

from database import db_session

Base = declarative_base()

def _unique(session, cls, hashfunc, queryfunc, constructor, arg, kw):
    cache = getattr(session, '_unique_cache', None)
    if cache is None:
        session._unique_cache = cache = {}

    key = (cls, hashfunc(*arg, **kw))
    if key in cache:
        return cache[key]
    else:
        with session.no_autoflush:
            q = session.query(cls)
            q = queryfunc(q, *arg, **kw)
            obj = q.first()
            if not obj:
                obj = constructor(*arg, **kw)
                session.add(obj)
        cache[key] = obj
        return obj

def unique_constructor(scoped_session, hashfunc, queryfunc):
    def decorate(cls):
        def _null_init(self, *arg, **kw):
            pass
        def __new__(cls, bases, *arg, **kw):
            # no-op __new__(), called
            # by the loading procedure
            if not arg and not kw:
                return object.__new__(cls)

            session = scoped_session()

            def constructor(*arg, **kw):
                obj = object.__new__(cls)
                obj._init(*arg, **kw)
                return obj

            return _unique(
                    session,
                    cls,
                    hashfunc,
                    queryfunc,
                    constructor,
                    arg, kw
               )

      # note: cls must be already mapped for this part to work
        cls._init = cls.__init__
        cls.__init__ = _null_init
        cls.__new__ = classmethod(__new__)
        return cls

    return decorate




@unique_constructor(
db_session,
lambda unit: unit,
lambda query, unit: query.filter(Currency.unit == unit)
)
class Currency(Base):
    __tablename__ = 'Currency'

    id = Column(Integer, primary_key=True)
    unit = Column(String(16), unique=True)

    def __init__(self, unit):
        self.unit = unit

class Price(Base):
    __tablename__ = 'Price'

    id = Column(BigInteger, primary_key=True)

    currency_id = Column(Integer, ForeignKey("Currency.id"), nullable=False)
    currency = relationship("Currency", backref="Currency.id")

    hour1 = Column(Float)
    hour2 = Column(Float)

    def __init__(self, hour1, hour2):
        self.hour1 = hour1
        self.hour2 = hour2

script.py:

from model import *
from database import engine, db_session as session

Base.metadata.create_all(engine)

oPrice = Price(2.5, 2.5)
oPrice.currency = Currency("EUR")
session.add(oPrice)

tPrice = Price(5.5, 1.5)
tPrice.currency = Currency("EUR")
session.add(tPrice)

session.commit()

Altri suggerimenti

The best simplest solution is to use the currency codes as the primary keys in Currency, and foreign keys in Price. Then you can have

price.currency_id = "EUR"

This also makes your database tables more readable - as in you won't have 28342 but 'GBP'.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top