- Entity selection (includes some real-time jQuery filtering in case the model gets really large with lots of entities)
- All CRUD features (Add, Edit, Delete, View)
- Searching against any fields in the model
- Sorting and Paging
- Client-side & server-side validation
- MVC-type framework (the code should be intuitive to a .NET MVC developer)
- Security/Authentication
- Extended relational features (like custom web controls for selecting related items, etc…)
- Other things not specifically outlined above …
Step 1 – LLBLGen and SqlAlchemy model generation
With LLBLGen, you can begin a project by creating a model from a database, or just create the model in the model designer. I wanted to get up and running fast, so I connected to a Northwind database in my local SQL Server, and brought the model into the designer that way (as a shortcut). One of the nice things is that because the entire code-generation process to Python is custom, and because SqlAlchemy works with a quantity of dialects, this process generates just as well for SQLite, as for MySQL, as for SQLServer. The model looks like this in LLBLGen:
/*
* This linqpad file can be used to create LLBLGen templates. When you own a licensed version of Linqpad, you get full
* .NET intellisense, which makes this a great tool to build and leverage the full LLBLGen SDK to generate useful code
* for your project starting from either a database model, or just an entity model.
*/
public void Main(){ LLBLGen.LinqpadExtensions.LptFileGenerator.GenerateLptFile( Util.CurrentQueryPath, Console.Out); }
public Generator _executingGenerator { get; set; }
public EntityDefinition _activeObject { get; set; }
public IDictionary<string, TaskParameter> _parameters { get; set; }
// START HERE – DO NOT ERASE THIS COMMENT LINE
public string GenerateCode() {
var sb = new StringBuilder();
sb.Append(
@"#!/usr/bin/env python
# models.py
from sqlalchemy import *
from sqlalchemy.orm import scoped_session, sessionmaker, relation, relationship
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.dialects.mssql import *
Base = declarative_base()
def init_db(engine):
Base.metadata.create_all(bind=engine)
");
navnavPairs = new List<string>();
foreach(var entity in _executingGenerator.Entities.OrderBy(e => e.Name)) {
sb.AppendLine(BuildEntityTableDefinition(entity));
}
foreach(var entity in _executingGenerator.Entities.OrderBy(e => e.Name)) {
sb.AppendLine(BuildEntityClassDefinition(entity));
}
sb.AppendLine(@"
def main():
engine = create_engine('mssql+pyodbc://"+BuildConnectionString()+@"', convert_unicode=True, echo=True)
init_db(engine)
db = scoped_session(sessionmaker(bind=engine))
#try a count on each table to make sure it's setup properly
counts = [");
foreach(var entity in _executingGenerator.Entities.OrderBy (e => e.Name)) {
sb.AppendLine(" str(db.query(" + entity.Name + ").count()) + \" " + entity.Name + " records\",");
}
sb.AppendLine(@" ]
for c in counts:
print(c)
if __name__ == '__main__':
main()
");
return sb.ToString();
}
List<string> navnavPairs;
public string BuildEntityClassDefinition(EntityDefinition entity) {
var entityMapping = GetEntityMapping(entity);
var sb = new StringBuilder();
sb.AppendFormat("class {0}(Base):\r\n", entity.Name);
sb.AppendFormat(" __table__ = {0}Table\r\n", entity.Name);
sb.Append(BuildRelationDefinitions(entity, navnavPairs));
sb.AppendFormat("\r\n{0}", DotNetTemplateEngine.GetUserCodeRegion("additional_model_information_" + entity.Name.Replace(' ','_').ToLower(), "#").Replace("\t"," "));
return sb.ToString();
}
public void LogNavNavPair(string entity1, string navigator1, string entity2, string navigator2, StringBuilder sb){
//sb.AppendLine(string.Format("#LOG: {0}-{1}-{2}-{3}", entity1, navigator1, entity2, navigator2));
}
public bool HasNavNavPairAlreadyBeenMapped(string entity1, string navigator1, string entity2, string navigator2, List<string> navnavPairs) {
return navnavPairs.Contains(string.Format("{0}-{1}-{2}-{3}", entity1, navigator1, entity2, navigator2)) || navnavPairs.Contains(string.Format("{0}-{1}-{2}-{3}", entity2, navigator2, entity1, navigator1));
}
public void RegisterNavNavPairUniqueIdentifiers(string entity1, string navigator1, string entity2, string navigator2, List<string> navnavPairs) {
navnavPairs.AddRange(new[]{
string.Format("{0}-{1}-{2}-{3}", entity1, navigator1, entity2, navigator2),
string.Format("{0}-{1}-{2}-{3}", entity2, navigator2, entity1, navigator1)
});
}
public string BuildRelationDefinitions(EntityDefinition entity, List<string> navnavPairs) {
var entityMapping = GetEntityMapping(entity);
var tableName = entityMapping.MappedTarget.Name;
var sb = new StringBuilder();
var fks = entityMapping.MappedTarget.ForeignKeyConstraints.Where(fkc => !fkc.MarkedForDeletion).OrderBy(fkc => fkc.ConstraintName);
var allRelationshipInfosToTraverse = GeneratorUtils.GetAllRelationshipInfosForEntity(_executingGenerator, entity);
var ert = new[]{false, false, false, false};
var oneToOneAlreadyMapped = false;
foreach(var relationshipInfo in allRelationshipInfosToTraverse.OrderBy(ri=>ri.Navigator).OrderBy(ri => ri.RelationshipType))
{
string property = string.Empty;
// omit relationships that are flagged in the designer as hidden
if(relationshipInfo.NavigatorIsHidden) continue;
List<string> fragments = new List<string>();
var fkFieldsFromFkSide = new List<FieldElement>();
if(relationshipInfo.RelationshipType!=EntityRelationshipType.ManyToMany)
fkFieldsFromFkSide = relationshipInfo.NormalRelationship.GetFkFieldsFromFkSide().ToList();
switch(relationshipInfo.RelationshipType)
{
case EntityRelationshipType.ManyToMany:
if(!ert[0]){sb.AppendFormat("\r\n {0}\r\n", "#relation definitions: many to many with backref");ert[0]=true;}
var relationshipAsMtoM = (IndirectRelationshipEdge)relationshipInfo.Relationship;
if(!relationshipAsMtoM.IsPureManyToMany) {
//sql alchemy uses association objects for M:N relationships that are not pure
//TODO: let's implement this in a future version of this template
LogNavNavPair(relationshipInfo.ForEntity.Name, relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipInfo.OppositeNavigator, sb);
oneToOneAlreadyMapped = HasNavNavPairAlreadyBeenMapped(relationshipInfo.ForEntity.Name, relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipInfo.OppositeNavigator, navnavPairs);
sb.AppendFormat(" {3}{0} = relationship('{1}', backref='{2}')\r\n", relationshipInfo.Navigator, relationshipAsMtoM.DetermineIntermediateEntity().Name, relationshipInfo.OppositeNavigator, (oneToOneAlreadyMapped?"#see backref mapping on "+relationshipInfo.RelatedEntity.Name+" class –> ":""));
RegisterNavNavPairUniqueIdentifiers(relationshipInfo.ForEntity.Name, relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipInfo.OppositeNavigator, navnavPairs);
continue;
} else {
if((relationshipInfo.Relationship as NormalRelationshipEdge) != null && ((NormalRelationshipEdge)relationshipInfo.Relationship).EntityFkSide.IsPureManyToManyIntermediate) {
continue;
}
LogNavNavPair(relationshipInfo.ForEntity.Name, relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipInfo.OppositeNavigator, sb);
oneToOneAlreadyMapped = HasNavNavPairAlreadyBeenMapped(relationshipInfo.ForEntity.Name, relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipInfo.OppositeNavigator, navnavPairs);
sb.AppendFormat(" {4}{0} = relationship('{1}', secondary={2}Table, backref='{3}')\r\n", relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipAsMtoM.DetermineIntermediateEntity().Name, relationshipInfo.OppositeNavigator, (oneToOneAlreadyMapped?"#see backref mapping on "+relationshipInfo.RelatedEntity.Name+" class –> ":""));
RegisterNavNavPairUniqueIdentifiers(relationshipInfo.ForEntity.Name, relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipInfo.OppositeNavigator, navnavPairs);
}
break;
case EntityRelationshipType.OneToOne:
oneToOneAlreadyMapped = HasNavNavPairAlreadyBeenMapped(relationshipInfo.ForEntity.Name, relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipInfo.OppositeNavigator, navnavPairs);
if(!ert[1]){sb.AppendFormat("\r\n {0}\r\n", "#relation definitions: one to one with backref");ert[1]=true;}
sb.AppendFormat(" {3}{0} = relationship('{1}', backref=backref('{2}', uselist=False))\r\n", relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipInfo.OppositeNavigator, (oneToOneAlreadyMapped?"#see backref mapping on "+relationshipInfo.RelatedEntity.Name+" class –> ":""));
RegisterNavNavPairUniqueIdentifiers(relationshipInfo.ForEntity.Name, relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipInfo.OppositeNavigator, navnavPairs);
break;
case EntityRelationshipType.ManyToOne:
if(!ert[2]){sb.AppendFormat("\r\n {0}\r\n", "#relation definitions: many to one with backref (also takes care of one to many)");ert[2]=true;}
// watch out for self-referential properties
if(relationshipInfo.ForEntity.Name == relationshipInfo.RelatedEntity.Name) {
sb.AppendFormat(" {0} = relationship('{1}', primaryjoin=({3}.c.{4}=={3}.c.{5}), remote_side='{1}.{4}', backref='{2}')\r\n",
relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipInfo.OppositeNavigator,
string.Format("{0}Table", relationshipInfo.ForEntity.Name),
GetTableColumnName(entity, relationshipInfo.GetRelationshipFieldPairs().First().Value2 as FieldElement)/*GetFieldMapping(entity, relationshipInfo.GetRelationshipFieldPairs().First().Value2 as FieldElement).MappedTarget.FieldName*/,
GetTableColumnName(entity, relationshipInfo.GetRelationshipFieldPairs().First().Value1 as FieldElement)/*GetFieldMapping(entity, relationshipInfo.GetRelationshipFieldPairs().First().Value1 as FieldElement).MappedTarget.FieldName*/);
} else {
sb.AppendFormat(" {0} = relationship('{1}', backref='{2}')\r\n", relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipInfo.OppositeNavigator);
}
break;
case EntityRelationshipType.OneToMany:
if(!ert[3]){sb.AppendFormat("\r\n {0}\r\n", "#relation definitions: one to many");ert[3]=true;}
sb.AppendFormat(" {3}{0} = relationship('{1}', backref='{2}')\r\n", relationshipInfo.Navigator, relationshipInfo.RelatedEntity.Name, relationshipInfo.OppositeNavigator, "#see backref mapping on "+relationshipInfo.RelatedEntity.Name+" class –> ");
break;
}
}
return sb.ToString();
}
public string BuildEntityTableDefinition(EntityDefinition entity) {
var entityMapping = GetEntityMapping(entity);
var tableName = entityMapping.MappedTarget.Name;
var sb = new StringBuilder();
sb.AppendFormat("{0}Table = Table(u'{1}', Base.metadata,\r\n", entity.Name, tableName);
sb.AppendFormat("\r\n {0}\r\n", "#column definitions");
foreach(FieldElement field in entity.Fields.OrderBy(f => GetFieldMapping(entity, f).MappedTarget.OrdinalPosition))
sb.AppendFormat(" {0},\r\n", BuildFieldColumnDefinition(entity, field));
sb.AppendFormat("\r\n {0}\r\n", "#foreign keys");
sb.Append(BuildForeignKeyDefinitions(entity));
sb.AppendFormat("\r\n {0}\r\n", "#unique constraints");
sb.Append(BuildUniqueConstraintDefinitions(entity));
sb.AppendFormat("\r\n {0}\r\n", "#schema");
sb.AppendFormat(" schema=\"{0}\"\r\n", entityMapping.MappedTarget.ContainingSchema.SchemaOwner);
sb.AppendFormat("\r\n{0}", DotNetTemplateEngine.GetUserCodeRegion("additional_table_information_" + tableName.Replace(' ','_').ToLower(), "#").Replace("\t"," "));
sb.AppendLine(")");
return sb.ToString();
}
public string BuildFieldColumnDefinition(EntityDefinition entity, FieldElement field){
var entityMapping = GetEntityMapping(entity);
var fieldMapping = GetFieldMapping(entity, field);
var columnName = GetTableColumnName(entity, field)/*fieldMapping.MappedTarget.FieldName*/;
var sequenceInfo = fieldMapping.SequenceToUse == null ? null: ", Sequence('seq_" + entityMapping.MappedTarget.Name.ToLowerInvariant() + "_" + columnName.ToLowerInvariant() + "')";
return string.Format("Column(u'{0}', {1}{2}{3}{4})",
columnName, ProduceSqlAlchemyDbTypeString(fieldMapping), sequenceInfo,
(field.IsPartOfIdentifyingFields ? ", primary_key=True": ""),
(!field.IsOptional ? ", nullable=False": "")
);
}
public string BuildForeignKeyDefinitions(EntityDefinition entity) {
var entityMapping = GetEntityMapping(entity);
//var schema = entityMapping.MappedTarget.ContainingSchema.SchemaOwner;
var sb = new StringBuilder();
foreach(var fkc in entityMapping.MappedTarget.ForeignKeyConstraints.Where(fkc => !fkc.MarkedForDeletion).OrderBy(fkc => fkc.ConstraintName)) {
var fkTable = fkc.AppliesToTable;
var schemaName = fkTable.ContainingSchema.SchemaOwner;
var pkTable = fkc.PKSide;
var constraintName = fkc.ConstraintName;
var fkpkPairs = fkc.GetFkPkFieldPairs();
sb.AppendFormat(" ForeignKeyConstraint({0},{1},name={2}),\r\n",
string.Join(", ", fkpkPairs.Select(p=>p.Value1).Select(f=>string.Format("['{0}']", f.FieldName)).ToArray()),
string.Join(", ", fkpkPairs.Select(p=>p.Value2).Select(f=>string.Format("['{0}']", f.FullName/*.Replace(schemaName + ".","")*/)).ToArray()),
string.Format("'{0}'", constraintName));
}
return sb.ToString();
}
public string BuildUniqueConstraintDefinitions(EntityDefinition entity) {
var entityMapping = GetEntityMapping(entity);
var sb = new StringBuilder();
foreach(var uc in entityMapping.MappedTarget.UniqueConstraints.Where(uc => !uc.MarkedForDeletion).OrderBy(uc => uc.ConstraintName)) {
var tableName = uc.AppliesToTable.Name;
var constraintName = uc.ConstraintName;
sb.AppendFormat(" UniqueConstraint({0},name={1}),\r\n",
string.Join(", ", uc.Fields.Select(f => string.Format("'{0}'", f.FieldName)).ToArray()),
string.Format("'{0}'", constraintName));
}
return sb.ToString();
}
public EntityMapping GetEntityMapping(EntityDefinition entity) {
return (EntityMapping)_executingGenerator.ProjectDefinition.GetGroupableModelElementMapping(entity, _executingGenerator.DriverID);
}
public FieldMapping GetFieldMapping(EntityDefinition entity, FieldElement field) {
return GetEntityMapping(entity).GetFieldMappingOfField(field);
}
public string GetTableColumnName(EntityDefinition entity, FieldElement field) {
//return field.Name;
return GetFieldMapping(entity, field).MappedTarget.FieldName;
}
public string ProduceSqlAlchemyDbTypeString(FieldMapping fieldMapping)
{
/*
from sqlalchemy.dialects.mssql import \\cf0
BIGINT, BINARY, BIT, CHAR, DATE, DATETIME, DATETIME2, \\cf0
DATETIMEOFFSET, DECIMAL, FLOAT, IMAGE, INTEGER, MONEY, \\cf0
NCHAR, NTEXT, NUMERIC, NVARCHAR, REAL, SMALLDATETIME, \\cf0
SMALLINT, SMALLMONEY, SQL_VARIANT, TEXT, TIME, \\cf0
TIMESTAMP, TINYINT, UNIQUEIDENTIFIER, VARBINARY, VARCHAR
*/
var dbTypeAsString = fieldMapping.MappedTarget.DbTypeAsString.ToLowerInvariant();
var dotNetType = fieldMapping.MappedTarget.TypeDefinition.DBTypeAsNETType;
var length = fieldMapping.MappedTarget.Length;
var precision = fieldMapping.MappedTarget.Precision;
var scale = fieldMapping.MappedTarget.Scale;
var sb = new StringBuilder();
switch(dbTypeAsString) {
case "int":
sb.Append("INTEGER()");
break;
case "ntext":
sb.AppendFormat("UnicodeText(length={0})", length);
break;
case "smalldatetime":
sb.Append("DateTime()");
break;
case "tinyint":
sb.Append("Integer()");
break;
case "binary":
case "char":
case "nchar":
case "nvarchar":
case "varbinary":
case "varchar":
sb.AppendFormat("{0}(length={1})", dbTypeAsString.ToUpper(), length);
break;
case "float":
case "real":
sb.AppendFormat("{0}(precision={1})", dbTypeAsString.ToUpper(), precision);
break;
case "decimal":
case "numeric":
sb.AppendFormat("{0}(precision={1}, scale={2})", dbTypeAsString.ToUpper(), precision, scale);
break;
//case "bigint":
//case "bit":
//case "date":
//case "datetime":
//case "datetimeoffset":
//case "image":
//case "money":
//case "smallint":
//case "smallmoney":
//case "text":
//case "time":
//case "timestamp":
//case "uniqueidentifier":
default:
sb.AppendFormat("{0}()", dbTypeAsString.ToUpper());
break;
}
return sb.ToString();
}
public Project _project { get { return _executingGenerator.ProjectDefinition; } }
public string BuildConnectionString() {
var csCatalog = _project.GetAllPopulatedCatalogsFromMetaData(_executingGenerator.DriverID).First();
var csBuilder = new System.Data.SqlClient.SqlConnectionStringBuilder(_project.CreateConnectionStringForDatabase(_executingGenerator.DriverID));
var cs = string.Format("{0}:{1}@{2}/{3}", csBuilder.UserID, (string.IsNullOrEmpty(csBuilder.Password) ? "admin": csBuilder.Password), csBuilder.DataSource, csCatalog.CatalogName);
return cs;
}
The “models.py” output then looks like this:
#!/usr/bin/env python
# models.py
from sqlalchemy import *
from sqlalchemy.orm import scoped_session, sessionmaker, relation, relationship
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.dialects.mssql import *
Base = declarative_base()
def init_db(engine):
Base.metadata.create_all(bind=engine)
CategoryTable = Table(u'Categories', Base.metadata,
#column definitions
Column(u'CategoryID', INTEGER(), Sequence('seq_categories_categoryid'), primary_key=True, nullable=False),
Column(u'CategoryName', NVARCHAR(length=15), nullable=False),
Column(u'Description', UnicodeText(length=1073741823)),
Column(u'Picture', IMAGE()),
#foreign keys
#unique constraints
UniqueConstraint('CategoryName',name='CategoryName'),
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_categories
# __LLBLGENPRO_USER_CODE_REGION_END
)
CustomerTable = Table(u'Customers', Base.metadata,
#column definitions
Column(u'CustomerID', NCHAR(length=5), primary_key=True, nullable=False),
Column(u'CompanyName', NVARCHAR(length=40), nullable=False),
Column(u'ContactName', NVARCHAR(length=30)),
Column(u'ContactTitle', NVARCHAR(length=30)),
Column(u'Address', NVARCHAR(length=60)),
Column(u'City', NVARCHAR(length=15)),
Column(u'Region', NVARCHAR(length=15)),
Column(u'PostalCode', NVARCHAR(length=10)),
Column(u'Country', NVARCHAR(length=15)),
Column(u'Phone', NVARCHAR(length=24)),
Column(u'Fax', NVARCHAR(length=24)),
#foreign keys
#unique constraints
UniqueConstraint('CompanyName',name='CompanyName'),
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_customers
# __LLBLGENPRO_USER_CODE_REGION_END
)
CustomerCustomerDemoTable = Table(u'CustomerCustomerDemo', Base.metadata,
#column definitions
Column(u'CustomerID', NCHAR(length=5), primary_key=True, nullable=False),
Column(u'CustomerTypeID', NCHAR(length=10), primary_key=True, nullable=False),
#foreign keys
ForeignKeyConstraint(['CustomerTypeID'],['dbo.CustomerDemographics.CustomerTypeID'],name='FK_CustomerCustomerDemo'),
ForeignKeyConstraint(['CustomerID'],['dbo.Customers.CustomerID'],name='FK_CustomerCustomerDemo_Customers'),
#unique constraints
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_customercustomerdemo
# __LLBLGENPRO_USER_CODE_REGION_END
)
CustomerDemographicTable = Table(u'CustomerDemographics', Base.metadata,
#column definitions
Column(u'CustomerTypeID', NCHAR(length=10), primary_key=True, nullable=False),
Column(u'CustomerDesc', UnicodeText(length=1073741823)),
#foreign keys
#unique constraints
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_customerdemographics
# __LLBLGENPRO_USER_CODE_REGION_END
)
EmployeeTable = Table(u'Employees', Base.metadata,
#column definitions
Column(u'EmployeeID', INTEGER(), Sequence('seq_employees_employeeid'), primary_key=True, nullable=False),
Column(u'LastName', NVARCHAR(length=20), nullable=False),
Column(u'FirstName', NVARCHAR(length=10), nullable=False),
Column(u'Title', NVARCHAR(length=30)),
Column(u'TitleOfCourtesy', NVARCHAR(length=25)),
Column(u'BirthDate', DATETIME()),
Column(u'HireDate', DATETIME()),
Column(u'Address', NVARCHAR(length=60)),
Column(u'City', NVARCHAR(length=15)),
Column(u'Region', NVARCHAR(length=15)),
Column(u'PostalCode', NVARCHAR(length=10)),
Column(u'Country', NVARCHAR(length=15)),
Column(u'HomePhone', NVARCHAR(length=24)),
Column(u'Extension', NVARCHAR(length=4)),
Column(u'Photo', IMAGE()),
Column(u'Notes', UnicodeText(length=1073741823)),
Column(u'ReportsTo', INTEGER()),
Column(u'PhotoPath', NVARCHAR(length=255)),
#foreign keys
ForeignKeyConstraint(['ReportsTo'],['dbo.Employees.EmployeeID'],name='FK_Employees_Employees'),
#unique constraints
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_employees
# __LLBLGENPRO_USER_CODE_REGION_END
)
EmployeeTerritoryTable = Table(u'EmployeeTerritories', Base.metadata,
#column definitions
Column(u'EmployeeID', INTEGER(), primary_key=True, nullable=False),
Column(u'TerritoryID', NVARCHAR(length=20), primary_key=True, nullable=False),
#foreign keys
ForeignKeyConstraint(['EmployeeID'],['dbo.Employees.EmployeeID'],name='FK_EmployeeTerritories_Employees'),
ForeignKeyConstraint(['TerritoryID'],['dbo.Territories.TerritoryID'],name='FK_EmployeeTerritories_Territories'),
#unique constraints
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_employeeterritories
# __LLBLGENPRO_USER_CODE_REGION_END
)
OrderTable = Table(u'Orders', Base.metadata,
#column definitions
Column(u'OrderID', INTEGER(), Sequence('seq_orders_orderid'), primary_key=True, nullable=False),
Column(u'CustomerID', NCHAR(length=5)),
Column(u'EmployeeID', INTEGER()),
Column(u'OrderDate', DATETIME()),
Column(u'RequiredDate', DATETIME()),
Column(u'ShippedDate', DATETIME()),
Column(u'ShipVia', INTEGER()),
Column(u'Freight', MONEY()),
Column(u'ShipName', NVARCHAR(length=40)),
Column(u'ShipAddress', NVARCHAR(length=60)),
Column(u'ShipCity', NVARCHAR(length=15)),
Column(u'ShipRegion', NVARCHAR(length=15)),
Column(u'ShipPostalCode', NVARCHAR(length=10)),
Column(u'ShipCountry', NVARCHAR(length=15)),
#foreign keys
ForeignKeyConstraint(['CustomerID'],['dbo.Customers.CustomerID'],name='FK_Orders_Customers'),
ForeignKeyConstraint(['EmployeeID'],['dbo.Employees.EmployeeID'],name='FK_Orders_Employees'),
ForeignKeyConstraint(['ShipVia'],['dbo.Shippers.ShipperID'],name='FK_Orders_Shippers'),
#unique constraints
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_orders
# __LLBLGENPRO_USER_CODE_REGION_END
)
OrderDetailTable = Table(u'OrderDetails', Base.metadata,
#column definitions
Column(u'OrderID', INTEGER(), primary_key=True, nullable=False),
Column(u'ProductID', INTEGER(), primary_key=True, nullable=False),
Column(u'UnitPrice', MONEY(), nullable=False),
Column(u'Quantity', SMALLINT(), nullable=False),
Column(u'Discount', REAL(precision=24), nullable=False),
#foreign keys
ForeignKeyConstraint(['OrderID'],['dbo.Orders.OrderID'],name='FK_Order_Details_Orders'),
ForeignKeyConstraint(['ProductID'],['dbo.Products.ProductID'],name='FK_Order_Details_Products'),
#unique constraints
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_orderdetails
# __LLBLGENPRO_USER_CODE_REGION_END
)
ProductTable = Table(u'Products', Base.metadata,
#column definitions
Column(u'ProductID', INTEGER(), Sequence('seq_products_productid'), primary_key=True, nullable=False),
Column(u'ProductName', NVARCHAR(length=40), nullable=False),
Column(u'SupplierID', INTEGER()),
Column(u'CategoryID', INTEGER()),
Column(u'QuantityPerUnit', NVARCHAR(length=20)),
Column(u'UnitPrice', MONEY()),
Column(u'UnitsInStock', SMALLINT()),
Column(u'UnitsOnOrder', SMALLINT()),
Column(u'ReorderLevel', SMALLINT()),
Column(u'Discontinued', BIT(), nullable=False),
#foreign keys
ForeignKeyConstraint(['CategoryID'],['dbo.Categories.CategoryID'],name='FK_Products_Categories'),
ForeignKeyConstraint(['SupplierID'],['dbo.Suppliers.SupplierID'],name='FK_Products_Suppliers'),
#unique constraints
UniqueConstraint('ProductName',name='ProductName'),
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_products
# __LLBLGENPRO_USER_CODE_REGION_END
)
RegionTable = Table(u'Region', Base.metadata,
#column definitions
Column(u'RegionID', INTEGER(), primary_key=True, nullable=False),
Column(u'RegionDescription', NCHAR(length=50), nullable=False),
#foreign keys
#unique constraints
UniqueConstraint('RegionDescription',name='IX_Region'),
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_region
# __LLBLGENPRO_USER_CODE_REGION_END
)
ShipperTable = Table(u'Shippers', Base.metadata,
#column definitions
Column(u'ShipperID', INTEGER(), Sequence('seq_shippers_shipperid'), primary_key=True, nullable=False),
Column(u'CompanyName', NVARCHAR(length=40), nullable=False),
Column(u'Phone', NVARCHAR(length=24)),
#foreign keys
#unique constraints
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_shippers
# __LLBLGENPRO_USER_CODE_REGION_END
)
SupplierTable = Table(u'Suppliers', Base.metadata,
#column definitions
Column(u'SupplierID', INTEGER(), Sequence('seq_suppliers_supplierid'), primary_key=True, nullable=False),
Column(u'CompanyName', NVARCHAR(length=40), nullable=False),
Column(u'ContactName', NVARCHAR(length=30)),
Column(u'ContactTitle', NVARCHAR(length=30)),
Column(u'Address', NVARCHAR(length=60)),
Column(u'City', NVARCHAR(length=15)),
Column(u'Region', NVARCHAR(length=15)),
Column(u'PostalCode', NVARCHAR(length=10)),
Column(u'Country', NVARCHAR(length=15)),
Column(u'Phone', NVARCHAR(length=24)),
Column(u'Fax', NVARCHAR(length=24)),
Column(u'HomePage', UnicodeText(length=1073741823)),
#foreign keys
#unique constraints
UniqueConstraint('CompanyName',name='SupplierName'),
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_suppliers
# __LLBLGENPRO_USER_CODE_REGION_END
)
TerritoryTable = Table(u'Territories', Base.metadata,
#column definitions
Column(u'TerritoryID', NVARCHAR(length=20), primary_key=True, nullable=False),
Column(u'TerritoryDescription', NCHAR(length=50), nullable=False),
Column(u'RegionID', INTEGER(), nullable=False),
#foreign keys
ForeignKeyConstraint(['RegionID'],['dbo.Region.RegionID'],name='FK_Territories_Region'),
#unique constraints
#schema
schema="dbo"
# __LLBLGENPRO_USER_CODE_REGION_START additional_table_information_territories
# __LLBLGENPRO_USER_CODE_REGION_END
)
class Category(Base):
__table__ = CategoryTable
#relation definitions: one to many
#see backref mapping on Product class –> Products = relationship('Product', backref='Category')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_category
# __LLBLGENPRO_USER_CODE_REGION_END
class Customer(Base):
__table__ = CustomerTable
#relation definitions: one to many
#see backref mapping on CustomerCustomerDemo class –> CustomerCustomerDemos = relationship('CustomerCustomerDemo', backref='Customer')
#see backref mapping on Order class –> Orders = relationship('Order', backref='Customer')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_customer
# __LLBLGENPRO_USER_CODE_REGION_END
class CustomerCustomerDemo(Base):
__table__ = CustomerCustomerDemoTable
#relation definitions: many to one with backref (also takes care of one to many)
Customer = relationship('Customer', backref='CustomerCustomerDemos')
CustomerDemographic = relationship('CustomerDemographic', backref='CustomerCustomerDemos')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_customercustomerdemo
# __LLBLGENPRO_USER_CODE_REGION_END
class CustomerDemographic(Base):
__table__ = CustomerDemographicTable
#relation definitions: one to many
#see backref mapping on CustomerCustomerDemo class –> CustomerCustomerDemos = relationship('CustomerCustomerDemo', backref='CustomerDemographic')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_customerdemographic
# __LLBLGENPRO_USER_CODE_REGION_END
class Employee(Base):
__table__ = EmployeeTable
#relation definitions: one to many
#see backref mapping on Employee class –> Employees = relationship('Employee', backref='Manager')
#see backref mapping on EmployeeTerritory class –> EmployeeTerritories = relationship('EmployeeTerritory', backref='Employee')
#see backref mapping on Order class –> Orders = relationship('Order', backref='Employee')
#relation definitions: many to many with backref
Territories = relationship('Territory', secondary=EmployeeTerritoryTable, backref='Employees')
#relation definitions: many to one with backref (also takes care of one to many)
Manager = relationship('Employee', primaryjoin=(EmployeeTable.c.EmployeeID==EmployeeTable.c.ReportsTo), remote_side='Employee.EmployeeID', backref='Employees')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_employee
# __LLBLGENPRO_USER_CODE_REGION_END
class EmployeeTerritory(Base):
__table__ = EmployeeTerritoryTable
#relation definitions: many to one with backref (also takes care of one to many)
Employee = relationship('Employee', backref='EmployeeTerritories')
Territory = relationship('Territory', backref='EmployeeTerritories')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_employeeterritory
# __LLBLGENPRO_USER_CODE_REGION_END
class Order(Base):
__table__ = OrderTable
#relation definitions: one to many
#see backref mapping on OrderDetail class –> OrderDetails = relationship('OrderDetail', backref='Order')
#relation definitions: many to many with backref
ProductCollectionViaOrderDetail = relationship('OrderDetail', backref='OrderCollectionViaOrderDetail')
#relation definitions: many to one with backref (also takes care of one to many)
Customer = relationship('Customer', backref='Orders')
Employee = relationship('Employee', backref='Orders')
Shipper = relationship('Shipper', backref='Orders')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_order
# __LLBLGENPRO_USER_CODE_REGION_END
class OrderDetail(Base):
__table__ = OrderDetailTable
#relation definitions: many to one with backref (also takes care of one to many)
Order = relationship('Order', backref='OrderDetails')
Product = relationship('Product', backref='OrderDetails')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_orderdetail
# __LLBLGENPRO_USER_CODE_REGION_END
class Product(Base):
__table__ = ProductTable
#relation definitions: one to many
#see backref mapping on OrderDetail class –> OrderDetails = relationship('OrderDetail', backref='Product')
#relation definitions: many to many with backref
#see backref mapping on Order class –> OrderCollectionViaOrderDetail = relationship('OrderDetail', backref='ProductCollectionViaOrderDetail')
#relation definitions: many to one with backref (also takes care of one to many)
Category = relationship('Category', backref='Products')
Supplier = relationship('Supplier', backref='Products')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_product
# __LLBLGENPRO_USER_CODE_REGION_END
class Region(Base):
__table__ = RegionTable
#relation definitions: one to many
#see backref mapping on Territory class –> Territories = relationship('Territory', backref='Region')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_region
# __LLBLGENPRO_USER_CODE_REGION_END
class Shipper(Base):
__table__ = ShipperTable
#relation definitions: one to many
#see backref mapping on Order class –> Orders = relationship('Order', backref='Shipper')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_shipper
# __LLBLGENPRO_USER_CODE_REGION_END
class Supplier(Base):
__table__ = SupplierTable
#relation definitions: one to many
#see backref mapping on Product class –> Products = relationship('Product', backref='Supplier')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_supplier
# __LLBLGENPRO_USER_CODE_REGION_END
class Territory(Base):
__table__ = TerritoryTable
#relation definitions: one to many
#see backref mapping on EmployeeTerritory class –> EmployeeTerritories = relationship('EmployeeTerritory', backref='Territory')
#relation definitions: many to many with backref
#see backref mapping on Employee class –> Employees = relationship('Employee', secondary=EmployeeTerritoryTable, backref='Territories')
#relation definitions: many to one with backref (also takes care of one to many)
Region = relationship('Region', backref='Territories')
# __LLBLGENPRO_USER_CODE_REGION_START additional_model_information_territory
# __LLBLGENPRO_USER_CODE_REGION_END
def main():
engine = create_engine('mssql+pyodbc://admin:admin@localhost/Northwind', convert_unicode=True, echo=True)
init_db(engine)
db = scoped_session(sessionmaker(bind=engine))
#try a count on each table to make sure it's setup properly
counts = [
str(db.query(Category).count()) + " Category records",
str(db.query(Customer).count()) + " Customer records",
str(db.query(CustomerCustomerDemo).count()) + " CustomerCustomerDemo records",
str(db.query(CustomerDemographic).count()) + " CustomerDemographic records",
str(db.query(Employee).count()) + " Employee records",
str(db.query(EmployeeTerritory).count()) + " EmployeeTerritory records",
str(db.query(Order).count()) + " Order records",
str(db.query(OrderDetail).count()) + " OrderDetail records",
str(db.query(Product).count()) + " Product records",
str(db.query(Region).count()) + " Region records",
str(db.query(Shipper).count()) + " Shipper records",
str(db.query(Supplier).count()) + " Supplier records",
str(db.query(Territory).count()) + " Territory records",
]
for c in counts:
print(c)
if __name__ == '__main__':
main()
To do a basic check and verify that the SqlAlchemy model file doesn’t have any errors in it, a simple count is applied against all entity classes/tables, by running “python models.py”.
First against the modelssqlite.py file, which is for the SQLite dialect:
Step 2 – The Tornado Web Server infrastructure
The way I built out the web server infrastructure was to first build the HTML / CSS for a single entity (the Customer entity), then build the LLBLGen templates to generate the whole application. In the end, here is a picture of the set of templates used to build out the application.- Used Tornado UI modules (similar to Razor views in ASP.NET MVC, or UserControls in ASP.NET Forms) for control blocks
- Created model binder classes for each entity derived from a Base class, with each model binder being knowledgeable of how to bind the model to the request
- Created a very generic handlers.py file to dispatch requests to the appropriate view templates
- Used CSS to generate the image PNG files
- Used jQuery and jQuery validation for client-side validation
- Created a detailed error page that enabled me to parse errors quickly to get to the root-cause of issues as I faced them
Step 3 – Testing and Playing with the Application in a Browser
To run the application, it’s simply “python app.py” at the command line. And here are some screenshots of the application after it’s code-generated.Custom error page
Home Page
Listing Records (with sorting, and paging)
Adding a Record (with client-side validation)
Editing a Record (with some jQuery UI)
Deleting a Record
Detailed search
View Record
Step 4 – In Conclusion
Building this little application was a lot of fun. Best of all, there are lots of opportunities for this to grow into something much bigger. Here are some things I take away from this experience:- Python is awesome (of course)!
- The LLBLGen tool continues to pay major dividends, and this is proof that it isn’t only just for the .NET folks, and for .NET projects
- SqlAlchemy is a rock-solid ORM, and I clearly only tapped the surface of it in developing this app
- The simplicity of the Tornado Web Server / Framework was a real delight to work with, and I can see myself using it for many projects in the future

Awesome stuff! 😀