Hi again Michael,
I'm still working with this problem but have changed it so that I'm no
longer trying to CAST in the database, since you made me aware about the
limitations of mixed types in traditional RDBMS.
What I want is a column, casted_value, on the model that gives me the
CASTed (decoded) value column and when writing to it it should encode and
update value column. Unfortunately I don't have the type on the same table
(variable_value) but in another table, variable_register.
I've been looking at both @property and @hybrid_property + expression but
I've ran into two problems:
1. Is that I need the value to be loaded upfront with a
session.query(model)
2. I want to avoid doing extra queries when casting in the casted_value. My
property would have to use a relation (register) to figure out the type:
@property
def dynamic_value(self):
if self.register.type == 'date':
return date_from_string(self.
strvalue)
elif self.register.type == 'number':
return Decimal(self.strvalue)
# ... etc.
I've resorted to a solution that feels overly complicated but works. The
solution is a column_property with a custom select that embeds the type in
the select. Due to how our API works all read, write and filter must be
pointed to the same column so to make this work I have a Comparator to
allow filtering on the value column, an AttributeExtension to encode and
update the value column when casted_value is set and a TypeDecorator to
decode (CAST) the query result to the appropriate type:
class Comparator(sqlalchemy.orm.properties.ColumnProperty.Comparator):
'''Comparator class to compare on value instead of casted_value.'''
def __init__(self, column, mapper):
self.column = column
self.mapper = mapper
super(Comparator, self).__init__(column, mapper)
def __lt__(self, other):
return self.mapper.c.value < other
def __gt__(self, other):
return self.mapper.c.value > other
def __eq__(self, other):
return self.mapper.c.value == other
def __nq__(self, other):
return self.mapper.c.value != other
class UpdateCustomAttributeValue(sqlalchemy.orm.interfaces.
AttributeExtension):
def set(self, state, value, oldvalue, initiator):
'''Set value on custom attribute instead of casted_value.'''
obj = state.obj()
obj.value = encode(value)
return value
class DynamicTypeDecorator(types.TypeDecorator):
'''Type decorator class for converting links.'''
impl = types.Unicode
def process_result_value(self, raw_value, dialect):
'''Process query result and cast to appropriate type.'''
try:
value, custom_attribute_type = raw_value.split(separator)
except:
return None
return decode(value, custom_attribute_type)
class Variable_Value(Base):
'''Represent a custom attribute value.'''
__tablename__ = 'variable_value'
__table_args__ = table_args
registerid = Column(
types.CHAR(36), ForeignKey('variable_register.registerid'),
default=None, primary_key=True
)
register = relation('Variable_Register', backref=backref('values'))
value = Column(types.UnicodeText)
entityid = Column(types.CHAR(36), primary_key=True)
@classmethod
def __declare_last__(cls):
from .custom_attribute import Variable_Register
variable_register = Variable_Register.__table__.alias()
variable_value = cls.__table__.alias()
# Join to register to figure out the type.
from_obj = variable_value.join(
variable_register,
variable_register.c.registerid == variable_value.c.registerid
)
dynamic_cast = sqlalchemy.type_coerce(
variable_value.c.value + separator + variable_register.c.type,
DynamicTypeDecorator
)
cls.casted_value = column_property(
sqlalchemy.select(
[dynamic_cast],
from_obj=fromObj
).where(
sqlalchemy.and_(
variable_value.c.entityid == cls.entityid,
variable_value.c.registerid == cls.registerid
)
).as_scalar(),
comparator_factory=Comparator,
extension=[UpdateCustomAttributeValue()]
)
With this I can read:
for variable in session.query(model.Variable_Value):
# Correct type and no extra queries are issued even though the type is
stored on a relation.
print variable.casted_value
I can also write:
# Run this and do my own encoding
variable.casted_value = True # 100.1, datetime.now(), etc.
I can query on it with MySQL implicit casting to number, boolean or other
values:
print
session.query(model.Variable_Value).filter(model.Variable_Value.casted_value
> 5).all()
On Wednesday, September 16, 2015 at 5:00:02 PM UTC+2, Michael Bayer wrote:
>
>
>
> On 9/16/15 6:46 AM, Mattias Lagergren wrote:
>
>
> The important part is the casting inside of the case expression:
>
> dynamic_cast = sqlalchemy.sql.expression.case(
> [
> (
> variable_register.c.type == 'number',
> sqlalchemy.cast(variable_value.c.value, types.Numeric)
> ),
> (
> variable_register.c.type == 'date',
> sqlalchemy.cast(variable_value.c.value, types.Date)
> )
> ],
> else_=variable_value.c.value
> )
>
> The result when using this in a query only gives back strings, <type
> 'str'>:
>
>
> A creative idea, but unfortunately that's not how SQL works. A given
> column that comes back in a result tuple must be of a fixed datatype on a
> traditional RDBMS. The datatype for an individual result-column is not
> dynamic per-row (except on SQLite). SQLAlchemy Core makes this same
> assumption, so the datatype can't switch like this on a per-row basis.
>
> Also, the mixing of SQL-side CAST with a SQL column that is essentially
> just a string is not correct. On the SQL side, operations with this column
> all deal with the same type (string) so there's no need for CASTing within
> the database.
>
> In this case since all the vardata is just in a char column, there's no
> need for SQL-side manipulation and you only need to make sure that Python
> types are converted to String on the setter side and converted to
> appropriate datatype on the getter side. A simple Python @property would
> achieve this:
>
> class MyClass(Base):
> # ...
>
> @property
> def dynamic_value(self):
> if self.type == 'date':
> return date_from_string(self.strvalue)
> elif self.type == 'number':
> return Decimal(self.strvalue)
> # ... etc.
>
> @dynamic_value.setter
> def dynamic_value(self, value):
> if self.type == 'date':
> self.strvalue = date_as_string(value)
> elif self.type == 'number':
> self.strvalue = str(value)
> # ... etc.
>
>
>
>
>
>
> for item in session.query(Variable_Value):
> print item, type(item)
>
> When reading the SqlAlchemy docs I can see that sqlalchemy.cast 1. Creates
> a SQL CAST statement, 2. Associates it with an appropriate type. I can see
> that casting in a case will make this impossible for SqlAlchemy to solve.
>
> Do you have any thoughts on the approach or any suggestions on how I can
> do it differently. This is to improve an existing system and a solution
> that does not involve changes to the database schema would be ideal.
>
> Thanks!
> Mattias L
>
> --
> You received this message because you are subscribed to the Google Groups
> "sqlalchemy" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to [email protected] <javascript:>.
> To post to this group, send email to [email protected]
> <javascript:>.
> Visit this group at http://groups.google.com/group/sqlalchemy.
> For more options, visit https://groups.google.com/d/optout.
>
>
>
--
You received this message because you are subscribed to the Google Groups
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.