Hi,
I want to implement my own internationalization type for tables like this one where the different languages are stores in columns rather than an extra i.e. country_i18n table.
I know this is not so flexible when adding extra languages but our goal is to eliminate as much joins as possible.
CREATE TABLE `country` (
`id` int(11) auto_increment NOT NULL AUTO_INCREMENT,
`iso2` char(2) NOT NULL,
`name_en` varchar(64),
`name_de` varchar(64),
PRIMARY KEY (id)
) ENGINE=InnoDB;
insert into `country`(`id`,`iso2`,`name_de`,`name_en`) values (1,'DE','Germany','Deutschland');
insert into `country`(`id`,`iso2`,`name_de`,`name_en`) values (2,'IT', 'Italy','Italien');
The goal is to have a mapped class Country with a property “name” that is populated depending on a current language setting i.e. in the user session.
With the help of the documentation I did it this way:
# global language setting (for this example in a global var)
GLOBAL_LANGUAGE = 'en'
# custom column clause: modifies the column name depending on GLOBAL_LANGUAGE
class i18NColumnClause(ColumnClause):
pass
@compiles(i18NColumnClause)
def compile_i18n_column_clause(element, compiler, **kw):
return '%s_%s' % (element.name, GLOBAL_LANGUAGE)
# custom type
class i18NType(sqlalchemy.types.TypeDecorator):
impl = sqlalchemy.types.Unicode
def column_expression(self, colexpr):
return i18NColumnClause(colexpr)
# test class for custom type
class Country(Base):
__tablename__ = 'country'
id = Column(Integer, primary_key=True)
iso2 = Column(String(2))
name = Column(i18NType(64))
def __repr__(self):
return "<Country(%d/%s/%s)>" % (self.id, self.iso2, self.name)
This works for read operations because the new type uses a custom column_expression so I can do something like this:
germany = session.query(Country).get(1)
print germany # <Country(1/DE/Germany)>
Unfortunately it does not work when saving an object:
australia = Country()
australia.iso2 = 'AU'
australia.name = 'Australia'
session.add(australia)
session.commit()
The following query is executed and fails because the column “name” is not in table, it should be name_en:
INSERT INTO country (iso2, name) VALUES ('AU', 'Australia')
What am I missing? Or is this a practicable approach at all? Or are there better ways to do that?
Can anyone help?
Cheers, Alex
(I am using Sqlalchemy 0.8.0b2, mysql 5.5.16 and python 2.7.3)
@compiles(i18NColumnClause)def compile_i18n_column_clause(element, compiler, **kw):return '%s_%s' % (element.name, GLOBAL_LANGUAGE)# custom typeclass i18NType(sqlalchemy.types.TypeDecorator):impl = sqlalchemy.types.Unicodedef column_expression(self, colexpr):return i18NColumnClause(colexpr)# test class for custom typeclass Country(Base):__tablename__ = 'country'id = Column(Integer, primary_key=True)iso2 = Column(String(2))name = Column(i18NType(64))def __repr__(self):
On Jan 5, 2013, at 7:42 AM, Alexander Jacob wrote:@compiles(i18NColumnClause)def compile_i18n_column_clause(element, compiler, **kw):return '%s_%s' % (element.name, GLOBAL_LANGUAGE)# custom typeclass i18NType(sqlalchemy.types.TypeDecorator):impl = sqlalchemy.types.Unicodedef column_expression(self, colexpr):return i18NColumnClause(colexpr)# test class for custom typeclass Country(Base):__tablename__ = 'country'id = Column(Integer, primary_key=True)iso2 = Column(String(2))name = Column(i18NType(64))def __repr__(self):
Interesting approach with the column_expression(), though that seems like it would circumvent the labeling behavior of the compiler and cause more complex expressions to fail. The INSERT case might work if you also implemented bind_expression() but I'm not sure if that approach overall doesn't break too much of the existing compilation functionality.
A more direct route with @compiles would be to compile Column, but still tricky, in particular that the compilation process for a Column is very involved. At the very least, for that to work it would have to be like this:class i18NColumn(Column): # note it has to be Column, not ColumnClausepass@compiles(i18NColumn)def compile(element, compiler, **kw):text = compile.visit_column(element, **kw)text = text.replace(element.name, '%s_%s' %(element.name, GLOBAL_LANGUAGE))return textso that existing labeling rules are not circumvented. but even that I'm not 100% sure might not have some gotchas.
Another variant on this would be to use the before_cursor_execute event to just do a search and replace on the SQL statement before it's passed to the DBAPI, I sometimes recommend that as well. Giving the "fake" column a name that clearly indicates internationalization, like Column("name_<i18N>"), is a good way to give a statement filter a clear path to where those names would go. If you need this translation behavior everywhere, that might be the best approach.
Though here, there's a less intrusive way to get this behavior if ORM level is all you need which is by using hybrids:class Country(Base):# ...name_de = Column(String)name_en = Column(String)@hybrid_propertydef name(self):return getattr(self, "name_%s" % GLOBAL_LANGUAGE)@name.setterdef name(self, value):setattr(self, "name_%s" % GLOBAL_LANGUAGE, value)the above can be generalized:def make_hybrid(name):@hybrid_propertydef name_attr(self):return getattr(self, "%s_%s" % (name, GLOBAL_LANGUAGE))@name_attr.setterdef name_attr(self, value):setattr(self, "%s_%s" % (name, GLOBAL_LANGUAGE), value)name_attr.__name__ = namereturn name_attr