Files
Django/gvsdsdk/model_base.py
TermiNexus 2479a878c5 revert f252fc2aff
revert 修复了兼容层存在的一些显著问题
时间上无法容忍,暂且先恢复到这个版本重新进入生产状态
2026-06-17 04:02:07 +08:00

405 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import typing
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from django.db import models
from django.db.models import Q
from .fluent import FluentQuery
class classproperty:
def __init__(self, func):
self.fget = func
def __get__(self, obj, objtype=None):
return self.fget(objtype if objtype is not None else type(obj))
ABBREVIATIONS = {
'uuid': 'UUID',
'id': 'ID',
'ip': 'IP',
'url': 'URL',
'uri': 'URI',
'api': 'API',
'html': 'HTML',
'css': 'CSS',
'js': 'JS',
'json': 'JSON',
'xml': 'XML',
'sql': 'SQL',
'http': 'HTTP',
'https': 'HTTPS',
'smtp': 'SMTP',
'ssl': 'SSL',
'tls': 'TLS',
'ssh': 'SSH',
'ftp': 'FTP',
'2fa': '2FA',
'elt': 'ELT',
'sve': 'SVE',
't4': 'T4',
'mc': 'MC',
'crb': 'CRB',
'ccgo': 'CCGO',
'ugm': 'UGM',
'gerp': 'GERP',
'abac': 'ABAC',
'rbac': 'RBAC',
'smtm': 'SMTM',
'pwd': 'PWD',
}
def snake_to_pascal(name):
if '_' not in name:
return name[0].upper() + name[1:] if name else name
parts = name.split('_')
result = []
for p in parts:
lower = p.lower()
if lower in ABBREVIATIONS:
result.append(ABBREVIATIONS[lower])
else:
result.append(p.capitalize())
return ''.join(result)
def pascal_to_snake(name):
import re
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1)
parts = s2.lower().split('_')
return '_'.join(parts)
class _InvertedFieldExpression:
def __init__(self, field_expr):
self._field_expr = field_expr
def startswith(self, value):
return ~Q(**{f'{self._field_expr._django_name}__startswith': value})
def contains(self, value):
return ~Q(**{f'{self._field_expr._django_name}__contains': value})
def like(self, pattern):
clean = pattern.strip('%')
return ~Q(**{f'{self._field_expr._django_name}__icontains': clean})
def in_(self, values):
return ~Q(**{f'{self._field_expr._django_name}__in': values})
def is_(self, other):
if other is None:
return ~Q(**{f'{self._field_expr._django_name}__isnull': True})
return ~Q(**{self._field_expr._django_name: other})
class FieldExpression:
_resolve_cache = {}
def __init__(self, model_class, field_name):
self.model_class = model_class
self.field_name = field_name
self._django_name = self._resolve_django_name()
def _resolve_django_name(self):
cache_key = (self.model_class.__name__, self.field_name)
if cache_key in FieldExpression._resolve_cache:
return FieldExpression._resolve_cache[cache_key]
if hasattr(self.model_class, '_meta'):
for f in self.model_class._meta.local_fields:
if f.name == self.field_name:
FieldExpression._resolve_cache[cache_key] = f.name
return f.name
FieldExpression._resolve_cache[cache_key] = self.field_name
return self.field_name
def __eq__(self, other):
if isinstance(other, FieldExpression):
return NotImplemented
if other is None:
return Q(**{f'{self._django_name}__isnull': True})
return Q(**{self._django_name: other})
def __ne__(self, other):
if isinstance(other, FieldExpression):
return NotImplemented
return ~Q(**{self._django_name: other})
def __gt__(self, other):
return Q(**{f'{self._django_name}__gt': other})
def __ge__(self, other):
return Q(**{f'{self._django_name}__gte': other})
def __lt__(self, other):
return Q(**{f'{self._django_name}__lt': other})
def __le__(self, other):
return Q(**{f'{self._django_name}__lte': other})
def __getitem__(self, key):
return _JsonKeyExpression(self._django_name, key)
def __hash__(self):
return hash((self.model_class.__name__, self.field_name))
def __and__(self, other):
if isinstance(other, Q):
return Q(self) & other
return NotImplemented
def __or__(self, other):
if isinstance(other, Q):
return Q(self) | other
return NotImplemented
def __invert__(self):
return _InvertedFieldExpression(self)
def is_(self, other):
if other is None:
return Q(**{f'{self._django_name}__isnull': True})
return Q(**{self._django_name: other})
def is_not(self, other):
if other is None:
return Q(**{f'{self._django_name}__isnull': False})
return ~Q(**{self._django_name: other})
def like(self, pattern):
clean = pattern.strip('%')
return Q(**{f'{self._django_name}__icontains': clean})
def contains(self, value):
return Q(**{f'{self._django_name}__contains': value})
def ilike(self, pattern):
return self.like(pattern)
def startswith(self, value):
return Q(**{f'{self._django_name}__startswith': value})
def istartswith(self, value):
return Q(**{f'{self._django_name}__istartswith': value})
def endswith(self, value):
return Q(**{f'{self._django_name}__endswith': value})
def iendswith(self, value):
return Q(**{f'{self._django_name}__iendswith': value})
def in_(self, values):
if hasattr(values, '_qs'):
return Q(**{f'{self._django_name}__in': values._qs.values(self._django_name)})
return Q(**{f'{self._django_name}__in': values})
def between(self, low, high):
return Q(**{f'{self._django_name}__gte': low}) & Q(**{f'{self._django_name}__lte': high})
def regex(self, pattern):
return Q(**{f'{self._django_name}__regex': pattern})
def iregex(self, pattern):
return Q(**{f'{self._django_name}__iregex': pattern})
def desc(self):
return f'-{self._django_name}'
def asc(self):
return self._django_name
def __repr__(self):
return f'FieldExpr({self.model_class.__name__}.{self.field_name})'
def __str__(self):
return self._django_name
class _JsonKeyExpression:
def __init__(self, field_name, key):
self.field_name = field_name
self.key = key
def _lookup(self, suffix=''):
if suffix:
return f'{self.field_name}__{self.key}__{suffix}'
return f'{self.field_name}__{self.key}'
def cast(self, type_hint):
return _CastJsonExpression(self.field_name, self.key, type_hint)
def __eq__(self, other):
return Q(**{self._lookup(): other})
def __ne__(self, other):
return ~Q(**{self._lookup(): other})
def __gt__(self, other):
return Q(**{self._lookup('gt'): other})
def __ge__(self, other):
return Q(**{self._lookup('gte'): other})
def __lt__(self, other):
return Q(**{self._lookup('lt'): other})
def __le__(self, other):
return Q(**{self._lookup('lte'): other})
def between(self, low, high):
return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high})
def __repr__(self):
return f'JsonKeyExpr({self.field_name}[{self.key!r}])'
class _CastJsonExpression:
def __init__(self, field_name, key, type_hint):
self.field_name = field_name
self.key = key
self.type_hint = type_hint
def _lookup(self, suffix=''):
if suffix:
return f'{self.field_name}__{self.key}__{suffix}'
return f'{self.field_name}__{self.key}'
def between(self, low, high):
return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high})
def __eq__(self, other):
return Q(**{self._lookup(): other})
def __ne__(self, other):
return ~Q(**{self._lookup(): other})
def __gt__(self, other):
return Q(**{self._lookup('gt'): other})
def __ge__(self, other):
return Q(**{self._lookup('gte'): other})
def __lt__(self, other):
return Q(**{self._lookup('lt'): other})
def __le__(self, other):
return Q(**{self._lookup('lte'): other})
def __repr__(self):
type_name = getattr(self.type_hint, '__name__', str(self.type_hint))
return f'CastJsonExpr({self.field_name}[{self.key!r}]->{type_name})'
class QModelBase(models.base.ModelBase):
def __new__(mcs, name, bases, namespace, **kwargs):
from django.db import models as dj_models
for attr_name, attr_val in list(namespace.items()):
if isinstance(attr_val, dj_models.Field) and not attr_val.db_column:
# ForeignKey/OneToOneField 的数据库列名是 attr_name + '_id'
if isinstance(attr_val, (dj_models.ForeignKey, dj_models.OneToOneField)):
attr_val.db_column = attr_name + '_id'
else:
attr_val.db_column = attr_name
# 动态设置 managed子服务器可设 GVSDSDK_MANAGE_TABLES=True 让 gvsdsdk 管理表
meta = namespace.get('Meta')
if meta and getattr(meta, 'app_label', None) == 'gvsdsdk':
try:
from django.conf import settings
if getattr(settings, 'GVSDSDK_MANAGE_TABLES', False):
meta.managed = True
except Exception:
pass
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
# 模型创建后,自动为所有字段添加描述符,支持 Model.field_name 链式 OOP
if not cls._meta.abstract:
_attach_field_descriptors(cls)
return cls
def _attach_field_descriptors(cls: Type) -> None:
"""为 QModel 子类的所有字段自动添加 FieldExpressionDescriptor / ForeignKeyIdDescriptor
使 Model.field_name 在类级别访问时返回 FieldExpression支持链式 OOP 查询。"""
from django.db import models as dj_models
for field in cls._meta.local_fields:
fname = field.name
# 跳过已有描述符或内部属性
if fname.startswith('_'):
continue
# 检查是否已有同名描述符(避免覆盖手动定义的)
existing = cls.__dict__.get(fname)
if isinstance(existing, (FieldExpressionDescriptor, ForeignKeyIdDescriptor)):
continue
# 跳过 Django 自动创建的反向关系和 id 主键(如果用户未显式定义)
if fname == 'id' and field.primary_key and not isinstance(
cls.__dict__.get(fname), (FieldExpressionDescriptor, ForeignKeyIdDescriptor)
):
# id 字段也添加描述符
setattr(cls, fname, FieldExpressionDescriptor(fname))
continue
if isinstance(field, (dj_models.ForeignKey, dj_models.OneToOneField)):
# FK 字段:类级别访问返回 FieldExpression实例级别访问返回关联对象
setattr(cls, fname, ForeignKeyIdDescriptor(fname))
else:
setattr(cls, fname, FieldExpressionDescriptor(fname))
class QModel(models.Model, metaclass=QModelBase):
"""QModel 兼容层基类,提供 SQLAlchemy 风格的链式 OOP 查询接口。
所有继承 QModel 的模型自动获得:
- ``Model.query`` : 返回 FluentQuery支持链式查询
- ``Model.field_name`` : 类级别访问返回 FieldExpression支持表达式查询
- ``Model.field_name`` : 实例级别访问返回字段值(与普通 Django Model 一致)
用法示例::
# 链式查询
orders = Dingdan.query.filter(zhuangtai=8).order_by('-create_time').to_list()
# 字段表达式
Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1,2,3]))
# 聚合
Dingdan.query.aggregate(total=Sum('jine'))
# values / annotate
Dingdan.query.values('zhuangtai').annotate(cnt=Count('id'))
# 分页
Dingdan.query.paginate(page=1, per_page=20)
"""
class Meta:
abstract = True
@classproperty
def query(cls) -> FluentQuery:
"""返回 FluentQuery 兼容层查询构建器,支持链式 OOP 查询。"""
return FluentQuery(cls)
@classmethod
def __class_getitem__(cls, item) -> FluentQuery:
"""支持 Model[condition] 语法,返回 filter 后的 FluentQuery。
用法::
Dingdan[Dingdan.zhuangtai == 8].order_by('-create_time').to_list()
"""
if isinstance(item, Q):
return cls.query.filter(item)
if isinstance(item, dict):
return cls.query.filter(**item)
return cls.query.filter(item)