feat: add MySQL database support alongside SQL Server

This commit implements multi-database support, allowing the system to switch
between SQL Server and MySQL databases seamlessly.

## New Features
- Database type selection (SQL Server or MySQL) via configuration
- Automatic table name conversion between formats ([dbo].[table] → dbo_table)
- Automatic parameter placeholder handling (? for SQL Server, %s for MySQL)
- GUI settings tab now includes database type dropdown and MySQL configuration

## Database Abstraction Layer
- db/base_connection.py: Abstract base class for database connections
- db/sqlserver_connection.py: SQL Server implementation
- db/mysql_connection.py: MySQL implementation using mysql-connector-python
- db/connection_factory.py: Factory pattern for creating connections
- db/table_name_converter.py: Table name format conversion utility

## DAO Base Class
- db/base_dao.py: Base DAO with helper methods for SQL conversion and placeholders

## Updated Components
- config/schema.py: Extended with DatabaseType enum and MySQL/SQLServer config classes
- config/defaults.py: Added MySQL default configuration
- config/loader.py: Updated to handle new database structure
- db/connection.py: Refactored to use factory pattern and load user config
- All DAO files: Updated to inherit from BaseDAO with automatic conversion

## Dependencies
- Added mysql-connector-python>=8.0.0 to requirements.txt

## Configuration
To use MySQL, set db_type to "mysql" in config/user_settings.json:
{
  "database": {
    "db_type": "mysql",
    "mysql": {
      "host": "192.168.31.83",
      "port": 3306,
      "database": "BLD_DB",
      "username": "remote_user",
      "password": "3.1415926Beeke"
    }
  }
}

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-02-09 21:50:54 +08:00
parent 04b99292ad
commit caac411e17
17 changed files with 1661 additions and 513 deletions

View File

@@ -2,37 +2,20 @@
Data Access Object for DiscreteMaterialPlanData table.
This module provides CRUD operations for persisting discrete material plan
data to SQL Server database. It handles mapping between Chinese DataFrame
data to SQL Server/MySQL database. It handles mapping between Chinese DataFrame
columns (from ExcelConverter) and English database columns.
"""
from db.base_dao import BaseDAO
from db.connection import get_connection
from typing import List, Dict, Any
import pandas as pd
from config.schema import DatabaseType
class DiscreteMaterialPlanDAO:
class DiscreteMaterialPlanDAO(BaseDAO):
"""Data Access Object for DiscreteMaterialPlanData table"""
def __init__(self):
self.db = None
def __enter__(self):
"""Enter context manager and establish database connection"""
self.db = get_connection()
self.db.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit context manager and close database connection"""
if self.db:
self.db.disconnect()
def close(self):
"""Close database connection"""
if self.db:
self.db.disconnect()
def save_dataframe_with_replace(self, df: pd.DataFrame) -> Dict[str, int]:
"""
Save DataFrame using REPLACE strategy (DELETE + INSERT).
@@ -97,8 +80,13 @@ class DiscreteMaterialPlanDAO:
for i in range(0, len(plan_numbers), batch_size):
batch = plan_numbers[i:i + batch_size]
placeholders = ','.join(['?' for _ in batch])
sql = f"DELETE FROM DiscreteMaterialPlanData WHERE PlanNumber IN ({placeholders})"
placeholder = self._get_placeholder()
placeholders = ','.join([placeholder for _ in batch])
# 根据数据库类型选择表名
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]')
sql = f"DELETE FROM {table_name} WHERE PlanNumber IN ({placeholders})"
deleted = db.execute_update(sql, tuple(batch))
total_deleted += deleted
@@ -119,15 +107,19 @@ class DiscreteMaterialPlanDAO:
Returns:
Total number of records inserted
"""
sql = """
INSERT INTO DiscreteMaterialPlanData (
# 根据数据库类型选择表名
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]')
placeholder = self._get_placeholder()
sql = f"""
INSERT INTO {table_name} (
Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType,
ProductCode, ProductName, ProductUnit, ProductPlanQuantity,
UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate,
SequenceNumber, MaterialCode, MaterialName, Specification, Model,
DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate,
Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
) VALUES ({self._build_placeholders(28)})
"""
total_inserted = 0
@@ -217,7 +209,9 @@ class DiscreteMaterialPlanDAO:
List of dictionaries representing records
"""
with get_connection() as db:
sql = "SELECT * FROM DiscreteMaterialPlanData WHERE PlanNumber = ?"
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]')
placeholder = self._get_placeholder()
sql = f"SELECT * FROM {table_name} WHERE PlanNumber = {placeholder}"
return db.execute_query(sql, (plan_number,))
def query_by_plan_numbers(self, plan_numbers: List[str]) -> List[Dict]:
@@ -232,8 +226,10 @@ class DiscreteMaterialPlanDAO:
"""
if not plan_numbers:
return []
placeholders = ','.join(['?' for _ in plan_numbers])
sql = f"SELECT * FROM DiscreteMaterialPlanData WHERE PlanNumber IN ({placeholders})"
placeholder = self._get_placeholder()
placeholders = ','.join([placeholder for _ in plan_numbers])
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]')
sql = f"SELECT * FROM {table_name} WHERE PlanNumber IN ({placeholders})"
with get_connection() as db:
return db.execute_query(sql, tuple(plan_numbers))
@@ -248,7 +244,9 @@ class DiscreteMaterialPlanDAO:
List of dictionaries representing records
"""
with get_connection() as db:
sql = "SELECT * FROM DiscreteMaterialPlanData WHERE SourceNumber = ?"
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]')
placeholder = self._get_placeholder()
sql = f"SELECT * FROM {table_name} WHERE SourceNumber = {placeholder}"
return db.execute_query(sql, (order_id,))
def count_by_plan_number(self, plan_number: str) -> int:
@@ -262,7 +260,9 @@ class DiscreteMaterialPlanDAO:
Number of records
"""
with get_connection() as db:
sql = "SELECT COUNT(*) as count FROM DiscreteMaterialPlanData WHERE PlanNumber = ?"
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]')
placeholder = self._get_placeholder()
sql = f"SELECT COUNT(*) as count FROM {table_name} WHERE PlanNumber = {placeholder}"
result = db.execute_query(sql, (plan_number,))
return result[0]['count'] if result else 0
@@ -274,7 +274,8 @@ class DiscreteMaterialPlanDAO:
Total number of records
"""
with get_connection() as db:
sql = "SELECT COUNT(*) as count FROM DiscreteMaterialPlanData"
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]')
sql = f"SELECT COUNT(*) as count FROM {table_name}"
result = db.execute_query(sql)
return result[0]['count'] if result else 0
@@ -300,14 +301,15 @@ class DiscreteMaterialPlanDAO:
unique plans, unique orders, and date range
"""
with get_connection() as db:
sql = """
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]')
sql = f"""
SELECT
COUNT(*) as total_records,
COUNT(DISTINCT PlanNumber) as unique_plans,
COUNT(DISTINCT SourceNumber) as unique_orders,
MIN(CreateDate) as earliest_record,
MAX(CreateDate) as latest_record
FROM DiscreteMaterialPlanData
FROM {table_name}
"""
result = db.execute_query(sql)
return result[0] if result else {}
@@ -322,7 +324,8 @@ class DiscreteMaterialPlanDAO:
List of dictionaries representing all records
"""
with get_connection() as db:
sql = "SELECT * FROM DiscreteMaterialPlanData"
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]')
sql = f"SELECT * FROM {table_name}"
return db.execute_query(sql)
def query_by_source_numbers(self, source_numbers: List[str]) -> List[Dict]:
@@ -344,8 +347,10 @@ class DiscreteMaterialPlanDAO:
for i in range(0, len(source_numbers), batch_size):
batch = source_numbers[i:i + batch_size]
placeholders = ','.join(['?' for _ in batch])
sql = f"SELECT * FROM DiscreteMaterialPlanData WHERE SourceNumber IN ({placeholders})"
placeholder = self._get_placeholder()
placeholders = ','.join([placeholder for _ in batch])
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]')
sql = f"SELECT * FROM {table_name} WHERE SourceNumber IN ({placeholders})"
with get_connection() as db:
results = db.execute_query(sql, tuple(batch))
all_results.extend(results)
@@ -362,9 +367,11 @@ class DiscreteMaterialPlanDAO:
Returns:
List of unique material names
"""
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]')
if source_numbers is None or not source_numbers:
# No filter - get all unique material names
sql = "SELECT DISTINCT MaterialName FROM DiscreteMaterialPlanData WHERE MaterialName IS NOT NULL"
sql = f"SELECT DISTINCT MaterialName FROM {table_name} WHERE MaterialName IS NOT NULL"
with get_connection() as db:
results = db.execute_query(sql)
return [r['MaterialName'] for r in results if r.get('MaterialName')]
@@ -375,10 +382,11 @@ class DiscreteMaterialPlanDAO:
for i in range(0, len(source_numbers), batch_size):
batch = source_numbers[i:i + batch_size]
placeholders = ','.join(['?' for _ in batch])
placeholder = self._get_placeholder()
placeholders = ','.join([placeholder for _ in batch])
sql = f"""
SELECT DISTINCT MaterialName
FROM DiscreteMaterialPlanData
FROM {table_name}
WHERE SourceNumber IN ({placeholders})
AND MaterialName IS NOT NULL
"""