Уверен многие тру-программисты и без меня это знают, но я решился опубликовать пару реализаций циклов через while, которыми я активно пользуюсь, как автоматизатор, тестировщик и разработчик ETL.

while not all(check_list)

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

-- SQL

delete from my_shema.my_table 
      where my_column = 'my_variable'; 
commit;

insert into my_shema.my_table 
     select * 
       from my_shema.my_table@src_dblink 
      where my_column = 'my_variable';
commit;

Дополнительно, нужно полазить по схеме и выяснить зависимости между таблицами, которые могут вызывать исключения типа ORA-02291 и ORA-02292 (это когда мы не можем или удалить данные в таблице, не удалив предварительно данные из другой таблицы, или вставить, предварительно не вставив данные в другую таблицу).

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

Чтобы этого не делать, можно при срабатывании исключения БД отправлять DML-операцию в конец очереди и для этого можно использовать цикл while not all(check_list):

# python3.8
import cx_Oracle


# составляем чек-лист переноса данных, 
# если значение в списке 0, значит данные не перенесены, обратно, если 1
etl_objects = [('MY_SCHEMA', 'MY_TABLE1'), ('MY_SCHEMA', 'MY_TABLE2'), ...]
check_list = [0 for _ in range(len(etl_objects))]
i = 0
while not all(check_list):
  i = i % len(check_list)  # нужно для второго и последующих циклов
  try:
    if not check_list[i]:
      with cx_Oracle.connect(user=user,
                             password=password,
                             tns=tns,
                             encoding='utf-8') as db_conn:
        with db_conn.cursor() as cursor:
          sql_text = (f"delete from {etl_objects[i][0]}.{etl_objects[i][1]} "
                      "where my_column = 'my_variable'")
          cursor.execute(sql_text)
          cursor.execute('commit')
          sql_text = (f"insert into {etl_objects[i][0]}.{etl_objects[i][1]} "
                      "select * "
                      f"from {etl_objects[i][0]}.{etl_objects[i][1]}@src_dblink "
                      "where my_column = 'my_variable';")
          cursor.execute(sql_text)
          cursor.execute('commit')
      check_list[i] = 1
  except Exception as e:
    print(f"Сработало исключение {str(e)}: объект загрузки отправлен в конец очереди")
  i += 1
  

Понятное дело, что формально объект загрузки не отправляется в конец очереди, но цикл while будет повторяться до тех пор, пока не останется ни одного незагруженного объекта. Также понятное дело, что если сработали другие исключения, отличные от ORA-02291 и ORA-02292, то цикл рискует стать бесконечным, но этот пример подразумевает, что в остальном ограничения источника и приемника не будут мешать загрузке.

while i < len(table_trg) and j < len(table_src)

При тестировании DDL таблиц в разных базах данных может понадобиться сравнивать их наборы полей и типов данных, которые могут отличаться. То есть семантика таблиц одинаковая, а реализация отличается. Например, обе таблицы хранят один и тот же набор основных данных, но названия полей погут отличаться, также для полей с одинаковыми названием и назначением могут отличаться типы данных или ограничения типа данных.

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

-- SQL

select column_name, data_type, data_length ,..., 
  from all_tab_columns
 where owner = 'MY_SHEMA' and table_name = 'MY_TABLE';

select column_name, data_type, data_length ,..., 
  from all_tab_columns@src_dblink
 where owner = 'MY_SHEMA' and table_name = 'MY_TABLE';

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

# python3.8
import cx_Oracle


with cx_Oracle.connect(user=user,
                       password=password,
                       tns=tns,
                       encoding='utf-8') as db_conn:
  with db_conn.cursor() as cursor:
    sql_text = ("select column_name, data_type, data_length "
                "from all_tab_columns "
                "where owner = 'MY_SHEMA' and table_name = 'MY_TABLE' "
                "order by column_name")
    cursor.execute(sql_text)
    table_trg = cursor.fetchall()
    sql_text = ("select column_name, data_type, data_length "
                "from all_tab_columns@src_dblink "
                "where owner = 'MY_SHEMA' and table_name = 'MY_TABLE' "
                "order by column_name")
    cursor.execute(sql_text)
    table_src = cursor.fetchall()

i, j = 0, 0
tmp_table_trg, tmp_table_src = [], []
while i < len(table_trg) and j < len(table_src):
  if table_trg[i][0] == table_src[j][0]:  # сравниваем названия полей
    tmp_table_trg.append(table_trg[i])
    tmp_table_src.append(table_src[j])
    i += 1
    j += 1
  elif table_trg[i][0] < table_src[j][0]:
  # если название поля меньше, то в правой таблице поля с таким же названием нет
    tmp_table_trg.append(table_trg[i])
    tmp_table_src.append((None, None, None))  # добавляем в правую таблицу "пустую" строку
    i += 1
  else:
    tmp_table_trg.append((None, None, None))  # добавляем в левую таблицу "пустую" строку
    tmp_table_src.append(table_src[j])
    j += 1

diffs = list(filter(lambda x: x[0] != x[1], zip(tmp_table_trg, tmp_table_src)))

Вот такие две реализации циклов через while я использую в своей работе.

Первый пример ( while not all() ) помогает гонять циклы по чек-листам, а второй ( while i < len(table_trg) and j < len(table_src) ), помогает выравнивать списки кортежей.

Чтобы мой код выше можно было проверить, если у вас нет возможности делать запросы к БД, можно переписать мой код на работу с таблицами csv.

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


  1. digtatordigtatorov
    26.10.2024 14:09

    Чет как будто не проще просто экспортировать бд файлом .sql, а затем просто воссоздать из файла бд?

    Ну или там другой какой-нибудь формат

    А что делать, если в цикле ты удаляешь данные из базы, а они не вставляются в новую, ты же теряешь данные насовсем, разве это безопасно?


    1. mrmcmva Автор
      26.10.2024 14:09

      Чет как будто не проще просто экспортировать бд файлом .sql, а затем просто воссоздать из файла бд?

      Ну или там другой какой-нибудь формат

      А зачем экспорт/импорт, если линки между БД настроены?

      А что делать, если в цикле ты удаляешь данные из базы, а они не вставляются в новую, ты же теряешь данные насовсем, разве это безопасно?

      При работе ETL всегда есть база-источник с исходными данными и база приемник. Данные на источнике в этом случае не теряются, так как из нее они только селектятся.


  1. IvanBodhidharma
    26.10.2024 14:09

    Самый малоиспользуемый цикл, как по мне, это do..while. За свою двадцатилетнюю карьеру он мне раза 2 понадобился, когда с ним вот прямо проще было. Даже уже и не припомню конкретные ситуации...


    1. APXEOLOG
      26.10.2024 14:09

      Удобно для работы с paginated response


      1. spaceatmoon
        26.10.2024 14:09

        Да, это прям топчик когда нужен цикл с пагинацией.