sqlalchemy polymorphism without discriminators - python

I am trying to use an external library which defines a class model in my own program. I want the classes I define to be the same in all respects to their parents from the library, except that I want to append some helper methods to my local extensions. For example:
External Library:
Base = declarative_base()
class BaseUser(Base):
__tablename__ = 'user'
email = Column(String(100), nullable=False, unique=True)
password = Column(String(128), nullable=False)
address_uid = Column(Integer, ForeignKey('address.uid'))
address = relationship('BaseAddress', back_populates="users")
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.address = BaseAddress()
class BaseAddress(Base):
__tablename__ = 'address'
street = Column(String(100))
unit = Column(String(32))
city = Column(String(64))
state = Column(String(32))
postal = Column(String(32))
country = Column(String(32))
users = relationship('user', back_populates="address")
Local model:
class User(BaseUser):
def in_country(county):
return self.address.country == country
class Address(BaseAddress):
pass
The goal here is to create subclasses which sqlalchemy need not distinguish from their parents. If I insert an Address into User.address, for example, sqlalchemy should not complain about a type mismatch (Address instead of the expected BaseAddress).
The only way of doing this that I can discern would involve using polymorphic_on in the parent classes. I don't want to do this, because it doesn't accurately model what is happening. It would require a discriminator, and it might behave strangely in the event I used a migration script locally. Is there a way with sqlalchemy to achieve polymorphism (I think it's called "ad-hoc polymorphism") without using discriminators, or some other way of achieving my goal?
UPDATE
I believe I could get part of the way there by using enable_typechecks=False on the relationships. However, this doesn't exactly get me what I want. I'd like to be able to do things like User.query() and get back a User rather than a BaseUser.

Related

SQLAlchemy and DDD

I am implementing an application using a DDD approach. I want to separate domain model into a separate package, declare a Repository and the implementation will use SQLAlchemy as ORM in a separate package.
From the domain standpoint, a User looks like:
class User:
def __init__(self, name):
self.id = autogenerate_id()
self.name = name
But for SQLAlchemy, it should be implemented as:
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
My question is: is there any way of minimizing the amount of work that is needed to define the SQLAlchemy model such that:
I don't have to write mapping code from domain-SQLAlchemy and viceversa
I can "guarantee" that I didn't forget to include any attribute from domain in the SQLAlchemy counterpart (when making changes to the domain model).
If I understand correctly you want to have two implementations of aggregate DTO without mapping sqlalchemy table to python data struct. In this case, I would recommend you create UserDTO interface that will be implemented by SQLAlchemy Model and by you plain domain implementation (for tests?).
class User:
id: int
name: Text
class PlainUser(User):
def __init__(self, name):
self.id = autogenerate_id()
self.name = name
class DBUser(db.Model, User):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
About mapping DTO to DB and strategies on how to accomplish it. Also about I write more in this post Persistency of DDD aggregate
Hope it will help you with other question that you will have down the road with DDD (for example separate migrations in modular monolith architecture).

SQL Alchemy Error when moving colums to init method

This is my User object that I'm using to write to MySQL using SQLAlchemy
class User(Base):
def __init__(self):
self.id = Column(Integer, primary_key=True)
self.first_name = Column(String)
self.last_name = Column(String)
self.email_id = Column(String)
self.mobile = Column(String)
self.username = Column(String)
self.hashed_password = Column(String)
def set_first_name(self, first_name):
self.first_name = first_name
def set_last_name(self, last_name):
self.last_name = last_name
def set_email_id(self, email):
self.email_id = email
def set_mobile(self, mobile):
self.mobile = mobile
def set_username(self, username):
self.username = username
def set_hashed_password(self, password):
self.hashed_password = password
def __repr__(self):
return "<User(id=%d, first_name=%s, last_name=%s, email_id=%s, mobile=%s, username=%s)>"%(self.id, self.first_name, self.last_name, self.email_id, self.mobile, self.username)
When I run the program, this is what I get the following error,
sqlalchemy.exc.ArgumentError: Mapper Mapper|User|user could not assemble any primary key columns for mapped table 'user'
This code works if I take the attribute definitions out of the init and remove the self prefix. Can someone help me understand what's going on here?
SQLAlchemy's declarative base mechanism establishes a Python metaclass. That means that SQLAlchemy will specially process the definition of your class.
The purpose of that processing is to construct an sqlalchemy.orm.Mapper for each mapped class. That mapper represents the mapping between your database tables and your class.
In order to do that, SQLAlchemy generally needs to be able to find a primary key. This is required in order to define the identity associated with each mapped instance, so that mapped objects can be cached/found in sessions. That at least needs to be possible when your mapped class is constructed.
That means that you need to define the column of at least the primary key on the class.
Other answers have explained that much, although I think I've provided a bit more detail.
There is a more fundamental problem though.
id = Column(Integer, primary_key = True)
is of course a call to the Column function you import from SQLAlchemy. However, the return from the Column function is a schema item. This schema item is converted by declarative base into a descriptor similar to the kind of descriptor that the property decorator gives you. Such descriptors only work on a class, not an instance of that class.
Let's say I have a class mapped to a table called User and an instance of that user in a variable bob.
User.id
Is a description of the identity column. However
bob.id
is the number that identifies Bob in the users table.
That is, columns aren't intended to be assigned to members of self, they are intended to be assigned to classes.
So:
You need to have at least the primary key column on your class when you define it.
It's generally a good idea to have all your Columns there.
You can add a Column definition to your class later, although things will only work if you arrange for that column to get into your table
It's always wrong to add a Column to an instance of a mapped class. self.x = Column is always wrong.
The SQLAlchemy ORM (almost) always requires a primary key. You have indeed defined one inside your __init__() function. The problem is that __init__() doesn't get called until you create a User object. I assume you create your database before a User object ever gets created. Thus as far as the SQLAlchemy ORM is concerned, a primary key does not exist for User (nor any of the other attributes declared inside __init__).
The solution, as I think you already found from your last line, is to declare them as class attributes where as soon you do something like from models import User the attributes are defined and SQLAlchemy can properly build your User table.
class User(Base):
id = Column(Integer, primary_key=True)
first_name = Column(String)
last_name = Column(String)
email_id = Column(String)
mobile = Column(String)
username = Column(String)
hashed_password = Column(String)
def set_first_name(self, first_name):
self.first_name = first_name
...
You don't have to declare the column definitions inside the __init__() function. Change your class definition to something like this and it should work:
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
first_name = Column(String)
last_name = Column(String)
email_id = Column(String)
mobile = Column(String)
username = Column(String)
hashed_password = Column(String)

Creating second model upon instantiation of the first within sqlachemy

I am currently working with some legacy code that looks as follows:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Unicode
from sqlalchemy.dialects.postgresql import ARRAY, TEXT
Base = declarative_base()
class Book(Base):
__tablename__ = 'book'
id = Column(Integer, primary_key=True)
title = Column(Unicode)
keywords = Column('keywords', ARRAY(TEXT), primary_key=False)
The keywords are currently being kept as an array, but I'd like to flatten this out and have them be in their own separate model
class Keyword():
__tablename__ = 'keyword'
id = Column(Integer, primary_key=True)
book_id = Column(Integer, ForeignKey('book.id', ondelete='cascade'),
nullable=False)
keyword = Column(Unicode)
How can I make it such that when a Book() is created, it also creates the
accompanying keywords? As an intermediate step for migrating the API, I'd like to keep the current array column, but also have the accompanying Keyword() instances be created.
I could do this within an __init__ method, but would need to know what the current Session() was, in order to run a commit. I could also perhaps use a property attribute, attached to keywords, but am not sure how that would work given that I am working with a class that inherits from SQLAlchemy's base, and not with a regular class that I have defined. What's the correct way to do this?
You can use object_session to find out the session of a given instance.
But if you define relationship between a Book and Keywords, you should not need even bother:
class Book(Base):
# ...
rel_keywords = relationship('Keyword', backref='book')
def init_keyword_relationship(self):
for kw in self.keywords:
self.rel_keywords.add(Keyword(keyword=kw))
sess = # ... get_session...
books = sess.query(Book).all()
for book in books:
book.init_keyword_relationship()
sess.commit()
However, I would do a migration once and get rid of the keywords array in order not to add a logic to keep those in sync.

Get custom attributes of a class Python

Is it possible to get the name of our custom attributes of a class in Python ? For instance, here's my class :
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
login = db.Column(db.String(100))
password = db.Column(db.String(100))
first_name = db.Column(db.String(100))
last_name = db.Column(db.String(100))
email = db.Column(db.String(100))
age = db.Column(db.Integer)
sex = db.Column(db.String(10))
What I want is to get the list of my class attributes (and only those that I defined !). I was thinking about using dir(self) and filtering on those not starting with __ but it's not really revelant because there are other fields who are built-in such as metadata, query and so on.
I saw a function getattr (or getattribute) but it's only for a given field.
I don't want to use a dict of keys because it have to stay generic and I don't want to modify the dict everytime I add a field.
As I'm using SqlAlchemy ORM, I got this when trying self.__dict__ :
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x7ffbcf252050>}
I also tried a lot of things such as those described here :
Python dictionary from an object's fields but nothing worked.
Does anyone have a solution ?
Thanks !

How do I extend a SQLAlchemy bound declarative model with extra methods?

For example, I have a declarative class on module a:
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
addresses = relationship("Address", backref="user")
Now, in module b I want to use the mapped entity, but add a method:
from a import User
class UserWithExtraMethod(User):
def name_capitalized(self):
return self.name.capitalize()
user = UserWithExtraMethod()
print(user.name_capitalized)
However, when I run the script, I will get the following error:
InvalidRequestError: Multiple classes found for path "User" in the registry of this declarative base. Please use a fully module-qualified path.
What have I missed when declaring the user entity? I would like to reuse the previous declared entity.
I am expecting something would be like:
class UserWithExtraMethod(User):
___magic_reuse_previous_mapper__ = True
def name_capitalized(self):
return self.name.capitalize()
Unless you've got a particular reason to have separate classes, you should just write:
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
addresses = relationship("Address", backref="user")
def name_capitalized(self):
return self.name.capitalize()
Since the name_capitalized is not special as far as SQLAlchemy is concerned (it's not a ColumnExpression or some such), it is completely ignored by the mapper.
Actually, there's an even better way to do this; your version works fine for instances of User, but is of no use in sql expressions.
from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
class User(Base):
# ... body as before
#hybrid_method
def name_capitalized(self):
return self.name.capitalize()
#name_capitalized.expression
def name_capitalized(cls):
# works for postgresql, other databases spell this differently.
return sqlalchemy.func.initcap(cls.name)
which will allow you to do things like:
>>> print Query(User).filter(User.name_capitalized() == "Alice")
SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE initcap(users.name) = :initcap_1
Perhaps a little late for this reply. Do you have any other relationships setup that are pointing to User?
For example, if you have Address defined as:
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
address = Column(String(50))
Users = relationship("User", backref="addresses")
when Address is trying to resolve to which User within the declarative base to point to, it will find two of them. To verify try Base._decl_class_registry['User']. This is similar to this topic covered by Michael.
In ./sqlalchemy/ext/declarative/clsregistry.py there is an example on how to use the fully qualified path. In this case it would be changing the relationship within address from Users = relationship("User", backref="addresses") to
Users = relationship("a.User", backref="addresses")
Hope this helps point you in the right direction for debugging.
Hacky, but why not just monkey-patch the User class for your purpose instead of inheriting from it?
# modude b
from a import User
def name_capitalized(self):
return self.name.capitalize()
User.name_capitalized = name_capitalized
user = User() # and it has extra-method as well
print(user.name_capitalized)
This may not work for you. I had a similar issue. I ended up passing an instance of User to UserWithExtraMethod during instantiation
class UserWithExtraMethod(object):
def __init__(self, user):
self.user = user
def name_capitalized(self):
return self.user.name.capitalize()
Hope this helps

Categories

Resources