Python с postgres с использованием именованных переменных и массовых вставок

#python #postgresql #transactions

#python #postgresql #транзакции

Вопрос:

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

  • Windows 7 64bit
  • Python 3.2
  • Postgresql 9.1
  • psycopg2

Вот мой сценарий: я конвертирую данные из одной базы данных (oracle) в строки xml и вставляю эти данные в новую базу данных (postgres). Это большой набор данных, поэтому я пытаюсь оптимизировать некоторые из моих вставок. Большую часть этих данных я рассматриваю как объекты библиотечного типа, поэтому у меня есть таблица библиотеки, а затем таблицы для моих метаданных xml и содержимого xml, поля для этих данных представляют собой текстовые типы в базе данных. Я извлекаю данные из oracle, а затем создаю словари данных, которые мне нужно вставить. У меня есть 3 оператора insert, первая вставка создает запись в таблице библиотеки с использованием последовательного идентификатора, и этот идентификатор необходим для связи в следующих двух запросах, которые вставляют xml в таблицы метаданных и содержимого. Вот пример того, о чем я говорю:

 for inputKey in libDataDict.keys():
  metaString = libDataDict[inputKey][0]
  contentString = libDataDict[inputKey][1]
  insertLibDataList.append({'objIdent':"%s" % inputKey, 'objName':"%s" % inputKey, objType':libType})
  insertMetadataDataList.append({'objIdent':inputKey,'objMetadata':metaString}) 
  insertContentDataList.append({'objIdent':inputKey, 'objContent':contentString})

dataDict['cmsLibInsert'] = insertLibDataList
dataDict['cmsLibMetadataInsert'] = insertMetadataDataList
dataDict['cmsLibContentInsert'] = insertContentDataList

sqlDict[0] = {'sqlString':"insert into cms_libraries (cms_library_ident, cms_library_name, cms_library_type_id, cms_library_status_id) 
              values (%(objIdent)s, %(objName)s, (select id from cms_library_types where cms_library_type_name = %(objType)s), 
              (select id from cms_library_status where cms_library_status_name = 'active'))", 'data':dataDict['cmsLibInsert']}

sqlDict[1] = {'sqlString':"insert into cms_library_metadata (cms_library_id, cms_library_metadata_data) values 
              ((select id from cms_libraries where cms_library_ident = %(objIdent)s), $$%(objMetadata)s$$)", 
              'data':dataDict['cmsLibMetadataInsert']}

sqlDict[2] = {'sqlString':"insert into cms_library_content (cms_library_id, cms_library_content_data) values 
              ((select id from cms_libraries where cms_library_ident = %(objIdent)s), $$%(objContent)s$$)", 
              'data':dataDict['cmsLibContentInsert']}

bulkLoadData(myConfig['pgConn'], myConfig['pgCursor'], sqlDict)
  

Проблема, с которой я сталкиваюсь, заключается в том, что когда я запускаю первый запрос (sqlDict[0] ) и выполняю вставку, все работает нормально, пока я делаю это отдельно и фиксирую, прежде чем запускать следующие два. В идеале я хотел бы, чтобы все эти запросы были в одной транзакции, но это не удается, потому что он не может найти идентификатор из таблицы cms_libraries для 2-го и 3-го запросов.
Вот мой текущий код вставки:

 def bulkLoadData(dbConn, dbCursor, sqlDict):
 try:
   libInsertSql = sqlDict.pop(0)
   dbSql = libInsertSql['sqlString']
   data = libInsertSql['data']
   dbCursor.executemany(dbSql, data)
   dbConn.commit()
   for sqlKey in sqlDict:
     dbSql = sqlDict[sqlKey]['sqlString']
     data = sqlDict[sqlKey]['data']
     dbCursor.executemany(dbSql, data)

   dbConn.commit()
  

Ранее я добавлял значения в запрос, а затем выполнял запрос для каждой вставки. Когда я это делаю, я могу поместить все это в одну транзакцию, и она находит сгенерированный идентификатор, и все в порядке. Я не понимаю, почему он не находит идентификатор, когда я выполняю массовую вставку с помощью executemany()? Есть ли способ выполнить массовую вставку и два других запроса в одной транзакции?

Я читал эту документацию и искал stackoverflow и Интернет, но не нашел ответа на свою проблему: документы pyscopg, а также документы postgres: Postgresql string docs

Любая помощь, предложения или комментарии будут оценены. Спасибо, Митч

Комментарии:

1. По-видимому, я недостаточно четко сформулировал свой вопрос, чтобы получить какие-либо ответы, поэтому я удалил 2-ю часть вопроса и задам ее отдельно. Я надеюсь, что кто-нибудь может помочь мне понять это…

2. Можете ли вы активировать SET [log_statement](http://www.postgresql.org/docs/current/interactive/runtime-config-logging.html) = all и проверить журнал сервера PostgreSQL, какие команды он фактически получил и в каком порядке?

3. Я был в путешествии, поэтому я медленно отвечаю, но спасибо за комментарий, это хорошее предложение. Я попробую это и посмотрю, покажет ли это, что происходит.

Ответ №1:

Здесь у вас есть два варианта. Либо сгенерируйте идентификаторы извне (что позволяет выполнять массовые вставки), либо сгенерируйте их из serial (что означает, что вам нужно выполнять вставки с одной записью). Я думаю, что довольно просто разобраться с генерацией внешнего идентификатора и массовой загрузкой (хотя я бы рекомендовал вам взглянуть на инструмент ETL, а не вручную что-то кодировать на python). Если вам нужно извлекать идентификаторы из последовательного файла, вам следует рассмотреть подготовленные инструкции на стороне сервера.

Ваш первый оператор должен выглядеть следующим образом:

 dbCursor.execute("""
PREPARE cms_lib_insert (bigint, text, text) AS 
INSERT INTO cms_libraries (cms_library_ident, cms_library_name, cms_library_type_id, cms_library_status_id)
VALUES ($1, $2,
    (select id from cms_library_types where cms_library_type_name = $3), 
    (select id from cms_library_status where cms_library_status_name = 'active')
)
RETURNING cms_library.id
""")
  

Вы запустите это один раз, во время запуска. Затем вы захотите выполнить следующую инструкцию EXECUTE на уровне для каждого входа.

 dbCursor.execute("""
EXECUTE cms_lib_insert(%(objIndent)s, %(objName)s, %(objType)s)
""", {'objIndent': 345, 'objName': 'foo', 'objType': 'bar'))
my_new_id = dbCursor.fetchone()[0]
  

Это вернет сгенерированный серийный идентификатор. В дальнейшем я настоятельно рекомендую вам отказаться от шаблона, которому вы в настоящее время следуете, пытаясь абстрагироваться от взаимодействия с базой данных (ваш подход sqlDict) и перейти к очень прямому шаблону кодирования (здесь ваш враг — умный, это усложняет настройку производительности).).

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

 BLOCK_SIZE = 500
while not_done:
   dbCursor.begin()
   for junk in irange(BLOCK_SIZE):
       dbCursor.execute("EXECUTE cms_lib_insert(...)")
       cms_lib_id = dbCursor.fetchone()[0]     # you're using this below.
       dbCursor.executemany("EXECUTE metadata_insert(...)")
       dbCursor.executemany("EXECUTE library_insert(...)")
   dbCursor.commit()
  

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

Комментарии:

1. Спасибо за ваш подробный ответ, это огромная помощь, и я буду внедрять эти изменения в наш производственный код.