SZN Database Access Object Generator for MySQL. Generates Models and DataManagers from existing MySQL database structure.
Project description
SZN DAOGen
Database Access Object Generator by Seznam.cz
Is a simple utility which enables you to generate Models and Data Managers for your project based on MySQL database to access data easily.
Description
DAOGenerator enables you to generate Model
s and DataManager
s from existing
database structure. For each database table and view the python Model
will be generated.
Each model will contain all table attributes with its datatypes and other meta
information. For each Model
one base DataManager
for database access will be generated
too. For each base DataManager
one DataManager
will be generated if it does not
exist from previous szndaogen
run. If DataManager
exists file generation of it
will be skiped.
DataManager
is designed for future custom functionality extending. Model
s
and base DataManager
s should not be edited it will be overwiten by next
szndaogen
script execution.
Advantages
- changes are needed only on database side
- you can stop beeing a coding monkey to write python models
- autogenerated table meta information
- works with Tables and Views
- View performance issue solved
- it is not nessesary to have view defined on production database
- Models and DataManages templates are written in Jinja2
- PyCharm code completion works perfectly with generated Models and DataManagers
- fast and easy change of all Models and DataManager if DB structure changed (it takes less than second)
Disadvantages
- you can feel shamed how easy to use it could be
Usage
szndaogen --help
Usage of SZN DAOGen v2.3.0 - Database Access Object Generator by Seznam.cz
szndaogen [options] output_path
example: szndaogen -a localhost -d my_database -u root -p pass /path/to/data_access
example: szndaogen -a localhost -d my_database -u root -p pass ./data_access
Options:
-h, --help show this help message and exit
-a DB_HOST, --host-address=DB_HOST
MySQL database host. (required)
-r DB_PORT, --port=DB_PORT
MySQL database port.
-d DB_NAME, --database=DB_NAME
MySQL database name. (required)
-u DB_USER, --user=DB_USER
User name for MySQL DB authentication. (required)
-p DB_PASS, --password=DB_PASS
Password for MySQL DB authentication.
-t TEMPLATES_PATH, --templates-path=TEMPLATES_PATH
Path to custom templates of Models (model.jinja),
DataManagers (manager.jinja) and DataManagerBases
(manager_base.jinja).
Installation
python3 setup.py install
or
pip3 install szndaogen
Example
For our example we will use sample database from MySQLTutorial.org. Zipped database file is here.
Importing database:
mysql < mysqlsampledatabase.sql
Create custom application directory or use example
directory from our repository.
Lets generate Models and DataManagers from imported database by typing command:
szndaogen
It will run simple szndaogen wizzard (if no arguments passed):
Required parameters are not satisfied. Would you like to run setup wizard? [Y/n] y
MySQL host address: localhost
MySQL port (default 3306): 3306
MySQL database name: classicmodels
MySQL username: mysql-user
MySQL password: mysql-user-password
Output path where all models and managers will be generated (default "./data_access"): ./example_dao
Before you proceed, would you like to save this configuration as a bash script in CWD for future use? [Y/n] y
Shortcut script 'szndaogen-localhost-classicmodels.sh' created in current working directory.
Writing model `Customers` into `example_dao/models/customers_model.py`
Writing manager `Customers` into `example_dao/managers/customers_manager.py`
Writing manager `Customers` into `example_dao/managers/base/customers_manager_base.py`
Writing model `Employees` into `example_dao/models/employees_model.py`
Writing manager `Employees` into `example_dao/managers/employees_manager.py`
Writing manager `Employees` into `example_dao/managers/base/employees_manager_base.py`
Writing model `Offices` into `example_dao/models/offices_model.py`
Writing manager `Offices` into `example_dao/managers/offices_manager.py`
Writing manager `Offices` into `example_dao/managers/base/offices_manager_base.py`
Writing model `Orderdetails` into `example_dao/models/orderdetails_model.py`
Writing manager `Orderdetails` into `example_dao/managers/orderdetails_manager.py`
Writing manager `Orderdetails` into `example_dao/managers/base/orderdetails_manager_base.py`
Writing model `Orders` into `example_dao/models/orders_model.py`
Writing manager `Orders` into `example_dao/managers/orders_manager.py`
Writing manager `Orders` into `example_dao/managers/base/orders_manager_base.py`
Writing model `Payments` into `example_dao/models/payments_model.py`
Writing manager `Payments` into `example_dao/managers/payments_manager.py`
Writing manager `Payments` into `example_dao/managers/base/payments_manager_base.py`
Writing model `Productlines` into `example_dao/models/productlines_model.py`
Writing manager `Productlines` into `example_dao/managers/productlines_manager.py`
Writing manager `Productlines` into `example_dao/managers/base/productlines_manager_base.py`
Writing model `Products` into `example_dao/models/products_model.py`
Writing manager `Products` into `example_dao/managers/products_manager.py`
Writing manager `Products` into `example_dao/managers/base/products_manager_base.py`
Now your project contains file structure as you can see bellow:
tree .
├── szndaogen-localhost-classicmodels.sh
├── example_dao
│ ├── __init__.py
│ ├── managers
│ │ ├── base
│ │ │ ├── __init__.py
│ │ │ ├── customers_manager_base.py
│ │ │ ├── employees_manager_base.py
│ │ │ ├── offices_manager_base.py
│ │ │ ├── orderdetails_manager_base.py
│ │ │ ├── orders_manager_base.py
│ │ │ ├── payments_manager_base.py
│ │ │ ├── productlines_manager_base.py
│ │ │ └── products_manager_base.py
│ │ ├── __init__.py
│ │ ├── customers_manager.py
│ │ ├── employees_manager.py
│ │ ├── offices_manager.py
│ │ ├── orderdetails_manager.py
│ │ ├── orders_manager.py
│ │ ├── payments_manager.py
│ │ ├── productlines_manager.py
│ │ └── products_manager.py
│ └── models
│ ├── __init__.py
│ ├── customers_model.py
│ ├── employees_model.py
│ ├── offices_model.py
│ ├── orderdetails_model.py
│ ├── orders_model.py
│ ├── payments_model.py
│ ├── productlines_model.py
│ └── products_model.py
└── requirements.txt
Example of auto-generated Model class:
# !!! DO NOT MODIFY !!!
# Automatically generated Model class
# Generated by "szndaogen" tool
import typing
from szndaogen.data_access.model_base import ModelBase
class EmployeesModel(ModelBase):
class Meta:
TABLE_NAME: str = "employees"
TABLE_TYPE: str = "BASE TABLE"
# fmt: off
SQL_STATEMENT: str = "SELECT {PROJECTION} FROM `employees` {WHERE} {ORDER_BY} {LIMIT} {OFFSET}"
# fmt: on
SQL_STATEMENT_WHERE_BASE: str = "1"
SQL_STATEMENT_ORDER_BY_DEFAULT: str = ""
PRIMARY_KEYS: typing.List = ["employeeNumber", ]
ATTRIBUTE_LIST: typing.List = ["employeeNumber", "lastName", "firstName", "extension", "email", "officeCode", "reportsTo", "jobTitle", ]
ATTRIBUTE_TYPES: typing.Dict = {
"employeeNumber": int,
"lastName": str,
"firstName": str,
"extension": str,
"email": str,
"officeCode": str,
"reportsTo": int,
"jobTitle": str,
}
MODEL_DATA_CONVERTOR: typing.Dict = {
}
# Class attribute to table attribute name conversion
employeeNumber: str = "employeeNumber"
lastName: str = "lastName"
firstName: str = "firstName"
extension: str = "extension"
email: str = "email"
officeCode: str = "officeCode"
reportsTo: str = "reportsTo"
jobTitle: str = "jobTitle"
def __init__(self, init_data: typing.Dict = {}):
self.employeeNumber: int = None
"""Type: int(11), Can be NULL: NO, Key: PRI"""
self.lastName: str = None
"""Type: varchar(50), Can be NULL: NO"""
self.firstName: str = None
"""Type: varchar(50), Can be NULL: NO"""
self.extension: str = None
"""Type: varchar(10), Can be NULL: NO"""
self.email: str = None
"""Type: varchar(100), Can be NULL: NO"""
self.officeCode: str = None
"""Type: varchar(10), Can be NULL: NO, Key: MUL"""
self.reportsTo: int = None
"""Type: int(11), Can be NULL: YES, Key: MUL"""
self.jobTitle: str = None
"""Type: varchar(50), Can be NULL: NO"""
super().__init__(init_data)
Example of auto-generated Base Manager class:
# !!! DO NOT MODIFY !!!
# Automatically generated Base Manager class
# Generated by "szndaogen" tool
import typing
from szndaogen.data_access.manager_base import TableManagerBase
from ...models.employees_model import EmployeesModel
class EmployeesManagerBase(TableManagerBase):
MODEL_CLASS = EmployeesModel
@classmethod
def create_model_instance(cls, init_data: typing.Dict = None) -> EmployeesModel:
if init_data is None:
init_data = {}
return super().create_model_instance(init_data)
def select_one(self, employeeNumber: int, condition: str = "1", condition_params: typing.Tuple = (), projection: typing.Tuple = (), order_by: typing.Tuple = ()) -> EmployeesModel:
return super().select_one(employeeNumber, condition=condition, condition_params=condition_params, projection=projection, order_by=order_by)
def select_all(self, condition: str = "1", condition_params: typing.Tuple = (), projection: typing.Tuple = (), order_by: typing.Tuple = (), limit: int = 0, offset: int = 0) -> typing.List[EmployeesModel]:
return super().select_all(condition=condition, condition_params=condition_params, projection=projection, order_by=order_by, limit=limit, offset=offset)
Example of auto-generated Manager class which could be modified/extended by your own functionality:
# This file can be modified. If file exists it wont be replaced by "szndaogen" any more.
# Automatically generated Manager class
# Generated by "szndaogen" tool
from .base.employees_manager_base import EmployeesManagerBase
class EmployeesManager(EmployeesManagerBase):
pass
Example application
Now you can write application like this:
from example_dao.managers.employees_manager import EmployeesManager
from szndaogen.config import Config
from szndaogen.data_access.db import DBI
from szndaogen.tools.log import Logger, StdOutLogger
@DBI.transaction("dbi")
def update_employee_first_name(employee_id: int, new_first_name: str, dbi: DBI = None) -> int:
manager = EmployeesManager(dbi=dbi) # tell manager to work with passed DBI instance to keep transaction connection
model_instance = manager.select_one(employee_id)
model_instance.firstName = new_first_name
return manager.update_one(model_instance)
if __name__ == '__main__':
Config.MANAGER_AUTO_MAP_MODEL_ATTRIBUTES = True # if disabled, you can control attributes mapping by `map_model_attributes()` method on model instance if needed to better performance
Config.MYSQL_HOST = "localhost"
Config.MYSQL_DATABASE = "classicmodels"
Config.MYSQL_USER = "root"
Config.MYSQL_PASSWORD = ""
Logger.set_external_logger(logger_instance=StdOutLogger())
employee_manager = EmployeesManager()
employee_result = employee_manager.select_all(order_by=(f"{EmployeesManager.MODEL_CLASS.Meta.employeeNumber} ASC",))
for employee_model_instance in employee_result:
print(f"{employee_model_instance.firstName} {employee_model_instance.lastName} - {employee_model_instance.employeeNumber}")
# autocommit update
employee_result = employee_manager.select_all("lastName=%s", ("Thompson",))
if len(employee_result) == 1:
employee_model_instance = employee_result[0]
print(f"Trying to update record id: {employee_model_instance.employeeNumber} - {employee_model_instance.firstName} {employee_model_instance.lastName}")
employee_model_instance.firstName = "New Leslie"
employee_manager.update_one(employee_model_instance)
employee_result = employee_manager.select_all("lastName=%s", ("Thompson",))
employee_model_instance = employee_result[0]
print(f"Updated record id: {employee_model_instance.employeeNumber} - {employee_model_instance.firstName} {employee_model_instance.lastName}")
# transaction update
update_employee_first_name(1166, "Leslie forever")
# new item
new_employee = employee_manager.create_model_instance()
new_employee.employeeNumber = 9999
new_employee.firstName = "John"
new_employee.lastName = "Doe"
new_employee.extension = "xxx"
new_employee.email = "a@b.c"
new_employee.officeCode = 4
new_employee.jobTitle = "Incognito"
employee_manager.insert_one(new_employee)
# delete item
employee_manager.delete_one(new_employee)
# OR
employee_manager.delete_all(f"{EmployeesManager.MODEL_CLASS.Meta.employeeNumber}=%s", (9999,))
# OR simply
employee_manager.delete_all("employeeNumber=%s", (9999,))
Working with Views
szndaogen
could process defined complicated database views too. There is no performance issue with MySQL views. Because view is parsed by szndaogen
analyser and stored into Model
definition. View declaration is executed on python application side. So all indexes and database optimalisations are used. Lets define sample view for out application defined as bellow select
with a few joins:
SELECT o.`orderNumber`, od.`productCode`, od.`quantityOrdered`, p.`productName`, p.`quantityInStock`,
IF(p.`quantityInStock` > od.`quantityOrdered`, "enough", "to_stock_needed") AS productInStockStatus
FROM orders AS o
LEFT JOIN orderdetails AS od ON od.`orderNumber`=o.`orderNumber`
LEFT JOIN products AS p ON p.`productName`
WHERE o.`status`="In Process"
create view view_orders_to_be_processed
from it:
DELIMITER $$
CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `view_orders_to_be_processed` AS (
SELECT
`o`.`orderNumber` AS `orderNumber`,
`od`.`productCode` AS `productCode`,
`od`.`quantityOrdered` AS `quantityOrdered`,
`p`.`productName` AS `productName`,
`p`.`quantityInStock` AS `quantityInStock`,
IF((`p`.`quantityInStock` > `od`.`quantityOrdered`),'enough','to_stock_needed') AS `productInStockStatus`
FROM ((`orders` `o`
LEFT JOIN `orderdetails` `od`
ON ((`od`.`orderNumber` = `o`.`orderNumber`)))
LEFT JOIN `products` `p`
ON (`p`.`productName`))
WHERE (`o`.`status` = 'In Process'))$$
DELIMITER ;
run szndaogen
again by stored bash script:
szndaogen-localhost-classicmodels.sh
Writing model `Customers` into `example_dao/models/customers_model.py`
Skipping manager `Customers` exists `example_dao/managers/customers_manager.py`
Writing manager `Customers` into `example_dao/managers/base/customers_manager_base.py`
Writing model `Employees` into `example_dao/models/employees_model.py`
Skipping manager `Employees` exists `example_dao/managers/employees_manager.py`
Writing manager `Employees` into `example_dao/managers/base/employees_manager_base.py`
Writing model `Offices` into `example_dao/models/offices_model.py`
Skipping manager `Offices` exists `example_dao/managers/offices_manager.py`
Writing manager `Offices` into `example_dao/managers/base/offices_manager_base.py`
Writing model `Orderdetails` into `example_dao/models/orderdetails_model.py`
Skipping manager `Orderdetails` exists `example_dao/managers/orderdetails_manager.py`
Writing manager `Orderdetails` into `example_dao/managers/base/orderdetails_manager_base.py`
Writing model `Orders` into `example_dao/models/orders_model.py`
Skipping manager `Orders` exists `example_dao/managers/orders_manager.py`
Writing manager `Orders` into `example_dao/managers/base/orders_manager_base.py`
Writing model `Payments` into `example_dao/models/payments_model.py`
Skipping manager `Payments` exists `example_dao/managers/payments_manager.py`
Writing manager `Payments` into `example_dao/managers/base/payments_manager_base.py`
Writing model `Productlines` into `example_dao/models/productlines_model.py`
Skipping manager `Productlines` exists `example_dao/managers/productlines_manager.py`
Writing manager `Productlines` into `example_dao/managers/base/productlines_manager_base.py`
Writing model `Products` into `example_dao/models/products_model.py`
Skipping manager `Products` exists `example_dao/managers/products_manager.py`
Writing manager `Products` into `example_dao/managers/base/products_manager_base.py`
Writing model `ViewOrdersToBeProcessed` into `example_dao/models/vieworderstobeprocessed_model.py`
Writing manager `ViewOrdersToBeProcessed` into `example_dao/managers/vieworderstobeprocessed_manager.py`
Writing manager `ViewOrdersToBeProcessed` into `example_dao/managers/base/vieworderstobeprocessed_manager_base.py`
and all work is done in a second. Now you can use ViewOrdersToBeProcessedManager
with custom WHERE
conditions, ORDER BY
sorts, limits, etc.
from example_dao.managers.view_orders_to_be_processed_manager import ViewOrdersToBeProcessedManager
manager = ViewOrdersToBeProcessedManager()
results = manager.select_all(order_by=("`od`.`quantityOrdered` DESC",), limit=10)
print("Top 10 ordered quauntities waiting for processing")
for item in results:
print(f"{item.orderNumber} - {item.productCode}: {item.productName}, {item.quantityOrdered}/{item.quantityInStock}")
Grouping tools
Packageszndaogen
also comes with a set of helpful auto grouping tools placed in szndaogen/tools/auto_group.py
.
These tools solve a problem where you have M:N relations between two entities and need to join them together. Their functionality is also helpful when working with SQL views.
SELECT name, surname, job.name as job___name
FROM person
JOIN
works ON works.person_id = person.id
job ON job.id = works.job_id
WHERE
person.id = 2;
For a single person, who has two jobs, DB might output something like this:
name | surname | job___name |
---|---|---|
Jane | Doe | Accountant |
Jane | Doe | Developer |
Which might be OK when fetching only one person and his jobs. There are use cases, when you need to fetch more and more people. Output will be much larger. This is the place, where auto grouping tools come handy.
auto_group_dict
This function groups dict keys with same prefix under one dict key. Groups used as group keys are identified by group separator ___
.
person = {
"name": "Jane",
"surname": "Doe",
"job___name": "Accountant",
"job___established": 2001
}
ret = auto_group_dict(person)
# Returns in
ret = {
"name": "Jane",
"surname": "Doe",
"job": {
"name": "Accountant",
"established": 2001,
}
}
auto_group_list
IMPORTANT: All items which are inside lists are sorted exactly the same as they came from the DB.
Let's say that we have want to retrieve a new person from our DB. Jane Doe now has two jobs: an accountant and a developer.
Database returns two rows as specified above. But in object oriented world, it would be better for us to have it in one dict. This is where auto_group_list
comes handy.
SELECT person.name, person.surname, job.name as jobs__name
FROM person
JOIN
works ON works.person_id = person.id
job ON job.id = works.job_id
WHERE
person.id = 2;
Assuming our SQL query returns 2 rows like this:
rows = [
{
"name": "Jane",
"surname": "Doe",
"jobs__name": "Accountant",
},
{
"name": "Jane",
"surname": "Doe",
"jobs__name": "Developer",
}
]
ret = auto_group_list(rows)
# Returns in
ret = {
"name": "Jane",
"surname": "Doe",
"jobs": [
{
"name": "Accountant"
},
{
"name": "Developer"
}
]
}
This is kind of handy, isn't it? But what if we want to omit our WHERE statement? This is where auto_group_list_by_pkeys
comes in place.
auto_group_list_by_pkeys
Next and the last useful is handy when you want to for example fetch multiple people from DB, keep m..n relations and have everything grouped nicely. Like so:
SELECT person.id as _id, person.name, person.surname, job.name as jobs__name
FROM person
JOIN
works ON works.person_id = person.id
job ON job.id = works.job_id
WHERE
person.id IN (2, 3);
Our person no. 2 is Jane Doe, who works as an accountant and a developer. Person no. 3 is John Doe, works as an DevOps Engineer and a developer.
Let's say our grouping key is _id
.
Our fetched data converted to python might look something like this:
rows = [
{
"_id": 2,
"name": "Jane",
"surname": "Doe",
"jobs__name": "Accountant"
},
{
"_id": 2,
"name": "Jane",
"surname": "Doe",
"jobs__name": "Developer"
},
{
"_id": 3,
"name": "John",
"surname": "Doe",
"jobs__name": "DevOps Engineer"
},
{
"_id": 3,
"name": "John",
"surname": "Doe",
"jobs__name": "Developer"
}
]
Let's make it prettier!
ret = auto_group_list_by_pkeys(("_id",), rows, use_auto_group_dict=True)
# Returns dict with 2 items, grouped by key "_id"
ret = {
"2": {
"_id": 2,
"name": "Jane",
"surname": "Doe",
"jobs": [
{
"name": "Accountant"
},
{
"name": "Developer"
}
]
},
"3": {
"_id": 3,
"name": "John",
"surname": "Doe",
"jobs": [
{
"name": "DevOps Engineer"
},
{
"name": "Developer"
}
]
}
}
Now we have all our cases covered, ready to go.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file szndaogen-2.4.3.tar.gz
.
File metadata
- Download URL: szndaogen-2.4.3.tar.gz
- Upload date:
- Size: 30.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.3.0 pkginfo/1.7.0 requests/2.25.1 setuptools/51.1.0.post20201221 requests-toolbelt/0.9.1 tqdm/4.56.0 CPython/3.8.10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | ae5926eb96cc948a3f6a387d75d5aee5d9c4caf19cd74cb070187aab2c16e8e7 |
|
MD5 | 7b7699f4e76d5ea0787cb44a30b10cdb |
|
BLAKE2b-256 | 9e5cd3ebd53d5b8f38297e2acc28f3bf09658cae447dfa637853bccbddc1a0e7 |
File details
Details for the file szndaogen-2.4.3-py3-none-any.whl
.
File metadata
- Download URL: szndaogen-2.4.3-py3-none-any.whl
- Upload date:
- Size: 28.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.3.0 pkginfo/1.7.0 requests/2.25.1 setuptools/51.1.0.post20201221 requests-toolbelt/0.9.1 tqdm/4.56.0 CPython/3.8.10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8fee43c4f9c041f1e59bac3abb8d2e3e4f25ab57768d88b7909fdd36401e2ca4 |
|
MD5 | 0f0d595ccea9c98e02061097409779b4 |
|
BLAKE2b-256 | 5e60ffeccbcf040d20f53e67f79c48ada313578f409ef827af098d5ab9726c38 |