Запись/чтение вложенных ObservableList из приложения JavaFX

Я пишу небольшое приложение JavaFx, где основной класс содержит ObservableArryList пользователей. У этих пользователей есть ObservableList учетных записей, а у этих учетных записей есть ObservableList транзакций и так далее...

Вот диаграмма классов: Диаграмма классов

Я хотел бы сохранить, а затем прочитать данные приложения в/создать файл.

Я уже пытаюсь сохранить его, реализуя интерфейс Serializable во всех своих классах, но, видимо, вы не можете сериализовать ObservableList.

Я также пытался сохранить его в файле Json с помощью Gson или в виде файла XML с помощью JAXB, но ни один из них не сохранял списки рекурсивно.

Итак, мой вопрос: кто-нибудь знает, как я могу сохранить все объекты, которые в настоящее время находятся в моем приложении, а затем снова загрузить их?

EDIT: я реализовал подход к хранению на основе JAXB, предоставленный jewelsea, и теперь сохранение/загрузка данных работает отлично.


person UniqUnicorn    schedule 08.12.2016    source источник


Ответы (1)


Рекомендации по общему подходу к проектированию

Для вашей проблемы я бы предпочел использовать базу данных вместо сериализации. Есть из чего выбрать, в зависимости от ваших потребностей. Для небольшой встроенной базы данных разумным выбором будет что-то вроде H2. Здесь приведен пример интеграции JavaFX и H2.

Для сохранения вы можете использовать прямой JDBC или JPA. Для существенного приложения JPA будет лучше. Для небольшого приложения достаточно JDBC. Если вы используете JPA, вы можете интегрировать его с классами на основе свойств JavaFX, как определено в статьях, связанных с Объединить свойства JavaFX и объекты JPA (БЕЗ СМЕШАННОГО РЕЖИМА) и этот Пример JavaFX плюс JPA. Однако вы можете захотеть разделить объекты свойств модели представления JavaFX и использовать DAO шаблон за вашу настойчивость. Разделение объектов дает вам немного больше гибкости в дизайне и реализации вашего приложения, но нарушает DRY принципы. Однако это компромисс, поскольку результирующие объекты лучше соблюдают принцип единой ответственности.

Определите отдельные таблицы для каждой из ваших сущностей (пользователей, счетов, получателей, транзакций). Назначьте каждой записи объекта уникальный ключ идентификатора. Используйте отношения, чтобы связать ссылки на элементы. которые вы сохранили в своих ObservableLists.

Если вы хотите получить доступ к базе данных из удаленных мест и не можете открыть к ней прямое портовое соединение, вам потребуется предоставить службу на сервере, который предоставляет данные (например, сервер на основе REST, который выполняет доступ к базе данных). и предоставляет необходимые данные в виде JSON через HTTP, к которым ваш клиент JavaFX обращается через клиент REST, а затем обрабатывает ответы на вызовы REST в клиентские структуры данных на основе свойств JavaFX). Такие реализации быстро становятся большим объемом работы :-)

Вероятно, мне не следовало отвечать на этот вопрос, так как вопрос (или моя его интерпретация) слишком широк с точки зрения принципов StackOverflow, но, надеюсь, информация здесь будет вам полезна.

Конкретный ответ на основе дополнительной информации

На самом деле у меня уже есть веб-приложение на основе весенней загрузки с DAO и Hibernate, которое работает нормально, и это приложение JavaFX планируется подключить к этому веб-приложению. Мне просто нужны эти локально сохраненные файлы в качестве небольшой «демонстрации» программы, если в настоящее время нет подключения к Интернету.

Попался, это имеет общий смысл. Раньше я интегрировал JavaFX с SpringBoot, но, к сожалению, я не могу публично публиковать исходный код этих реализаций.

Для вашей демонстрационной программы должно быть достаточно персистентности через JAXB или Jackson. Makery предоставляет хороший пример сохраняемости на основе JAXB для JavaFX.

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

Пример хранения данных на основе JAXB

Этот пример основан на идеях из учебника Makery JavaFX. Чтобы лучше понять это, обратитесь к учебнику. Постоянство вложенного наблюдаемого списка достигается с использованием концепций из: JAXB: Как маршалировать объекты в списках?.

Ключом к решению является этот фрагмент кода в классе User. Он предоставляет список учетных записей в виде вложенного ObservableList и предоставляет стандартный метод доступа accounts() для получения ObservableList в соответствии с соглашениями JavaFX. Он также предоставляет метод getAccounts() и setAccounts(), который копирует ObservableList и из него в стандартный список Java и помечает геттер аннотациями JAXB @Xml..., чтобы позволить JAXB обрабатывать сериализацию и десериализацию учетных записей, связанных с пользователями.

private final ObservableList<Account> accounts = FXCollections.observableArrayList();

public ObservableList<Account> accounts() { return accounts; }

@XmlElementWrapper(name="accounts")
@XmlElement(name = "account")
public List<Account> getAccounts() {
    return new ArrayList<>(accounts);
}

public void setAccounts(List<Account> accounts) {
    this.accounts.setAll(accounts);
}

UserAccountPersistence.java

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.prefs.Preferences;
import java.util.stream.Collectors;

public class UserAccountPersistence {

    private ObservableList<User> users = FXCollections.observableArrayList();

    public UserAccountPersistence() throws JAXBException, IOException {
        File dbFile = getDatabaseFilePath();
        if (dbFile == null) {
            setDatabaseFilePath(new File(System.getProperty("user.home") + "/" + "user-account.xml"));
            dbFile = getDatabaseFilePath();
        }

        if (!dbFile.exists()) {
            createTestData();
            saveData(dbFile);
        } else {
            loadData(dbFile);
        }

        System.out.println("Persisted Data: ");
        System.out.println(
                Files.lines(dbFile.toPath())
                        .collect(Collectors.joining("\n"))
        );
        System.out.println("Database File: " + dbFile);
    }

    private void createTestData() {
        users.add(new User("Hans", "Muster"));
        users.add(new User("Ruth", "Mueller"));
        users.add(new User("Heinz", "Kurz"));

        users.get(0).accounts().addAll(
                new Account(10),
                new Account(20)
        );

        users.get(2).accounts().addAll(
                new Account(15)
        );
    }

    public File getDatabaseFilePath() {
        Preferences prefs = Preferences.userNodeForPackage(UserAccountPersistence.class);
        String filePath = prefs.get("filePath", null);
        if (filePath != null) {
            return new File(filePath);
        } else {
            return null;
        }
    }

    public void setDatabaseFilePath(File file) {
        Preferences prefs = Preferences.userNodeForPackage(UserAccountPersistence.class);
        if (file != null) {
            prefs.put("filePath", file.getPath());
        } else {
            prefs.remove("filePath");
        }
    }

    public void loadData(File file) throws JAXBException {
        JAXBContext context = JAXBContext
                .newInstance(UserListWrapper.class);
        Unmarshaller um = context.createUnmarshaller();

        UserListWrapper wrapper = (UserListWrapper) um.unmarshal(file);

        users.clear();
        users.addAll(wrapper.getPersons());

        setDatabaseFilePath(file);
    }

    public void saveData(File file) throws JAXBException {
        JAXBContext context = JAXBContext
                .newInstance(UserListWrapper.class);
        Marshaller m = context.createMarshaller();
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

        UserListWrapper wrapper = new UserListWrapper();
        wrapper.setPersons(users);

        m.marshal(wrapper, file);

        setDatabaseFilePath(file);
    }

    public static void main(String[] args) throws JAXBException, IOException {
        UserAccountPersistence userAccountPersistence = new UserAccountPersistence();
    }
}

UserListWrapper.java

import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "users")
public class UserListWrapper {

    private List<User> persons;

    @XmlElement(name = "user")
    public List<User> getPersons() {
        return persons;
    }

    public void setPersons(List<User> persons) {
        this.persons = persons;
    }
}

User.java

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class User {
    private final StringProperty id;
    private final StringProperty firstName;
    private final StringProperty lastName;

    private final ObservableList<Account> accounts = FXCollections.observableArrayList();

    public User() {
        this(UUID.randomUUID().toString(), null, null);
    }

    public User(String firstName, String lastName) {
        this(UUID.randomUUID().toString(), firstName, lastName);
    }

    public User(String id, String firstName, String lastName) {
        this.id = new SimpleStringProperty(id);
        this.firstName = new SimpleStringProperty(firstName);
        this.lastName = new SimpleStringProperty(lastName);
    }

    public String getId() {
        return id.get();
    }

    public void setId(String id) {
        this.id.set(id);
    }

    public StringProperty idProperty() {
        return id;
    }

    public String getFirstName() {
        return firstName.get();
    }

    public void setFirstName(String firstName) {
        this.firstName.set(firstName);
    }

    public StringProperty firstNameProperty() {
        return firstName;
    }

    public String getLastName() {
        return lastName.get();
    }

    public void setLastName(String lastName) {
        this.lastName.set(lastName);
    }

    public StringProperty lastNameProperty() {
        return lastName;
    }

    public ObservableList<Account> accounts() { return accounts; }

    @XmlElementWrapper(name="accounts")
    @XmlElement(name = "account")
    public List<Account> getAccounts() {
        return new ArrayList<>(accounts);
    }

    public void setAccounts(List<Account> accounts) {
        this.accounts.setAll(accounts);
    }

}

Account.java

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

import java.util.UUID;

public class Account {
    private final StringProperty id;
    private final IntegerProperty balance;

    public Account() {
        this(UUID.randomUUID().toString(), 0);
    }

    public Account(int balance) {
        this(UUID.randomUUID().toString(), balance);
    }

    public Account(String id, int balance) {
        this.id = new SimpleStringProperty(id);
        this.balance = new SimpleIntegerProperty(balance);
    }

    public String getId() {
        return id.get();
    }

    public void setId(String id) {
        this.id.set(id);
    }

    public StringProperty idProperty() {
        return id;
    }

    public int getBalance() {
        return balance.get();
    }

    public IntegerProperty balanceProperty() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance.set(balance);
    }
}

Вывод

$ cat /Users/jewelsea/user-account.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<users>
    <user>
        <accounts>
            <account>
                <balance>10</balance>
                <id>a17b8244-5d3a-4fb4-a992-da26f4e14917</id>
            </account>
            <account>
                <balance>20</balance>
                <id>f0b23df5-3cc0-418c-9840-633bc0f0b3ca</id>
            </account>
        </accounts>
        <firstName>Hans</firstName>
        <id>078dad74-ea9d-407d-9be5-d36c52c53b0d</id>
        <lastName>Muster</lastName>
    </user>
    <user>
        <accounts/>
        <firstName>Ruth</firstName>
        <id>78513f1b-75ee-4ca9-a6f0-444f517e3377</id>
        <lastName>Mueller</lastName>
    </user>
    <user>
        <accounts>
            <account>
                <balance>15</balance>
                <id>77c4fd3c-5f7a-46cf-a806-da1e6f93baab</id>
            </account>
        </accounts>
        <firstName>Heinz</firstName>
        <id>651d9206-42a5-4b76-b89e-be46dce8df74</id>
        <lastName>Kurz</lastName>
    </user>
</users>
person jewelsea    schedule 08.12.2016
comment
Большое спасибо за ваш быстрый ответ! На самом деле у меня уже есть веб-приложение на основе весенней загрузки с DAO и Hibernate, которое работает нормально, и это приложение JavaFX планируется подключить к этому веб-приложению. Мне просто нужны эти локально сохраненные файлы в качестве небольшой демонстрации программы, если в настоящее время нет подключения к Интернету. Я обязательно посмотрю примеры H2, которые вы предоставили! - person UniqUnicorn; 09.12.2016
comment
Я обновил вопрос новым примером JAXB, который вы предоставили! - person UniqUnicorn; 09.12.2016
comment
@UniqUnicorn Я не уверен, почему десортировка не работает для вашей версии кода. Я дважды проверил код в своем ответе, и десортировка работает для кода, который я предоставил. Я не знаю причину разницы в поведении, которую вы испытываете для своей реализации. - person jewelsea; 09.12.2016
comment
Довольно круто. Использовал этот небольшой паттерн с JSON (Джексон) и сразу заработал. - person Wesos de Queso; 25.01.2020