Классический сценарий: есть база данных и приложение на бэкенде. Для подключения достаточно знать адрес, порт, имя пользователя, пароль — и прямой доступ перед вами. Но что делать, если необходимо подключить no-code базу данных, которой можно управлять только через REST API? Есть ли способ интегрировать такие подключения в логику «красиво», не поломав архитектуру?

Привет, Хабр! Меня зовут Влад, в свободное время я занимаюсь разработкой. В этой статье расскажу, как можно относительно нативно интегрировать работу с платформой NocoDB на бэкенде, какие паттерны подходят и зачем мне понадобилось разработать собственный Python-модуль. Подробности под катом!

Особенности работы с NocoDB

NocoDB — это платформа с открытым исходным кодом, которая превращает базы данных в удобные таблицы и интерфейсы.

Если у вас есть собственный проект, которым занимаются люди без знания SQL, то подобное решение будет закрывать базовые потребности. Сотрудники смогут самостоятельно создавать и просматривать таблицы, добавлять собственные строки, настраивать необходимые поля и не только, а вы — легко управлять политиками доступа. Но уже на этом этапе проявляются все нюансы решения.

Создание базы данных в NocoDB

Допустим, ваш сотрудник создает базу с клиентами: все удобно, за пределы графического интерфейса выходить не нужно. Таблицы созданной базы поддерживают полный функционал NocoDB. Но все просто ровно до тех пор, пока вы не решите подключиться к базе со своего бэкенда. Спойлер: напрямую, например, через ORM вроде Django ORM или SQLAlchemy у вас это сделать не получится. Потому что к созданной внутри NocoDB базе доступ будет только через REST API.

Подключение внешней базы данных в NocoDB

Если же вам нужно прямое подключение к базе данных, например PostgreSQL или MySQL, при этом важно сохранить графическую оболочку для других участников проекта, можно создать интеграцию.

Тогда NocoDB становится «прослойкой» c GUI и REST API, что в большинстве случаев правильно. Ведь вы получаете возможность напрямую подключиться к базе данных с бэкенда и не теря��те в производительности.

Однако вы теряете полную интеграцию с функционалом NocoDB. Встроенные базы создаются и управляются напрямую, что обеспечивает максимальную совместимость — например, с триггерами, связями между таблицами, интерфейсом по умолчанию и прочим. Внешняя база данных может не поддерживать все «фичи» NocoDB из коробки. Кроме того, прямое подключение тесно связано с логикой ORM, из-за чего, например, при смене фреймворка придется переписывать слой бизнес-логики.

Выводы

REST API NocoDB — в целом, универсальный вариант подключения, если ваш проект небольших или средних размеров. Однако даже в таком случае важно сделать оговорку: для моделей вашей бизнес-логики, к которым пользователи обращаются с высокой частотой, все равно лучше использовать внешнюю базу данных с подключением напрямую. Это будет и производительнее, и безопаснее.

В остальных случаях можно использовать встроенные в NocoDB базы данных и REST API подключение. Например, для таблиц со списком товаров, если ваш проект — интернет-магазин. Такие данные изменяются нечасто, а если добавить кэширование, чтобы по каждому запросу модель не обращалась в NocoDB, разница в производительности между прямым подключением и REST API будет минимальной.

Облачные базы данных

Создайте готовую базу данных в облаке за 5 минут. Поддерживаем PostgreSQL, MySQL, Redis и не только.

Подробнее →

Интеграция NocoDB

Допустим, вы определили, что чувствительные и часто обновляемые данные будете хранить во внешней базе данных, с которой можно работать посредством ORM. Тем временем «наиболее статичные» данные, например список товаров, будут храниться в базе NocoDB. Тогда сотрудникам будет проще ими управлять: актуализировать стоимость, информацию о количестве и не только.

Если за ORM ответственны, очевидно, модели, то для интеграции NocoDB нам нужен особый тип моделей — некий Repository. Попробуем его описать с помощью нескольких сущностей, вдохновившись Django ORM.

Repository

Обычно я выношу всю бизнес-логику подальше от функций представления — так архитектура становится чище и в целом логичнее. Так, если пришел запрос на список товаров, мы можем обратиться к списку объектов через Product.objects.filter(name='Творог') и вернуть json. А если поступил запрос на покупку, то достаточно вызвать пользовательский метод buy — и вернуть результат, например номер продукта.

Нам нужно в том же models.py научиться локанично описывать свои модели, так называемые репозитории, которые будут инкапсулировать логику доступа «к другим данным» (в нашем случае — к NocoDB) и предоставлять чистый абстрактный интерфейс для выполнения запросов и операций. Такой подход называют Repository Pattern.

def method_allowed(func):
   def wrapper(self, *args, **kwargs):
       if hasattr(self, 'allowed_methods') and self.allowed_methods is not None:
           if func.__name__ not in self.allowed_methods:
               raise AttributeError(f"Method '{func.__name__}' is not allowed.")
       return func(self, *args, **kwargs)
   return wrapper

class Repository:
   allowed_methods = ['get', 'values', 'filter', 'registration']

   def __init__(self):
       self.table = self.__class__.data_source(
           table_name=self.__class__.__name__
       )
       self.objects = self.Objects(
           parent=self,
           allowed_methods=getattr(self, 'allowed_methods', None)
       )

   class Objects:
       def __init__(self, parent, allowed_methods=None):
           self.parent = parent
           self.allowed_methods = allowed_methods
      
       def recache(self):
           self.parent.table._data_cache = None
           self.parent.table.data

       @method_allowed
       def get(self, **kwargs):
           if not kwargs:
               return self.parent.table.data
           for item in self.parent.table.data:
               if all(getattr(item, k, None) == v for k, v in kwargs.items()):
                   return item
           raise AttributeError(f'No item found matching {kwargs}')

       @method_allowed
       def values(self, *args):
           if not args:
               return self.parent.table.data
           fields = [field for field in args]
           return self.parent.table.values(fields=fields)

       @method_allowed
       def filter(self, *, gt='', **kwargs):
           expression_mock = {}

           for field in kwargs.keys():
               value = kwargs[field]
               if '__in' in field:
                   field = field.replace('__in', '')
               expression_mock[field] = value
                  
           expression_list = []
           for field in expression_mock.keys():
               if isinstance(expression_mock[field], list):
                   exp_part = '~or'.join([f'({field},eq,{value})' for value in expression_mock[field]])
               else:
                   exp_part = f'({field},eq,{expression_mock[field]})'
               expression_list.append(exp_part)

           expression_list.append(gt)
           expression = '~and'.join(expression_list)

           return self.parent.table.filter(where=expression)
      
       @method_allowed
       def registration(self, **kwargs):
           if not kwargs:
               return Exception(f'Append is broken: undefined target')
           try:
               target = kwargs
               self.parent.table.append(target=target)
           except Exception as e:
               raise Exception(f'Append is broken: {e}')

Выше — простая реализация Repository, на базе которого можно будет создавать модели со своими пользовательскими методами. Здесь можно «творить» как хочется — например, я добавил динамический интерфейс allowed_methods, который блокирует нежелательный доступ к методам репозитория, если они явно не разрешены. Вот, как это выглядит на практике:

from common.repository import Repository
from transactions.services.nocodb_agent import TransactionsData
from transactions.exceptions import TransactionError

class Transactions(models.Model):
   class Shop(Repository):
       data_source = TransactionsData
       allowed_methods = ['registration']
  
       @staticmethod
       def buy(username, pcode):
           try:
               transactionCode = randomCode()

               Transactions.Shop.objects.registration(
                   transactionCode=transactionCode,
                   username=username,
                   pcode=pcode,
                   action=__name__
               )
               return Shop.Items.objects.get(pcode=pcode).item
           except InsufficientFunds as e:
               raise TransactionError(message=e, code=e.code)
           except Exception as e:
               print(e)

Как видим, мы создали модель Transactions.Shop, которая наследуется от Repository и явно указывает, какие методы разрешены: только регистрация новых транзакций, чтобы нельзя было считать чужие. Но что такое data_source?

RepositoryData

Repository должен брать откуда-то данные и возвращать коллекцию. Для этого у него есть слой данных приложения (data/repositorydata) — RepositoryData, который реализует прямой доступ к REST API NocoDB. В нашем примере за доступ отвечает TransactionsData:

from pycodb import DataBase, BaseTable

DataBase.DOMAIN = '127.0.0.1:80'
DataBase.NOCODB_API_KEY = '...'

class TransactionsData(DataBase):
   class Shop(BaseTable):
       view_id = '...'
       table_id = '...'

Иначе говоря: есть база данных Transactions, внутри которой — таблица Shop. Ключ и параметры доступа (view_id и table_id) можно взять из самого NocoDB. При этом вся основная логика уже реализована в моем модуле PycoDB.

Я написал PycoDB, так как сам активно использую NocoDB для хранения данных приложения. Это довольно простой модуль — но этим он и отличается от аналогичных решений. Так что смело делайте форк, предлагайте улучшения и используйте в своих проектах.

Итого, используя Repository Pattern и слой данных, который я называю RepositoryData, вы можете легко работать с базами данных по REST API в ORM-based формате. Но это мы начали «со сложного». Если вам просто нужен удобный интерфейс для работы с NocoDB, можно использовать PycoDB в отрыве от всего остального.

Погружение в PycoDB

Установка и начало работы

1. Для начала создадим базу данных и таблицу. Пусть это будет некая база SpecificData с таблицей Topics, которая заполнена тестовыми значениями.

2. Далее установим модули pycodb и requests с помощью менеджера пакетов PyPi:

pip3 install requests pycodb

3. Отлично. Теперь можем импортировать модуль в код своего сервиса, прописать домен или IP-адрес, по которому доступен NocoDB, и ключ доступа:

from pycodb import DataBase, BaseTable

DataBase.DOMAIN = 'Your_NocoDB_Domain_or_IP'
DataBase.NOCODB_API_KEY = 'Your_NocoDB_API_KEY'

4. Следующим шагом нужно описать структуру. Для этого нам понадобятся два основных суперкласса, от которых будет наследоваться RepositoryData: класс базы данных и таблицы.

from pycodb import DataBase, BaseTable

DataBase.DOMAIN = 'Your_NocoDB_Domain_or_IP'
DataBase.NOCODB_API_KEY = 'Your_NocoDB_API_KEY'

class SpecificData(DataBase):
   class Topics(BaseTable):
       view_id = 'View_Id' # можно посмотреть в Swagger базы данных
       table_id = 'Table_Id' # можно скопировать в списке таблиц

Получение данных

Можно приступать к работе с NocoDB. Сейчас REST API максимально скрыт за объектной мод��лью (но без лишних абстракций, так как это сервисный слой) и мы можем, например, вывести список всех записей:

all_topics = SpecificData(table_name='Topics').data
print(all_topics)

Вывод:

DataProxy([<DataItem Id=1, username=username1, Date time=2025-11-11 21:31:13+00:00, hash=hash1>, <DataItem Id=2, username=username2, Date time=2025-11-11 21:31:24+00:00, hash=hash2>, <DataItem Id=3, username=username3, Date time=2025-11-11 21:31:29+00:00, hash=hash3>, <DataItem Id=4, username=username4, Date time=2025-11-11 21:31:32+00:00, hash=hash4>])

Отлично! В качестве записей в таблице Topics мы получили коллекции DataProxy. 

Фильтрация данных

Если нет цели получать все подряд данные, можем воспользоваться методом filter:

filtered_topics = Transactions(table_name='Topics').filter(where='(username,eq,username1)')
print(filtered_topics)

Вывод:

DataProxy([<DataItem Id=1, username=username1, Date time=2025-11-11 21:31:13+00:00, hash=hash1>])

В целом, с помощью метода filter можно задавать любые query-параметры, упрощающие получение данных из NocoDB. Полный список можно посмотреть в таблице.

Получение определенных полей

Допустим, что в таблице есть конфиденциальные данные, которые не стоит лишний раз получать на бэкенде во избежании утечек. В таком случае можно воспользоваться методом values:

limited_topics = Transactions(table_name='Topics').values(['Id', 'username'])
print(limited_topics)

Вывод:

DataProxy([<DataItem Id=1, username=username1>, <DataItem Id=2, username=username2>, <DataItem Id=3, username=username3>, <DataItem Id=4, username=username4>])

Добавление данных

Наконец, если нужно добавить новую запись в таблицу, достаточно использовать метод append и параметр target:

Transactions(table_name='Topics').append(target={
   'username':'username9999', 'hash':'hash9999'
})

new_topic = Transactions(table_name='Topics').filter(where='(username,eq,username9999)')
print(new_topic)

Вывод:

DataProxy([<DataItem Id=5, username=username9999, Date time=2025-11-11 22:23:41+00:00, hash=hash9999>])

Разумеется, PycoDB еще нужно дорабатывать, так как его функционал я расширял по мере необходимости. Так, например, нет элементарных действий вроде удаления или обновления записей — потому что такие важные операции я предпочитаю выполнять вручную. Так что будут рад увидеть ваши форки на GitHub.

Заключение

Надеюсь, мой материал оказался для вас полезным. Repository Pattern — то, к чему я пришел не сразу, так как старался лишний раз не прибегать к no-code базам данных. На деле же это довольно удобное для абстрактного слоя решение, к которому можно адаптироваться, как мы выяснили в этой статье.

Что думаете на счет этого вы? Использовали ли подобные подходы в своих проектах? Делитесь опытом в комментариях!

Комментарии (0)