https://visualhunt.com/photo/176316/

Formularze w Play cz. 2 – DataBinder

Konkurs „Daj się poznać 2017” trwa, jest początek ósmego tygodnia prac nad moim projektem Sterownika domowego, a ja … spoglądam z zazdrością na niektóre realizacje konkursowe:) Serio, niektóre są już całkiem mocno zaawansowane. Siedem tygodni blogowania już za mną, kupa czasu, a nie mam jeszcze nawet połowy głównych funkcjonalności! Piszę o tym, bo mam pewne wnioski. Na pewno jeśli będzie w przyszłości kolejna edycja konkursu, to w niej wystartuję, bo to niezły mobilizator i daje kopa do nauki poprzez działanie. Poza tym więcej czasu powinienem poświęcać na kodowanie, mniej na blogowanie. Być może za bardzo staram się wejść w szczegóły w opisie tego co zrobiłem i powstają za długie posty. Dobra, koniec narzekania. Do roboty!

W poprzednim wpisie opisałem sposób utworzenia prostego formularza, umożliwiającego operacje na obiektach typu Room. Przykład był trywialny, bo w modelu nie było żadnych kluczy obcych. Trochę inaczej sytuacja wygląda w klasie Sensor, gdzie są dwa pola odwołujące się do obiektów innych klas, co w bazie danych tworzy relacje pomiędzy tabelami. Mowa tu o polu sensorType oraz room.

@Entity
@Table(name = "sensor")
public class Sensor extends Model {
    // (...)
 
    @Column(name = "type_id")
    @Constraints.Required
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "type_id")
    public SensorType sensorType;
 
    @Column(name = "room_id")
    @Constraints.Required
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "room_id")
    public Room room;
 
    // (...)
}

Wróćmy do początku, czyli do tego co chcę osiągnąć. Moim celem na tym etapie prac nad projektem jest dodanie funkcjonalności konfiguracji elementów zainstalowanych w systemie, a dokładniej dodawania oraz edycji sensorów. W najprostszym rozwiązaniu przekłada się to do stworzenia formularza umożliwiającego wykonanie takich operacji, jednak teraz podszedłem do tego trochę inaczej niż dla modelu pomieszczenia. Konkretniej mówiąc, szablony definiujące wygląd znaczników typu <select> napisałem z wykorzystaniem helperów, jak wcześniej zapowiadałem.

@(form: Form[Sensor], label: String = "CHANGEME", help: String = "", rooms: List[String])
 
<div class="form-group">
    <div class="col-lg-8">
        @helper.select(form("room"), helper.options(rooms), 'class -> "form-control", 
        '_showConstraints -> false, '_label -> label,'_id -> form("room").id, 'value -> form("room").name)
        <span class="help-block">@help</span>
        <span class="help-block">@{form("room").error.map { error => error.message }}</span>
    </div>
</div>

Po stronie kontrolera kod wygląda analogicznie jak w przypadku dodawania/edycji pomieszczenia, no może z wyjątkiem innego uporządkowania i poukrywania niektórych operacji w prywatnych metodach. Ogólna zasada jednak jest identyczna. Jest metoda wyświetlająca formularz, sparametryzowany w zależności od tego czy użytkownik tworzy czy edytuje sensor, są również metody wywoływane, gdy przekazywane są dane do serwera metodą POST, w których następuje powiązanie danych z formularza do modelu oraz walidacja ich poprawności.

public Result addEditSensorShowForm(String option, int sensorId) {
        prepareLabelsForForm();
        this.sensorForm = formFactory.form(Sensor.class);
        Sensor sensor;
        if ("edit".equals(option)) {
            sensor = sensorService.getSensorById(sensorId);
            sensorForm = sensorForm.fill(sensor);
        } else { // add new room
            sensor = new Sensor();
        }
        return ok(views.html.sensors.addEditSensorForm.render(sensorForm, option, sensor.id,
                sensorTypesNames, roomsNames, null));
    }
 
    public Result addSensor() {
        prepareLabelsForForm();
        sensorForm = sensorForm.bindFromRequest();
        if (sensorForm.hasErrors()) {
            return showFormErrors("new", 0);
        } else {
            return addSensorAction();
        }
    }
 
    public Result editSensor(int sensorId) {
        prepareLabelsForForm();
        sensorForm = sensorForm.bindFromRequest();
        if (sensorForm.hasErrors()) {
            return showFormErrors("edit", sensorId);
        } else {
            return editSensorAction(sensorId);
        }
    }

W przypadku formularza dodającego pomieszczenie w tym miejscu był koniec implementacji, jednak w tym przypadku pojawiły się błędy. W polu powiadomień z walidatora na stronie formularza pojawiają się komunikaty error.invalid.

Formularz dodawania/edycji nowego sensora
Formularz dodawania/edycji nowego sensora

Problem (a jak niektórzy wolą – wyzwanie) związany jest z tym, że w formularzu występuje tekstowa reprezentacja obiektu, co jest naturalne, bo użytkownik musi zrozumieć co ma wybrać. Jednak przy próbie wiązania pola formularza do obiektu, występuje problem zgodności typów.

Rozwiązaniem w takim przypadku jest ustalenie zasad mapowania ciągu znaków z formularza, na obiekt modelu, co odbywa się w trzech krokach:

  1. Utworzenie klasy implementującej Provider<Formatters>, w której znajduje się implementacja konwersji
  2. Utworzenie klasy modułu, dziedziczącej po AbstractModule
  3. Aktywacja modułu w pliku application.conf

W pierwszej kolejności utworzyłem serce mechanizmu konwersji, czyli klasę dostawcy. W konstruktorze wstrzykuję zależności wymagane w dalszym kroku do konwersji. Klasa FormattersProvider implementuje metodę get(), w której rejestrowane są obiekty typu SimpleFormatter<T>, utworzone jako klasy anonimowe. W formularzu pojawiają się dwa błędy i dotyczą pól sensorType oraz room, zatem konieczne było określenie dwóch metod konwersji dla każdego z tych typów, co ma miejsce w metodach parse(). Dla modelu pomieszczenia zamieniam argument funkcji typu String (roomName) na obiekt typu Room, natomiast dla typu sensora następuje konwersja również z typu String (sensorTypeLabel) na obiekt typu SensorType.

class FormattersProvider implements Provider<Formatters> {
 
    private final MessagesApi messagesApi;
 
    private final RoomService roomService;
 
    private final SensorTypeService sensorTypeService;
 
    @Inject
    public FormattersProvider(RoomService roomService, SensorTypeService sensorTypeService, MessagesApi messagesApi) {
        this.messagesApi = messagesApi;
        this.roomService = roomService;
        this.sensorTypeService = sensorTypeService;
    }
 
    @Override
    public Formatters get() {
        Formatters formatters = new Formatters(messagesApi);
 
        formatters.register(Room.class, new SimpleFormatter<Room>() {
            @Override
            public Room parse(String roomName, Locale locale) throws ParseException {
                Room room = roomService.getRoomByName(roomName);
                return room;
            }
 
            @Override
            public String print(Room room, Locale locale) {
                return room.name;
            }
 
        });
 
        formatters.register(SensorType.class, new Formatters.SimpleFormatter<SensorType>() {
            @Override
            public SensorType parse(String sensorTypeLabel, Locale locale) throws ParseException {
                return sensorTypeService.getSensorTypeByLabel(sensorTypeLabel);
            }
 
            @Override
            public String print(SensorType sensorType, Locale locale) {
                return sensorType.sensorType.getLabel();
            }
 
        });
        return formatters;
    }
}

W klasie FormattersModule zarejestrowałem klasę FormattersProvider. Gdy już wszystko będzie skonfigurowane, zakomentowanie linii z metodą bind() spowoduje ponowne wyświetlenie błędów.

public class FormattersModule extends AbstractModule {
 
    @Override
    protected void configure() {
        bind(Formatters.class).toProvider(FormattersProvider.class);
    }
}

Na koniec pozostała konfiguracja polegająca na aktywowaniu modułu w pliku konfiguracyjnym oraz dezaktywacja standardowego modułu FormattersModule. I tyle, problem został rozwiązany, a wyzwanie zakończone. Teoretycznie, bo w praktyce każde pozytywne rozwiązanie generuje nowe błędy:)

play.modules {
     disabled += "play.data.format.FormattersModule"
     enabled += "utils.FormattersModule"
}

Tym razem przy próbie dodania nowego sensora dostałem komunikat o próbie zapisania encji o ID, który w bazie danych już istnieje. Problemem była błędnie wstawiona adnotacja @OneToOne przy polu sensorType. Oczywiście jednego typu może być więcej sensorów, zatem należało zmienić relację na @ManyToOne.

[PersistenceException: ERROR executing DML bindLog[] error[ERROR: duplicate key value violates unique constraint "uq_sensor_sensor_type_id"\n   
Szczegóły: Key (sensor_type_id)=(2) already exists.\n   
Lokalizacja: Plik: nbtinsert.c, Procedura: _bt_check_unique, Linia: 432\n   Serwer SQLState: 23505]]

Jak widać, nie ma problemów bez wyjścia. Najczęściej pomaga dokumentacja, a ta do Play jest całkiem spoko, trochę intuicji, a jak nie to zawsze zostaje StackOverFlow albo ogólnie internety. Myślę, że w kolejnym tygodniu chciałbym dla odmiany popracować trochę nad warstwą w JS.

P.S. We wstępie pisałem, że to już ósmy tydzień konkursowy DSP2017, a właśnie zdałem sobie sprawę, że dziś mijają dokładnie 2 miesiące od opublikowania pierwszego posta na moim blogu! To jakaś tam rocznica, a tą pierwszą miesięcznicę przegapiłem. 🙂

Literatura obowiązkowa

Register a custom DataBinder
Register a custom DataBinder
Form template helpers

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *