gKrokod / webServer

Web server with posgresql + persisten + handlepattern
BSD 3-Clause "New" or "Revised" License
0 stars 0 forks source link

Тестовые данные и миграции #2

Closed roman-bodavskiy closed 3 weeks ago

roman-bodavskiy commented 2 months ago

Вообще как правило для заполнения БД тестовыми данными используются sql файлы, но в принципе можно прописать данные и в хаскеле, это ошибкой не считается. Но это все должно быть оформлено в виде миграций. Допустим, мы хотим добавить столбец lastName в таблицу User, и для всех существующих пользователей сделать его таким же как и name. Для этого должна быть простая миграция и простой способ ее применить

В твоей структуре есть функция makeAndFillTables

makeAndFillTables :: ConnectionString -> IO ()
makeAndFillTables pginfo = makeTables pginfo >> fillTables pginfo

makeTables :: ConnectionString -> IO ()
makeTables pginfo = do
  putStrLn "Drop All tables" -- all row
  -- runDataBaseWithLog pginfo dropAll
  runDataBaseWithOutLog pginfo dropAll
  putStrLn "Make Tables in data base"
  runDataBaseWithOutLog pginfo $ runMigration migrateAll
  -- runDataBaseWithLog pginfo $ runMigration migrateAll
  pure ()

Но она удаляет все таблицы и создает их заново. А как добавить столбец к существующей таблице, не потеряв данные?

  1. Для работы с базой можно использовать любые библиотеки, главное — самим создавать и поддерживать миграции. В идеале, в готовом проекте по миграциям должна прослеживаться история изменения таблиц в процессе разработки - то есть дожна быть не только инициализирующая миграция (разве что вы сразу всё красиво спроектировали:)), но и миграции для добавления новых таблиц, изменения названий и типов полей и т.д. Миграции — код, который задает структуру базе (то есть создание таблиц, изменение, переименование, удаление полей и тд). Обязательно держите в голове, что после того, как проект выпускается в продакшен, база на проде заполняется данными, которые ни в коем случае нельзя потерять. Но требование менять структуру базы регулярно появляется за время развития проекта, и надо уметь развивать базу без потери данных. Для этого нужно как можно тщательней формализовать все изменения, которые вы проводите над базой от версии к версии.

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

roman-bodavskiy commented 2 months ago

Я нашел у тебя пункт по поводу миграций

Before starting the server, please set the cCreateAndFillTable parameter in the /config/db.cfg configuration file as follows::
"cCreateAndFillTable": [] 

Во первых не очень понятный формат в виде пустого списка [], это скорей всего так FromJSON инстанс работает не очень корректно. Лучше уже использовать True/False

Но главное вот:

  whenMakeTables config $
    Logger.writeLog "Create and fill tables in the database"
      >> BB.makeAndFillTables (connectionString config)

whenMakeTables :: (Applicative f) => ConfigDataBase -> f () -> f ()
whenMakeTables = when . isJust . cCreateAndFillTable

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

gKrokod commented 2 months ago

Во первых не очень понятный формат в виде пустого списка [], это скорей всего так FromJSON инстанс работает не очень корректно. Лучше уже использовать True/False

Тут я как раз пытался избежать True/False, используя более богатую систему типов. Навеяно "Boolean blindness" (например, https://runtimeverification.com/blog/code-smell-boolean-blindness )

data ConfigDataBase = MkConfigDataBase
  { cHostDB :: T.Text,
    cPortDB :: T.Text,
    cUserDB :: T.Text,
    cNameDB :: T.Text,
    cPasswordDB :: T.Text,
    cLimitData :: Int,
    cPortServer :: Int,
    cLogLvl :: Log,
    cCreateAndFillTable :: Maybe DoIt
  }
  deriving stock (Show, Generic)
  deriving anyclass (ToJSON, FromJSON)

data DoIt = DoIt
  deriving stock (Show, Generic)
  deriving anyclass (ToJSON, FromJSON)
gKrokod commented 2 months ago

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

Да. Так удобно было все тестировать. Но ведь в документации написано, чтобы миграцию запустили один раз, а далее изменили конфиг, чтобы такого не произошло. Я могу просто убрать удаление таблиц и оставить наполнение, - это будет правильное решение? Или надо что-то иное?

gKrokod commented 2 months ago

Но она удаляет все таблицы и создает их заново. А как добавить столбец к существующей таблице, не потеряв данные?

Мне нужно написать дополнительную функцию, которая добавляет столбец к существующей таблице? Тут я не до конца понимаю задачу.

roman-bodavskiy commented 2 months ago

Тут я как раз пытался избежать True/False, используя более богатую систему типов. Навеяно "Boolean blindness" (например, https://runtimeverification.com/blog/code-smell-boolean-blindness )

Да, это я понял, но в конфиге у тебя это отбражается как пустой список, что не очень понятно "cCreateAndFillTable": [] Нужно либо добавить второй конструктор DoIt = DoIt | Skip, либо подправить инстанс, чтобы этот тип корректно отображался

roman-bodavskiy commented 2 months ago

Мне нужно написать дополнительную функцию, которая добавляет столбец к существующей таблице? Тут я не до конца понимаю задачу.

Ну типа того. Миграции это что-то типа системы контроля версий для базы данных. Допустим у тебя проект уже в продакшне и есть несколько тестовых юзеров user1, user2, user3, которых ты задал в своей миграции, и еще несколько реальных юзеров: Вася, Петя, Катя. Мы хотим добавить столбец lastName в таблицу User, и для всех существующих пользователей сделать его таким же как и name. Твоя миграция очевидно для этих целей не подходит, так как она удаляет все таблицы и создает их заново. То есть реальные юзеры Вася, Петя, Катя будут стерты.Она подходит только для начальной инициализации базы данных с нуля

Обычно миграции это пронумерованные sql файлы Например мы можем в первом файле добавить столбец к нашей таблице, а во втором сделать lastName таким же как и name. Можно поместить конечно и в один файл, это просто пример того, что в миграциях важен порядок файлов, мы не можем обновить столбец раньше чем его создали

migrations/0001-user-last-name-create-column.sql
migrations/0002-update-user-last-name.sql
...

При запуске команды, мы проверяем проверяем папку migrations, допустим мы находим пять миграций. Проверяем, какие из них уже были применены ранее (для этого можно создать отдельную таблицу с номером миграции). Допустим были применены ранее 3 миграции, тогда мы применяем только 4 и 5. Если были применены уже 5 миграций, то вообще ничего не делаем. Таким образом мы добавили новый столбец, обновили его, и все наши юзеры на месте, никакие данные не потеряны.

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

roman-bodavskiy commented 2 months ago

Да. Так удобно было все тестировать. Но ведь в документации написано, чтобы миграцию запустили один раз, а далее изменили конфиг, чтобы такого не произошло. Я могу просто убрать удаление таблиц и оставить наполнение, - это будет правильное решение? Или надо что-то иное?

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