Search code examples
javahibernatebindingvaadinvaadin8

How to bind a foreign key in Vaadin 8


I cannot resolve a problem. I have a window with TextField and I want to bind a foreign key but have no ideas what am I doing wrong. I've already read the answer here: Binding foreign key in Vaadin (EclipseLink) I decided to use Converter, but still stumble. So, there are two entities (simplified): Client and Order. An existent or a new client can have several orders, means that Order.clientID is foreign key

Client entity:

@Entity
@DynamicInsert
@DynamicUpdate
@Table(name = "Clients")
public class Client {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ClientID", updatable = false, nullable = false)
    private Long clientID;

    @Column(name = "Name", nullable = false, length = 20)
    private String name;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "clientID")
    private Set<Order> orders;

    public Long getId() { return clientID; }

    public void setId(Long clientID) { this.clientID = clientID; }

    public String getName() { return firstName; }

    public void setName(String name) { this.name = name; }
}

Order entity:

@Entity
@DynamicInsert
@DynamicUpdate
@Table(name = "Orders")
public class Order{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "OrderID", nullable = false)
    private Long orderID;

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "ClientID",
            referencedColumnName = "ClientID",
            updatable = false,
            nullable = false)
    private Client clientID;

    @Column(name = "Description", nullable = false, length = 1000)
    private String description;

    public Long getOrderID() { return orderID; }

    //public Long getClientID() { return clientID.getId(); }

    public Client getClientID() { return clientID; }

    public void setClientID(Client clientID) { this.clientID = clientID; }

    public String getDescription() { return description; }

    public void setDescription(String description) { this.description = description; }

}

and I want to bind Order.clientID to the TextField. But IDEA highlight the setter setClientID as "Cannot resolve method 'setClientID'"

public class AddOrderModalView extends Window {

    private OrderService orderService = new OrderService();
    private Order order = new Order();

    Binder<Order> binder = new Binder<>(Order.class);
    private ChangeHandler changeHandler = new ChangeHandler() {
        @Override
        public void onChange() {

        }
    };

    private FormLayout formLayout = new FormLayout();
    private TextField clientId = new TextField("Client ID");
    private TextField description = new TextField("Description");
    private Button save = new Button("Save");
    private Button cancel = new Button("Cancel");

    public AddOrderModalView() {
        super("Add a new order");

        VerticalLayout subContent = new VerticalLayout();
        subContent.setSizeFull();

        HorizontalLayout actions = new HorizontalLayout();
        actions.addComponents(save, cancel);

        formLayout.addComponents(clientId, description);

        subContent.addComponent(formLayout);
        setContent(subContent);

        save.addStyleNames(ValoTheme.BUTTON_SMALL, ValoTheme.BUTTON_PRIMARY);
        cancel.addStyleName(ValoTheme.BUTTON_SMALL);

        save.addClickListener(e -> save());
        cancel.addClickListener(e -> close());

        bindingFields();

        setModal(true);
    }

    private void bindingFields() {
        binder.forField(clientId)
                .withConverter(Long::valueOf, String::valueOf)
                .bind(Order::getClientID, Order::setClientID); //the error is here

        binder.forField(this.description)
                .withValidator(new StringLengthValidator(
                        "Please add description. The maximum length is 1000 characters", 1, 1000))
                .bind(Order::getDescription, Order::setDescription);

        binder.bindInstanceFields(this);

        binder.setBean(order);
    }

    public interface ChangeHandler {
        void onChange();
    }

    private void save() {
        if (binder.validate().isOk()) {
            orderService.persist(order);
            close();
            changeHandler.onChange();
        }
    }
}

ClientToClientIdConverter:

public class ClientToClientIdConverter implements Converter<String, Client> {
    @Override
    public Result<Client> convertToModel(String s, ValueContext valueContext) {
        return Result.error("not supported");
    }

    @Override
    public String convertToPresentation(Client client, ValueContext valueContext) {
        return Objects.toString(client.getId(), "");
    }
}

Could anyone help me with solving the problem?


Solution

  • Answer to the question: How to bind foreign key (or any other nested property) in a textfield (not what you need!)

    You can do it by providing lambda expressions to get and set the nested properties.

    TextField clientId = new TextField("Client ID");
    binder.forField(clientId)
        .withConverter(new StringToLongConverter("error message"))
        .bind(item -> item.getClient().getId(), (item, value) -> item.getClient().setId(value));
    

    This code can be cause of NullPointerExceptions if the order can have no client at this point. If that is possible, then use this instead (added checking for nullvalues):

    TextField clientId = new TextField("Client ID");
    binder.forField(clientId)
        .withConverter(new StringToLongConverter("error message"))
        .bind(
            item -> item.getClient() != null ? item.getClient.getId() : null,
            (item, value) -> {
                    if(item.getClient() != null){
                        item.getClient().setId(value);
                    }
        });
    

    Warning! Please know that manually changing the value in this textfield will change the id of it's already assigned client, and not select/assign a new Client for this Order. If it's the latter that you want, use a ComboBox instead! I'm not sure if it ever makes sense to do the first, but I answered because you asked. I am now sure that you want the latter option, so please follow the next part.


    The actual solution to your problem: It seems that you indeed need a ComboBox, because you want to select/assign a client for the order.

    So what you need is basically this:

    ComboBox<Client> clientSelection = new ComboBox<Client>("client");
    
    clientSelection.setItems(clientService.findAll()); // list/set of possible clients.
    
    // Using clients name for item captions now, but you can also use id or both
    clientSelection.setItemCaptionGenerator(Client::getName); 
    
    binder.forField(clientSelection)
                .bind(Order::getClient, Order::setClient);
    

    This way, you can select a client which then gets set as the bound orders client.