Проект Java Servlet + jsp. Часть 4. Запись данных в базу. Метод doPost()

Продолжаю цикл статей, посвящённый моему пет-проекту с Java Servlet, Hibernate и jsp страницами.

На сайте каждую неделю я публикую топ40 — свой импровизированный чарт лучших песен (так как блог у меня в большой части всё-таки музыкальный). Не знаю, как это делают другие люди с подобным увлечением (да-да, есть много людей, которые компилируют свои чарты в музыкальном коммьюнити), но я решил хранить каждый выпуск в базе. Чтобы не вводить каждую неделю туда данные вручную, я и работаю над этим проектом. Про базу данных, общие идеи и структуру проекта уже писал во второй части. Про то, как настраивать базовый проект на сервлетах писал в первой части.

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

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

Код самой формы:

    <form method="POST" action="/chartadd">
        <table id="chart-table" style="width:600px;">
            <thead>
            <tr>
                <th style="display:none;">id</th>
                <th style="text-align: center">Mov</th>
                <th style="text-align: center">Artists</th>
                <th style="width:30px;"></th>
                <th style="text-align: center">Title</th>
            </tr>
            </thead>
            <tbody>
            <c:forEach begin="1" end="40" var="val">
                <tr name="position">
                    <td style="display:none;"><input name="idSong[]" type="text" /></td>
                    <td style="text-align: center"><c:out value="${val}"/></td>
                    <td><input style="width:100%;" name="artists[]" type="text" /></td>
                    <td style="text-align: center"> - </td>
                    <td><input style="width:100%;" name="name[]" type="text" /></td>
                </tr>
            </c:forEach>
            </tbody>
        </table>
        <div style="text-align: center;">
            <input style="margin-top:10px;" type="submit" value="Сохранить" />
        </div>
    </form>

Так как в моём чарте фиксированно сорок записей, я создаю все сорок строчек таблицы в цикле. Пользователь видит поля с именем артистов и названием песни, а также есть скрытое поле с id песни в базе.

При вводе на видимые поля предлагается автозаполнение, если песня уже существует в базе (про автозаполнение я писал в прошлой статье). При выборе подсказки все три поля в строке заполняются. Таким образом, в таблице оказываются новые песни, которых ещё нет в базе (у них поле idSong пустое) и уже существующие поля с заполненным idSong.

Заполненный чарт выглядит так:

По нажатию на кнопку Сохранить обращаемся к адресу /chartadd. Вызывается метод doPost сервлета ChartAddServlet:

@WebServlet(name = "chartAdd", value = "/chartadd")
public class ChartAddServlet extends HttpServlet {

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        <...>
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        String ids[] = request.getParameterValues("idSong[]");
        String artists[] = request.getParameterValues("artists[]");
        String name[] = request.getParameterValues("name[]");

        ChartService.formChart(ids, name, artists);

        response.sendRedirect("/");
        response.flushBuffer();
    }
}

Так как всех полей у нас по 40, то для получения массива строк используем метод getParameterValues. И нужно не забыть квадратные скобки рядом с названием поля. Если бы поле было всего одно, то для получения одной строки использовали бы getParameter.

Вся логика по созданию нового чарта размещена на уровне сервисов в классе ChartService. Метод formChart создаёт новый чарт в таблице Chart, и потом в цикле обрабатывает все 40 записей. Он добавляет в таблицу Song все новые песни, обновляет существующие (нужно обновить пиковую позицию и число недель в чарте — поля peak и weeks) и записывает все сорок позиций в таблицу Position.

public static void formChart(String ids[], String name[], String artists[]) {
        long lastChartId = ChartDAOImpl.getLastId(); //получаю айди последнего чарта 

        Chart chart = new Chart(); //заполняю объект Чарт
        chart.setDate(ChartService.getNewChartDate());
        chart.setId(lastChartId+1);
        ChartDAOImpl.save(chart); //сохраняю чарт в базу

        Position position;
        Position_PK position_pk;
        Song song;
        for (int i = 0; i < ids.length; i++) {
            final int pos = i + 1; //позиции - натуральные числа и начинаются с 1, а не с 0
            song = new Song();
            try {
                Long id = Long.parseLong(ids[i]); //если id песни не было, выкинется исключение
                song = SongDAOImpl.getById(id);
                song.setWeeks(song.getWeeks() + 1); //увеличиваю число недель в чарте на 1
                if (song.getPeak() > pos) {
                    song.setPeak(pos); //если пик у песни больше текущей позиции, то изменяю значение
                }
                SongDAOImpl.update(song); //обновляем запись в базе
            } catch (Exception ex) { //заполняем новую песню
                song.setArtists(artists[i]);
                song.setName(name[i]);
                song.setWeeks(1);
                song.setPeak(pos);
                song.setId(SongDAOImpl.getLastId()+1);
                SongDAOImpl.save(song); //сохраняем новую песню
            }

            position_pk = new Position_PK(); //заполняю составной первичный ключ 
            position_pk.setChart(chart);
            position_pk.setSong(song);

            position = new Position(); //заполняю позицию
            position.setPosition(pos);
            position.setPk(position_pk); 

            //если в прошлом чарте была эта песня, то заполняю поле LastWeek 
            Position prevSongPosition = PositionDAOImpl.getPositionForSong(song.getId(), lastChartId);
            if (prevSongPosition != null) { 
                position.setLastWeek(prevSongPosition.getPosition());
            }

            PositionDAOImpl.save(position); //сохраняю позицию в базу
        }
    }

Так как при создании базы (давным давно) я не сделал первичные ключи таблиц с автоинкрементом, то я вручную задаю все поля, включая поля id. Если автоинкремент включён, то поле id для создания нового объекта перед вызовом метода save заполнять не нужно.

Логики довольно много, но нас интересуют только главные методы save и update. Взглянем на них ещё раз на примере SongDAOImpl (полностью про модели и работу с базой этого проекта написано во второй части цикла)

public class SongDAOImpl {

    public static long save(Song result) {
        Session session = HibernateUtil.getSessionFactory().openSession();
        session.beginTransaction();
        long id = (Long) session.save(result);
        session.getTransaction().commit();
        session.close();
        return id;
    }

    public static void update(Song result) {
        Session session = HibernateUtil.getSessionFactory().openSession();
        session.beginTransaction();
        session.update(result);
        session.getTransaction().commit();
        session.close();
    }

    <...>
}

Готово.

Напоследок покажу ещё, как вместо формы во фронте использовать ajax. Бэк можно оставить таким же, а вот во фронте в (джава)скрипте собрать вручную все нужные данные и передать их в вызове в бэк:

$('[name="someButton"]').on('click', function () {

        var idSong = [],
            artists = [],
            name = [];
                
        $("#chart-table tbody tr").each(function () {
            idSong.push($(this).find('[name="idSong[]"]').val());
            artists.push($(this).find('[name="artists[]"]').val());
            name.push($(this).find('[name="name[]"]').val());
        });

        $.ajax({
            url: "/chartAdd",
            type: "POST",
            dataType: 'json',
            data: {
                 idSong: idSong, artists: artists, name: name
            },
            complete: function () {
                top.location.href = "/";
            }
         });
 });

Здесь при нажатии на некий элемент someButton сначала собираем в цикле данные из таблицы. А потом передаём данные в бэк и указываем редирект на главную страницу по адресу / в разделе complete.

Мой проект почти закончен. На данный момент я могу смотреть архив чартов и создавать новые чарты с удобным автовводом. Осталось навести красоту со стилями и всё. После этого можно будет залить приложение в интернет и развернуть там базу.