lundi 19 décembre 2016

Total logic / data decoupling with SQLAlchemy

I am trying (purely as an exercice) to build a graphical software able to draw some charts representing Market data and doing some simple analysis.

I am using Python3.5 + SQLAlchemy1.1 for persistence (and PyQt but not relevant here).

To make it simple, I want to have a class 'Stock' representing a trading stock and a class 'Exchange' representing a market exchange (Paris market, London market, etc.). Each Stock has a Market.

By using Declarative Mapping, this would give the following:

from sqlalchemy import Column, ForeignKey, String
from sqlalchemy.orm import relationship

Base = declarative_base()

class Exchange(Base):

    __tablename__ = 'exchange'

    ticker = Column(String(2), primary_key=True)
    name   = Column(String(20))

class Stock(Base):

    __tablename__ = 'stock'

    ticker          = Column(String(5), primary_key=True)
    exchange_ticker = Column('exchange', String(2), ForeignKey('exchange.ticker'),
                             primary_key=True)
    name            = Column(String(20), unique=True)

    exchange = relationship(Exchange)

But as I said, this is purely an exercice, and I would like to make it more 'decoupled': I would like to totally separate the 'business logic' from the 'persistence layer' (I am using quotes because I see these expressions everywhere and I think I got their meaning, but I am not a professional developer so I may not see the whole thing).

Ultimately, I would like to separate the business logic and the persistence layer in 2 modules (or packages, same), potentially 'gitsubmoduling' them, and be able to install and launch the logic module without any persistence (without even installing sqlalchemy / psycopg2 for PostgreSQL, etc.). This is a good practice, or at least a good objective, right?

So I had a deeper look at SQLAlchemy doc and I found the concept of Classical Mapping (automatically handled by Declarative Mapping). This would gives:

core.py (purely logic, possibly functional without any persistence)

class Exchange:

    def __init__(self, ticker: str, name: str):
        self.ticker = ticker
        self.name = name

class Stock:

    def __init__(self, ticker: str, exchange: Exchange, name: str=None):
        self.ticker = ticker
        self.exchange = exchange
        self.name = name

    # Some business logic: correlations, data analysis, etc.

model.py (mapping logic to database)

from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, Table, UniqueConstraint
from sqlalchemy.orm import mapper

from core import Exchange, Stock

metadata = MetaData()

stock = Table(
    'stock', metadata,
    Column('ticker',   String(5), primary_key=True),
    Column('exchange', String(2), ForeignKey('exchange.ticker'), primary_key=False),
    Column('name',     String(20))
)

exchange = Table(
    'exchange', metadata,
    Column('ticker', String(2), primary_key=True),
    Column('name', String(20))
)

mapper(Stock, stock)
mapper(Exchange, exchange)

This does not work: when I try to insert some fixtures:

sqlalchemy.exc.InterfaceError: (sqlite3.InterfaceError) Error binding parameter 2 - probably unsupported type. [SQL: 'INSERT INTO stock (ticker, name, exchange) VALUES (?, ?, ?)'] [parameters: ('CAC', None, <core.Exchange object at 0x108bd0fd0>)]

As you can see, he tries to 'insert' an Exchange object directly. Therefore, I tried (with different options and configs):

mapper(Stock, stock, properties={
    'exchange': relationship(Exchange)
})

But SQLAlchemy raises an error:

sqlalchemy.exc.ArgumentError: WARNING: when configuring property 'exchange' on Mapper|Stock|stock, column 'exchange' conflicts with property '<RelationshipProperty at 0x1096d5cf8; exchange>'.

It seems that whatever I try (I tried a lot of other stuffs, with exclusive_properties, naming the relationship, etc.) SQLAlchemy wants a relationship object in my Stock (which makes sense..). So I am wondering is that level of decoupling is really reachable.

My questions are:

Aucun commentaire:

Enregistrer un commentaire