ORM, или объектно-реляционное отображение — это программная технология, которая позволяет взаимодействовать с базами данных с использованием объектно-ориентированной парадигмы. Вместо того чтобы писать SQL-запросы напрямую для работы с данными в базе данных, можно использовать ORM, чтобы взаимодействовать с данными, как если бы они были объектами в вашем коде.
Не бывало ли вам интересно, как работает изнутри такая идейно простая концепция? Благодаря чему достигается удобство работы? Сегодня мы напишем ORM самостоятельно и узнаем, какие инструменты python нам для этого понадобятся.
Ремарка: sqlite3 выбран из-за простоты, нетрудно заменить обращения к нему на обращения к любой удобной для вас базе данных. По синтаксису я ориентировался на джанго.
▍ Базовые типы данных
В sqlite3 существуют: INTEGER — вещественное число с указанной точностью, TEXT — текст, BLOB — двоичные данные, REAL — число с плавающей запятой(float24), NUMERIC — то же, что и INTEGER.
CREATE TABLE "example" (
"Field1" INTEGER NOT NULL,
"Field2" TEXT UNIQUE,
"Field3" BLOB,
"Field4" REAL DEFAULT 123,
"Field5" NUMERIC
);
У каждого из них есть параметры NULL, UNIQUE, DEFAULT, так что первым делом пишем класс, который будут наследовать все остальные:
class BaseType:
field_type: str #название типа данных поля, например, "INTEGER"
def __init__(self, unique: bool = False, null: bool = True, default: int = None):
self.unique = unique
self.null = null
self.default = default
На основе него прописываем остальные базовые классы:
class IntegerField(BaseType):
field_type = 'INTEGER'
class TextField(BaseType):
field_type = 'TEXT'
class BlobField(BaseType):
field_type = 'BLOB'
class RealField(BaseType):
field_type = 'REAL'
class NumericField(BaseType):
field_type = 'NUMERIC'
▍ Пользовательские модели
Я хочу, чтобы пользовательские модели выглядел максимально просто, например, так:
class Box(models.Model):
name = TextField()
width = IntegerField()
height = IntegerField()
Для этого реализуем родительский класс Model. Он должен задавать объекты с заданными переменными. Для этого пишем инициализатор:
class Model:
def __init__(self, *args, **kwargs):
fields = [el for el in vars(self.__class__) if not el.startswith("__")] #поля, которые мы создали в модели (в данном случае name, width, height)
for i, value in enumerate(args):
setattr(self, fields[i], value)#задаем переменные переданные с помощью args
for field, value in kwargs.items():#задаем переменные переданные с помощью kwargs
setattr(self, field, value)
Все методы, которые, мы напишем для класса Model будут работать с любым объектом, который мы зададим.
— *args позволяет передавать произвольное количество позиционных аргументов в функцию.
— Аргументы, переданные как *args, собираются в кортеж (tuple) внутри функции.
— Вы можете использовать любое имя вместо «args», но общепринято использовать именно «args».
Пример:
def print_args(*args):
for arg in args:
print(arg)
print_args(1, 2, 3) # Выводит: 1, 2, 3
2. **kwargs:
— **kwargs позволяет передавать произвольное количество именованных аргументов (ключ-значение) в функцию.
— Аргументы, переданные как **kwargs, собираются в словарь (dictionary) внутри функции.
— Вы можете использовать любое имя вместо «kwargs», но общепринято использовать именно «kwargs».
Пример:
def print_kwargs(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_kwargs(name="John", age=30, city="New York") # Выводит: name: John, age: 30, city: New York
Используя *args и **kwargs, вы можете создавать более гибкие функции, которые могут обрабатывать разные наборы аргументов.
Давайте также создадим сразу метод json(), чтобы возвращать объект в виде словаря, он понадобится нам, например, для api или удобного вывода в консоль.
def json(self):
attributes = {}
for key, value in vars(self).items():
if not key.startswith("__") and not callable(value):#проверка на системные методы и поля
attributes[key] = value
return attributes
В данный момент мы уже можем пользоваться моделями, но пока что без базы данных:
class Box(models.Model):
name = TextField()
width = IntegerField()
height = IntegerField()
print(Box('Box 1', 1, 1).json())#выведет {'name': 'Box 1', 'width': 1, 'height': 1}
▍ Добавляем sqlite3
Для использования базы данных я хочу, чтобы каждый объект имел поле objects — менеджер объектов, через который мы и будем обращаться к sqlite.
Например, так:
Box.objects.add(Box('BOX 1', 1, 1))
Box.objects.get(name='BOX 1')
Box.objects.filter(width=1, height=1)
Box.objects.delete(name="BOX 1")
Мы хотим, чтобы каждый объект имел это поле, но при этом поведение различалось в зависимости от модели объекта. Для решения этого создадим прокси-класс, который определяет модель и возвращает нужный менеджер.
class ProxyObjects:
def __get__(self, instance, owner):
return Object(owner)
__get__ определяется внутри класса, который также может иметь методы __set__ и __delete__, если необходимо управлять операциями присваивания и удаления атрибутов.
Пример использования __get__:
class MyDescriptor:
def __get__(self, instance, owner):
if instance is None:
# Если доступ к дескриптору осуществляется через класс, а не через экземпляр,
# то instance будет равен None, и мы можем вернуть сам дескриптор или другое значение.
return self
else:
# В этом случае instance - это экземпляр объекта, owner - это класс, к которому относится атрибут.
# Мы можем вернуть значение атрибута или выполнить другие действия при доступе к нему.
return instance._value
class MyClass:
def __init__(self, value):
self._value = value
# Используем дескриптор MyDescriptor для атрибута 'my_attribute'
my_attribute = MyDescriptor()
# Создаём экземпляр класса
obj = MyClass(42)
# Доступ к атрибуту 'my_attribute' будет вызывать метод __get__ дескриптора MyDescriptor
print(obj.my_attribute) # Выведет: 42
В этом примере MyDescriptor является дескриптором, который определяет поведение при доступе к атрибуту my_attribute класса MyClass. Метод __get__ определяет, что происходит при чтении значения этого атрибута через экземпляр obj.
Первое, что нужно сделать для работы с базой данных — создать таблицу. Для этого пишем метод:
import sqlite3 # Необходимо импортировать библиотеку для работы с SQLite
class Object:
def __init__(self, object_type: type):
# Конструктор класса принимает тип объекта (класс) и сохраняет его в атрибуте object_type.
self.object_type = object_type
def __createTable__(self):
# Метод для создания таблицы в базе данных, основанной на атрибутах класса object_type.
# Устанавливаем соединение с базой данных
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
# Создаём список custom_fields для хранения определений полей таблицы.
custom_fields = []
# Проходимся по атрибутам класса object_type и извлекаем информацию о полях.
for key, value in vars(self.object_type).items():
if not key.startswith("__") and not callable(value):
field_name = key
field_type = value.field_type
is_unique = value.unique
is_null = value.null
default_value = value.default
# Создаём строку с определением поля и добавляем её в список custom_fields.
field_declaration = [f'"{field_name}" {field_type}']
if is_unique:
field_declaration.append('UNIQUE')
if not is_null:
field_declaration.append('NOT NULL')
if default_value is not None:
field_declaration.append(f'DEFAULT {default_value}')
custom_fields.append(' '.join(field_declaration))
# Создаём SQL-запрос для создания таблицы с определёнными полями.
create_table_sql = f'''
CREATE TABLE IF NOT EXISTS {self.object_type.__name__} (
{", ".join(custom_fields)}
);
'''
# Выполняем SQL-запрос.
cursor.execute(create_table_sql)
# Фиксируем изменения и закрываем соединение с базой данных.
conn.commit()
conn.close()
Создавать таблицу нужно для каждой пользовательской модели — для этого удобно использовать простой декоратор для класса:
def simple_orm(class_: type):
EXTERN_TYPES[class_.__name__] = class_ # Сохраняем в словарь моделей
class_.objects.__createTable__() # Создаём таблицу в бд
return class_
Декораторы используются с помощью символа "@" перед определением функции, которую они декорируют. Вот пример использования декоратора:
def my_decorator(func):
def wrapper():
print("Что-то происходит перед вызовом функции")
func()
print("Что-то происходит после вызова функции")
return wrapper
@my_decorator
def say_hello():
print("Привет, мир!")
say_hello()
В этом примере my_decorator — это декоратор, который добавляет вывод текста до и после вызова функции say_hello. Затем декоратор @my_decorator применяется к функции say_hello, и при вызове say_hello() будет выполнено дополнительное действие, предусмотренное декоратором.
Декораторы часто используются для следующих задач:
- Логирования: Запись логов для функций или методов.
- Аутентификации: Проверка прав доступа перед вызовом функции.
- Кэширования: Сохранение результатов функции для ускорения будущих вызовов с теми же аргументами.
- Измерения времени выполнения: Оценка производительности функции.
- Модификации поведения: Изменение или расширение функциональности функции.
Python предоставляет множество встроенных декораторов, таких как @staticmethod, @classmethod, @property и другие, а также вы можете создавать свои собственные декораторы в соответствии с вашими потребностями.
Теперь при инициализации модели в бд создаётся таблица с соответствующими полями:
▍ JsonField
В sqlite3 нет JsonField по умолчанию, так что мы реализуем его на основе текста. Для начала добавляем его в базовые типы:
class JsonField(BaseType):
field_type = 'JSON'
Json, по сути, ведёт себя, как текст, за исключением того, что при создании и изменении надо использовать json.dumps(), а при получении — json.loads().
▍ ForeignKey
В реляционных базах данных, «foreign key» (внешний ключ) — это структурный элемент, который используется для установления связей между двумя таблицами. Внешний ключ представляет собой один или несколько столбцов в одной таблице, которые связаны с первичным ключом (обычно) в другой таблице. Эта связь позволяет базе данных поддерживать целостность данных и обеспечивать связи между данными в разных таблицах.
ForeignKey должен возвращать объект заданного типа по значению заданного поля.
class ForeignKey(BaseType):
field_type = 'FOREIGN_KEY'
def __init__(self, object_class: type, foreign_field: str, unique: bool = False,
null: bool = True, default=None):
self.object_class = object_class,
self.foreign_field = foreign_field,
self.unique = unique
self.null = null
self.default = default
Я реализую его просто, как json объект с параметрами type, key, value, например:
{"type": "Box", "key": "name", "value": "BOX 1"}.
Теперь мы с помощью него можем делать так:
@simple_orm
class Box(models.Model):
name = TextField()
width = IntegerField()
height = IntegerField()
@simple_orm
class Circle(models.Model):
box = ForeignKey(object_class=Box, foreign_field='name')
name = TextField()
radius = IntegerField()
data = JsonField()
box = Box.objects.add(Box('BOX 1', 1, 1))
circle = Circle.objects.add(Circle(box, "CIRCLE 1", 5, {'data': 5}))
print(circle.json()) #{'box': <__main__.Box object at 0x7f7637f6d850>, 'name': 'CIRCLE 1', 'radius': 5, 'data': {'data': 5}}
▍ Добавляем CRUD
class Object:
def __init__(self, object_type):
self.object_type = object_type
def add(self, obj):
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
d = copy.copy(obj.__dict__)
object_type_name = self.object_type.__name__
for key, value in vars(self.object_type).items():
if not key.startswith("__") and not callable(value):
if type(value) in BASIC_TYPES:
continue
if type(value) == JsonField:
d[key] = json.dumps(d[key])
if type(value) == ForeignKey:
d[key] = json.dumps({'type': value.object_class[0].__name__, 'key': value.foreign_field[0], 'value': getattr(d[key], value.foreign_field[0])})
insert_sql = f'INSERT INTO {object_type_name} ({", ".join(obj.__dict__.keys())}) VALUES ({", ".join(["?"] * len(obj.__dict__))});'
values = tuple(d.values())
cursor.execute(insert_sql, values)
conn.commit()
conn.close()
return obj
def save(self, obj):
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
d = copy.copy(obj.__dict__)
object_type_name = self.object_type.__name__
for key, value in vars(self.object_type).items():
if not key.startswith("__") and not callable(value):
if type(value) in BASIC_TYPES:
continue
if type(value) == JsonField:
d[key] = json.dumps(d[key])
if type(value) == ForeignKey:
d[key] = json.dumps({'type': value.object_class[0].__name__, 'key': value.foreign_field[0], 'value': getattr(d[key], value.foreign_field[0])})
upsert_sql = f'INSERT OR REPLACE INTO {object_type_name} ({", ".join(obj.__dict__.keys())}) VALUES ({", ".join(["?"] * len(obj.__dict__))});'
values = tuple(d.values())
cursor.execute(upsert_sql, values)
conn.commit()
conn.close()
return obj
def get(self, **kwargs):
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
object_type_name = self.object_type.__name__
attr_value_pairs = [(attr, value) for attr, value in kwargs.items()]
where_clauses = [f'{attr} = ?' for attr, _ in attr_value_pairs]
where_clause = ' AND '.join(where_clauses)
select_by_attrs_sql = f'SELECT * FROM {object_type_name} WHERE {where_clause};'
values = tuple(value for _, value in attr_value_pairs)
cursor.execute(select_by_attrs_sql, values)
row = cursor.fetchone()
conn.close()
if row:
obj = self.object_type()
for i, value in enumerate(row):
if type(getattr(obj, cursor.description[i][0])) == JsonField:
setattr(obj, cursor.description[i][0], json.loads(value))
elif type(getattr(obj, cursor.description[i][0])) == ForeignKey:
value = json.loads(value)
setattr(obj, cursor.description[i][0], EXTERN_TYPES[value['type']].objects.get(**{value['key']: value['value']}))
else:
setattr(obj, cursor.description[i][0], value)
return obj
else:
return None
def delete(self, **kwargs):
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
object_type_name = self.object_type.__name__
attr_value_pairs = [(attr, value) for attr, value in kwargs.items()]
where_clauses = [f'{attr} = ?' for attr, _ in attr_value_pairs]
where_clause = ' AND '.join(where_clauses)
delete_by_attrs_sql = f'DELETE FROM {object_type_name} WHERE {where_clause};'
values = tuple(value for _, value in attr_value_pairs)
cursor.execute(delete_by_attrs_sql, values)
conn.commit()
conn.close()
def filter(self, **kwargs):
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
object_type_name = self.object_type.__name__
attr_value_pairs = [(attr, value) for attr, value in kwargs.items()]
where_clauses = [f'{attr} = ?' for attr, _ in attr_value_pairs]
where_clause = ' AND '.join(where_clauses)
select_by_attrs_sql = f'SELECT * FROM {object_type_name} WHERE {where_clause};'
values = tuple(value for _, value in attr_value_pairs)
cursor.execute(select_by_attrs_sql, values)
rows = cursor.fetchall()
conn.close()
objects = []
for row in rows:
obj = self.object_type()
for i, value in enumerate(row):
if type(getattr(obj, cursor.description[i][0])) == JsonField:
setattr(obj, cursor.description[i][0], json.loads(value))
elif type(getattr(obj, cursor.description[i][0])) == ForeignKey:
value = json.loads(value)
setattr(obj, cursor.description[i][0],
EXTERN_TYPES[value['type']].objects.get(**{value['key']: value['value']}))
else:
setattr(obj, cursor.description[i][0], value)
objects.append(obj)
return objects
def all(self):
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
object_type_name = self.object_type.__name__
select_all_sql = f'SELECT * FROM {object_type_name};'
cursor.execute(select_all_sql)
rows = cursor.fetchall()
conn.close()
objects = []
for row in rows:
obj = self.object_type()
for i, value in enumerate(row):
if type(getattr(obj, cursor.description[i][0])) == JsonField:
setattr(obj, cursor.description[i][0], json.loads(value))
elif type(getattr(obj, cursor.description[i][0])) == ForeignKey:
value = json.loads(value)
setattr(obj, cursor.description[i][0],
EXTERN_TYPES[value['type']].objects.get(**{value['key']: value['value']}))
else:
setattr(obj, cursor.description[i][0], value)
objects.append(obj)
return objects
def __createTable__(self):
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
custom_fields = []
for key, value in vars(self.object_type).items():
if not key.startswith("__") and not callable(value):
field_name = key
field_type = value.field_type
is_unique = value.unique
is_null = value.null
default_value = value.default
if value.field_type == 'FOREIGN_KEY':
field_type = "TEXT"
if value.field_type == 'JSON':
field_type = 'TEXT'
field_declaration = [f'"{field_name}" {field_type}']
if is_unique:
field_declaration.append('UNIQUE')
if not is_null:
field_declaration.append('NOT NULL')
if default_value is not None:
field_declaration.append(f'DEFAULT {default_value}')
custom_fields.append(' '.join(field_declaration))
create_table_sql = f'''
CREATE TABLE IF NOT EXISTS {self.object_type.__name__} (
{", ".join(custom_fields)}
);
'''
cursor.execute(create_table_sql)
conn.commit()
conn.close()
▍ Список объектов
Сейчас функции all() и filter() возвращают list состоящий из объектов. Это неудобно, ведь нельзя, например, удалить все объекты. Исправим это, добавив класс ListOfObjects:
class ListOfObjects:
def __init__(self, objects):
self.objects = objects
def filter(self, **kwargs):
filtered_objects = []
for obj in self.objects:
if all(getattr(obj, attr, None) == value for attr, value in kwargs.items()):
filtered_objects.append(obj)
return ListOfObjects(filtered_objects)
def delete(self):
for obj in self.objects:
obj.delete()
def json(self):
object_dicts = [obj.json() for obj in self.objects]
return object_dicts
▍ Примеры
models.py
import simple_orm.models as models
from simple_orm.models import IntegerField, TextField, ForeignKey, JsonField
from simple_orm.models import simple_orm
from pathlib import Path
import sys
PATH = Path(__file__).absolute().parent.parent
sys.path.append(str(PATH))
@simple_orm
class Box(models.Model):
name = TextField()
width = IntegerField()
height = IntegerField()
@simple_orm
class Circle(models.Model):
box = ForeignKey(object_class=Box, foreign_field='name')
name = TextField()
radius = IntegerField()
data = JsonField()
main.py
from models import Box, Circle
box = Box.objects.add(Box('BOX 1', 1, 1))
circle = Circle.objects.add(Circle(box, "CIRCLE 1", 5, {'data': 5}))
print(circle.json())
print(Box.objects.filter(width=1, height=1).json())
print(Circle.objects.get(name="CIRCLE 1").json())
Box.objects.delete(name="BOX 1")
print(Box.objects.all().json())
▍ Заключение
В процессе создания своего ORM мы использовали много сложных инструментов языка python, которые помогли написать короткий и красивый код, решающий довольно сложную задачу.
Да, он не идеальный, вы можете предложить его улучшения в комментариях.
Полный код есть на https://github.com/leo-need-more-coffee/simple-orm
Библиотека для python: https://pypi.org/project/sqlite3-simple-orm
Скачать с помощью pip: pip install sqlite3-simple-orm
Узнавайте о новых акциях и промокодах первыми из нашего Telegram-канала ????
Комментарии (23)
PabloP
17.10.2023 13:05Цитата:
"В sqlite3 существуют: INTEGER — вещественное число с указанной точностью, TEXT — текст, BLOB — двоичные данные, REAL — число с плавающей запятой(float24), NUMERIC — то же, что и INTEGER.
...
У каждого из них есть параметры NULL, UNIQUE, DEFAULT, "
Вопросы:Существуют кто? Типы данных полей таблиц?
У кого есть параметры у типов данных, или конкретных полей таблиц?
Akina
17.10.2023 13:05+3У каждого из них есть параметры NULL, UNIQUE, DEFAULT
Во-первых, это не параметры поля, а ограничения таблицы (CHECK constraints). Просто указанные ограничения ограничивают значения только в одном поле.
Во-вторых, корректно [NOT] NULL. А с учётом того, что при отсутствии этого ограничения вообще значением по умолчанию является именно NULL, то корректнее указать хотя бы только NOT NULL. Но уж никак не только NULL.
В третьих, поле может иметь "параметр" CHECK. Допускаю, что он опущен по причине того, что его обработка куда как сложнее указанных "параметров", да и вообще он по сути есть ограничение именно таблицы, а не поля, но вообще о нём не сказать - неправильно. То же касается и PRIMARY / FOREIGN KEY.
taras_82
17.10.2023 13:05+14IMHO для sqlite проще писать запросы на sql. Мне кажется, что так удобнее, чем изучать сто двадцать пятый по счету ORM.
Emulyator
17.10.2023 13:05-2К сожалению без знания того же Room будет трудно вписаться в команду какой-нибудь андроид разработки. Мне иногда кажется, что sql воспринимается более сложным для многих начинающих разработчиков, чем основной язык программирования, и ORM в какой-то степени решает эту проблему, наряду с другими.
dtkbrbq
17.10.2023 13:05мне однажды пришлось переносить базу из sqlite на postgresql, а потом соответственно исправлять код приложения, и знаете, я очень пожалел что не использовал ORM, переписывать пришлось абсолютно все запросы и код обрабатывавший результат
taras_82
17.10.2023 13:05+1Ну, если в будущем стоит вопрос масшабирования, то имеет смысл писать запросы на чистом SQL, не используя диалекты. Это минимизирует подобные проблемы. Но это исключительно мое мнение и мое видение. Конечно, ситуации могут быть разными.
CrazyOpossum
17.10.2023 13:05Много ли на "чистом" sql можно написать такого что нельзя сделать через orm? Зато orm даёт больше безопасности.
Akina
17.10.2023 13:05+2Много ли на "чистом" sql можно написать такого что нельзя сделать через orm?
Ой, много! Достаточно регулярны вопросы на форумах типа "как вот это реализовать в моём ОРМ" - и изрядная их часть решается исключительно использованием RAW SQL..
mcferden
17.10.2023 13:05+12В целом очень хорошая демонстрация, как НЕ нужно реализовывать ORM:
ActiveRecord довольно проблемная вещь, но даже закрывая глаза на это:
Первичных ключей нет, как ваш
INSERT OR REPLACE
поймет, что заменять? Точнее он-то поймет поUNIQUE
, но у вас в примерах он нигде не указан. А если будет несколько полей сUNIQUE
?Внешние ключи реализованы через JSON, ломая весь смысл проверки целостности. Ваш же пример с удалением
Box
ломает данные.Из-за этого же гарантированный N+1 на любой запрос.
filter
сразу же выполняет запрос, последующие фильтры выполняются на стороне ORM. То естьFoo.objects.filter(a=1).filter(b=2)
сначала прочитает лишние записи (еще помним про N+1), съест RAM, потому будет тратить такты CPU на фильтрацию.filter
поддерживает только сравнение на=
, а как быть с другими операторами?На каждый запрос делается новое подключение к БД.
... тут можно еще писать и писать ...
Если это учебный материал, то ИМХО стоит в нем показывать как делать правильно, а не писать код ради "использования много сложных инструментов языка python, которые помогли написать короткий и красивый код, решающий довольно сложную задачу", тем более что код не слишком "короткий и красивый" и задачу решает, мягко говоря, так себе.
Если задача была продемонстрировать, как устроены другие ORM, то с уверенностью могу заявить, что ни одна популярная ТАК не устроена.
lebedec
17.10.2023 13:05+3Хотя SQLite называют СУБД, фактически это просто библиотека для работы с массивом данных через SQL подобный интерфейс. Поэтому реализация некоторых аспектов ORM на уровне приложения может быть не ошибкой, а необходимостью.
Вот для примера, несколько интересных моментов по вашим замечаниям:
SQLite может вообще не контролировать соответствие схемы таблицы и типов передаваемых данных. Если добавить сюда динамичную природу Python, то инкапсуляция всей работы с данными в ActiveRecord уже не плохая идея.
Первичного ключа может не быть, физически даже ROWID в таблице может не быть. Так что использование UNIQUE частая и уместная практика в SQLite.
Проверка целостности по внешним ключам - опциональная для SQLite фича, полностью по желанию клиента. Поэтому использование собственной реализации через JSON может быть оправдано.
Подключение к SQLite это просто получение ручки на файл или оперативную память. Дорогостоящих операций установки соединения или прогрева отдельного процесса тут нет. Зато бывают сценарии когда нужно закрывать соединение для слаженной работы с локом файла.
Стоит учитывать специфику. Некоторые практики, привычные нам по опыту работы только с "большими СУБД", могут быть просто не применимы для SQLite.
mcferden
17.10.2023 13:05+2То что специфику учитывать нужно спору нет, но я процитирую автора:
sqlite3 выбран из-за простоты, нетрудно заменить обращения к нему на обращения к любой удобной для вас базе данных
и вот тут с большими СУБД будут проблемы. Не хочется, чтобы новички переносили такой опыт на серьезные проекты.
kpmy
17.10.2023 13:05+1Можно было бы поспорить с тем, что это получился ORM, скорее это типобезопасный DSL для SQL-генератора (в Java например есть QueryDSL, очень похоже). Добавьте автогенератор классов по схеме, будет один в один, нишевая библиотека для своих задач.
CrazyOpossum
17.10.2023 13:05+3Прямо сейчас пишу себе микро-орм для Монги с поддержкой motor_asyncio. Мне не нравится дизайн многих орм в которых параметры соединения и сессии передаются через глобальные переменные и плохо работают с asyncio. Поэтому я начал с класса Adapter, который хранит в себе сессию и является фабрикой для моделей. Хочу чтобы интерфейс выглядел примерно так:
class User(metaclass=orm.Model): id_ = orm.Field(index=ASCENDING) name = orm.Field() adapter = Adapter("mongodb://localhost:27017") # protected определяет, будут ли методы типа insert, delete, update среди методов user_class user_class = adapter.retrieve_model(User, database="test", collection="users", protected=False) await user = user_class.find(name="test") await user_class.insert(User(name="test2"))
orm.Model делает свои классы наследуемыми от ProtectedModel / FreeModel, которые имеют разный набор CRUD операций. Adapter.retrieve_model добавляет инстанс AsyncIOMotorCollection в новый класс.
В принципе, всё вроде получается, но если кто-то подскажет хороший проект для референсов или ещё чего - будет круто.
Sap_ru
17.10.2023 13:05+1Вы точно уверены, что NUMERIC это то же самое, что INTEGER? Потому, что авторы sqlite с вами категорически несогласны: "column with NUMERIC affinity may contain values using all five storage classes".
NUMERIC - специальный тип, который может хранить любой из всех остальных типов. Согласитесь, что это совсем не то же самое, что и "INTEGER".lebedec
17.10.2023 13:05SQLite умеет динамически выбирать размер целочисленных данных в зависимости от фактической размерности значений. Это актуально только для процесса сериализации данных на хранение.
Клиентский драйвер вам всегда будет распаковывать и выдавать 64 битное знаковое число.
Его название в схеме таблицы не более чем сахар для адаптации различных диалектов SQL, можете называть как хотите: NUMERIC, INTEGER, INT, BIGINT, etc.
lebedec
17.10.2023 13:05А нет же. Сам сказанул не то, сам себя поправлю.
По официальной документации SQLite, действительно есть NUMERIC тип, и это совсем не то же самое что и INTEGER.
Мне как и автору видимо привычно числа с фиксированными точностью и масштабом называть DECIMAL, поэтому такая путаница возникла.
Karopka
17.10.2023 13:05+1До чего люди ленивы. Чтобы не разбираться с изучением SQL и особенностями конкретной СУБД , строят свои или используют готовые ORM. Столкнувшись с проблемами (например, производительности), они лезут внутрь кода ORM, но бестолку:SQL и особенности работы с конкретной СУБД они не знают...
А ведь достаточно выучить SQL "в общем" и таки разобраться с особенностями работы с конкретной. СУБД.
economist75
17.10.2023 13:05+1Недостаточно. DS-ники еще более ленивы чем люди. Они неплохо знают SQL, но предпочитают выдернуть все данные по SELECT * FROM целиком. И вместо написания утомительных CTE/JOIN - всё остальное делают в Pandas/аналогах, в небольших ячейках кода Jupyter/Lab. Просто потому что там быстрее, удобнее, "однострочнее" выходит, не теряется контекст (вычисленные значения висят в RAM).
Трех-экранные SELECT-запросы в той же 1С задолбали, работать с ними невероятно тяжело, даже их авторам. Прямое подтверждение - в современных типовых конфигурация 1С количество таблиц/views превысило разумное (10000+). При том что бухучет на бумаге ведется всего в трех таблицах: Главной книге, пяти однотипных Журналах-ордерах и Кассовой книге. И с этим учетом были сделаны все великие стройки страны.
Поэтому ORM и Pandas - никуда не уйдут (как и SQL), т.к. по-своему удобны на разных задачах. Они дают вам те самые "три таблицы", если не перетягивать в одну сторону одеяло технологий, а использовать все.
CrazyOpossum
17.10.2023 13:05Например, мне ещё далеко до того чтобы упереться в производительность. Зато баги случаются постоянно - поле не того типа, поле пропущено. Плюс замедляется разработка - каждое добавление колонки требует кучу переписываний, пропустишь одно - см. предыдущие предложение.
Stawros
BLOB — булевая переменная
Булевая - это true/false. Блобы - это массивы двоичных данных обычно.
pzrnqt1vrss Автор
Спасибо, исправил