Соединения с базами данных в Python: расширяемые, многоразовые и безопасные

Эрик Пэн

Соединители базы данных

Представьте, что вы специалист по данным, отвечающий за построение модели, и вам нужно подключиться к базе данных - довольно распространенная задача для вас. Как бы ты это сделал?

Самый простой способ, наверное, такой:

import cx_Oracle
import pandas as pd
cnn = cx_Oracle.connect(conn_str)
data = pd.read_sql(sql, cnn.connector)
# or do something else

(Мы будем использовать соединение с Oracle на протяжении всего сообщения в качестве примера, но этот блог применим и ко всем другим базам данных!)

Этот небольшой фрагмент кода кажется очевидным выбором, не так ли? Такой подход может сделать работу, но не лучший.

Почему? Что ж, надеюсь, нас проинструктировали закрыть все указатели или соединители после их использования. Подключения к базе данных - ограниченный и относительно дорогой ресурс. Если вы не будете осторожны, вы можете случайно достичь максимального количества подключений только с одним работающим проектом, но с десятками подключений из исторических проектов.

Более осторожный способ работы с коннекторами базы данных может быть следующим:

import cx_Oracle
import pandas as pd
cnn = cx_Oracle.connect(conn_str)
data = pd.read_sql(sql, cnn.connector)
# or do something else
cnn.close()

Выглядит солидно, правда? Что ж, лучше, но не совсем так.

Не уверен, почему? Рассмотрим следующее: что произойдет, если что-то посередине прервет сценарий? При таком подходе вы можете случайно достичь максимального количества допустимых подключений, если, скажем, несколько сценариев тестирования имеют сбой операций с базой данных во время выполнения. (Некоторые другие важные вопросы здесь: что, если вы изменили имя движка / коннектора и забыли изменить его в последующих строках кода? Или если у вас есть несколько коннекторов одновременно, но вы забыли об одном из них?)

Если вы очень осторожны и хотите решить каждый из этих случаев, вы можете придумать что-то вроде этого:

import cx_Oracle
import logging
try:
    cnn = cx_Oracle.connect(conn_str)
    cnn.do_something()
except Exception:
    cnn.rollback()
    logging.error("Database connection error")
    raise
else:
    cnn.commit()
finally:
    cnn.close()

Теперь это выглядит хорошо. Мы используем rollback, если что-то пойдет не так, commit, если все пойдет хорошо, и close наше соединение в конце. Хотя это, безусловно, значительное улучшение по сравнению с нашим первым подходом, нам все еще не хватает лучших практик.

Почему? Что ж, иногда вам приходится иметь дело с несколькими коннекторами базы данных (например, на этапе исследования данных), и вам придется регулярно читать данные из каждого из них. Это может привести к некоторой избыточности, которая предполагает, что функция многократного использования может хорошо подходить для этого сценария.

Менеджеры контекста и декораторы

Обсуждаемые выше передовые методы напоминают мне файловые объекты Python: просто инициализируйте соединение и безопасно закрывайте его, когда закончите, независимо от того, что происходит в середине. С этим синтаксисом файл должным образом закрывается после завершения блока, даже если по пути возникает исключение. Было бы здорово, если бы мы могли обрабатывать соединения с базой данных аналогичным образом.

Хотя мы не можем относиться к подключению к базе данных точно так же, как оказалось, мы можем использовать нечто подобное, известное как менеджер контекста:

import cx_Oracle
class oracle_connection(object):
    """oracle db connection"""
    def __init__(self, connection_string=conn_str):
        self.connection_string = connection_string
        self.connector = None
    def __enter__(self):
        self.connector = cx_Oracle.connect(self.connection_string)
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_tb is None:
            self.connector.commit()
        else:
            self.connector.rollback()
        self.connector.close()

Менеджеры контекста используются для управления ресурсами. Они позволяют вам распределять и высвобождать ресурсы именно тогда, когда вы хотите. С помощью этого конкретного диспетчера контекста мы можем вызывать соединения с базой данных следующим образом:

import pandas as pd
with oracle_connection() as conn:
    data =  pd.read_sql(sql, conn.connector)

(Подробнее о менеджерах контекста здесь. Вы также можете использовать contextlib в стандартной библиотеке Python для большего удовольствия от менеджера контекста!)

Если вы готовы запустить свой код в производство, вам может потребоваться несколько отдельных коннекторов базы данных, и, следовательно, вы обнаружите, что использование диспетчера контекста слишком многословно. Отличной альтернативой является декоратор, который представляет собой волшебный фрагмент кода, который изменяет функции, а не объекты. См. Ниже наш предыдущий пример, реконфигурированный как декоратор:

import cx_Oracle
import logging
def db_connector(func):
    def with_connection_(*args,**kwargs):
        conn_str = conn_str
        cnn = cx_Oracle.connect(conn_str)
        try:
            rv = func(cnn, *args,**kwargs)
        except Exception:
            cnn.rollback()
            logging.error("Database connection error")
            raise
        else:
            cnn.commit()
        finally:
            cnn.close()
        return rv
    return with_connection_

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

При разработке декоратора вам необходимо иметь внутреннюю функцию и функцию декоратора. Здесь db_connector - это функция декоратора, а with_connection_ - внутренняя функция, которая определяется только внутри декоратора. При вызове этого декоратора оформленная функция func в конечном итоге указывает на внутреннюю функцию декоратора, with_connection_. Затем эта внутренняя функция завершает декорированную функцию func, выполнив следующие действия:

  1. Принятие произвольного количества позиционных аргументов и аргументов ключевого слова, позволяющее декорированной функции работать нормально: with_connection_(*args,**kwargs)
  2. Передача коннектора базы данных декорированной функции:
    func(cnn, *args, **kwargs)
  3. Убедитесь, что коннектор базы данных правильно обрабатывается синтаксисом Python try/except, как мы говорили ранее в этом посте.

После этого, если вы хотите изменить существующую функцию для использования нашего коннектора базы данных, вы можете добавить декоратор прямо над ней, например:

@db_connector
def do_some_job(cnn, arg1, arg2=None):
    cur = cnn.cursor()
    cur.execute(SQL, (arg1, arg2)) # or something else
do_some_job(arg1, arg2)

Синтаксис символа @ в основном (хотя и не буквально) означает следующее:

do_some_job(arg1, arg2) = db_connector(do_some_job(arg1, arg2))

Этот декоратор создает соединение с базой данных и передает его функции под ним. По сути, он выполняет работу диспетчера контекста, но еще проще. Есть и другие интересные способы написания декораторов - подробнее здесь.

Обработка учетных данных

Итак, теперь вы знаете, как настроить безопасное и простое соединение с базой данных с помощью диспетчеров контекста, декораторов и других передовых методов. Но мы еще не закончили!

Возникает вопрос: как в любом из приведенных выше кодов ввести имя пользователя и пароль для подключения к базе данных? (Или, скорее, в этом случае передать полномочия вышеупомянутому conn_str?)

Одна из идей может заключаться в создании .py файла со всеми необходимыми учетными данными. Это, безусловно, лучше, чем жесткое кодирование ваших учетных данных в переменной conn_str в каждом сценарии, который использует соединение с базой данных. Но никогда не рекомендуется жесткое кодирование учетных данных, даже если они находятся в отдельном файле.

Представьте, что в начале вашего проекта вы использовали свой личный пароль в качестве учетных данных для подключения к базе данных. Когда приходит время открывать исходный код для вашего кода, вы можете забыть о первых днях, но история коммитов GitHub определенно этого не сделала! Даже при отсутствии каких-либо проблем с безопасностью, если кто-то, обслуживающий ваш проект, должен тщательно изучить все ваши словари кода / данных, чтобы найти соответствующие фрагменты, в которых можно обновить учетные данные, он не будет в восторге от вашей работы.

Короче говоря, жестко запрограммированные учетные данные громоздки и потенциально опасны.

С другой стороны, есть еще один способ изящно и безопасно хранить учетные данные вашей базы данных: использовать переменные среды. Независимо от того, используете ли вы Windows или систему на основе Unix, вы всегда можете установить эти переменные и скрыть их в своей системе, и они определенно более конфиденциальны, чем использование скриптов или файлов конфигурации.

Кроме того, при развертывании в контейнере (например, в Docker) вы можете легко установить переменные среды и не беспокоиться об их изменении между развертываниями. Например, при переключении из тестовой среды в производственную все конфигурации базы данных могут быть разными, но вы можете изменить конфигурацию контейнера, не изменяя базовый код. Отметьте здесь, нужна ли вам подсказка о том, как изменить переменные среды в вашей ОС.

Вот наши основные предыдущие фрагменты кода со ссылками на переменные среды (см. Биты os.environ):

import cx_Oracle
import logging
import os
class oracle_connection(object):
    """oracle db connection"""
    def __init__(self, connection_string=os.environ["CONN"]):
        self.connection_string = connection_string
        self.connector = None
    def __enter__(self):
        self.connector = cx_Oracle.connect(self.connection_string)
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_tb is None:
            self.connector.commit()
        else:
            self.connector.rollback()
        self.connector.close()

def db_connector(func):
    def with_connection_(*args, **kwargs):
        conn_str = os.environ["CONN"]
        cnn = cx_Oracle.connect(conn_str)
        try:
            rv = func(cnn, *args, **kwargs)
        except Exception:
            cnn.rollback()
            logging.error("Database connection error")
            raise
        else:
            cnn.commit()
        finally:
            cnn.close()
        return rv
    return with_connection_

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

_________________________________________________________________

Если вам понравился этот пост в блоге, узнайте больше о нашей работе, подпишитесь на нас в социальных сетях (Twitter, LinkedIn и Facebook) или присоединяйтесь к нам на наших бесплатных ежемесячных вебинарах Академии .