В части 4 этой серии статей мы полностью подключим интерфейс к базе данных, включая поддержку всех операций CRUD (создание, чтение, обновление и удаление).

Главы серии

  • Часть 1: Введение и начальная настройка: Maven, Spring и JPA / Backend (база данных)
  • Часть 2: Средний уровень: предоставление наших данных с помощью службы REST
  • Часть 3: Front End - Начальная реализация
  • Часть 4: Внешний интерфейс - функции сетки и CRUD (создание, чтение, обновление и удаление)

Вступление

Во время написания этой части блога я обнаружил две ошибки в уже проделанной работе.

Если вы до сих пор следили за этой серией, пожалуйста, взгляните на обе части 1 и 2, чтобы увидеть внесенные изменения (ищите Updated!).

Урок здесь? Пишите тесты, прежде чем писать код, и пишите их почаще!

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

В этой части мы будем строить на простой сетке, которая у нас есть до сих пор, и подключать наши простые операции CRUD к бэкэнду.

Простое встроенное редактирование

Начнем с простейшей операции CRUD - обновлений. В частности, мы начнем со встроенного редактирования «простых» значений - значений, которые содержат одно значение (т. Е. Не являются сложными объектами).

Из значений, которые мы в настоящее время отображаем, только name и country попадают в эту категорию, поэтому давайте начнем с них.

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

Позже нам нужно будет сделать то же самое для sport значений, поэтому давайте создадим новую службу, которая будет извлекать для нас статические данные (данные, которые не будут изменены пользователем).

Сначала нам нужно создать новый Spring Controller на среднем уровне, который мы можем использовать для доступа к статическим данным. Я не буду углубляться в этот сервис, поскольку мы уже рассмотрели использование Controllers в предыдущей части этой серии:

@CrossOrigin(origins = "http://localhost:4200")
@RestController
public class StaticDataController {
    private CountryRepository countryRepository;
    private SportRepository sportRepository;
    public StaticDataController(CountryRepository countryRepository,
                                SportRepository sportRepository) {
        this.countryRepository = countryRepository;
        this.sportRepository = sportRepository;
    }
    @GetMapping("/countries")
    public Iterable<Country> getCountries() {
        return countryRepository.findAll();
    }
    @GetMapping("/sports")
    public Iterable<Sport> getSports() {
        return sportRepository.findAll();
    }
}

Здесь мы просто получаем все возможные значения для country и sport соответственно.

Затем давайте создадим Angular Service, который будет вызывать эту конечную точку отдыха:

@Injectable()
export class StaticDataService {
    private apiRootUrl = 'http://localhost:8090';
    private countriesUrl = this.apiRootUrl + '/countries';
    private sportsUrl = this.apiRootUrl + '/sports';
    static alphabeticalSort() {
        return (a: StaticData, b: StaticData) => a.name.localeCompare(b.name);
    }
    constructor(private http: Http) {
    }
    countries(): Observable<Country[]> {
        return this.http.get(this.countriesUrl)
                        .map((response: Response) => response.json())
                        .catch(this.defaultErrorHandler());
    }
    sports(): Observable<Sport[]> {
        return this.http.get(this.sportsUrl)
                        .map((response: Response) => response.json())
                        .catch(this.defaultErrorHandler());
    }
    private defaultErrorHandler() {
        return (error: any) => Observable.throw(error.json().error || 'Server error');
    }
}

Опять же, мы не будем вдаваться в подробности, поскольку в предыдущей части мы уже рассмотрели доступ к данным из конечной точки REST. Мы добавили сюда статический метод (alphabeticalSort), который мы будем использовать позже, но это просто служебный метод, используемый для целей отображения.

Теперь, когда у нас это есть, мы можем начать вносить изменения в вашу сетку - сначала нам нужно сделать эти два столбца доступными для редактирования. Поскольку мы хотим, чтобы пользователи выбирали из предопределенного списка в случае country, поэтому мы будем использовать здесь richSelect редактор ag-Grid, поставляемый здесь (вы также можете использовать редактор select, если вы ' используя бесплатную версию ag-Grid).

richSelect требует, чтобы его значения были указаны заранее, поэтому давайте обратимся ко всем странам в подрядчике - как только они у нас появятся, мы можем определить наши определения столбцов с этими предоставленными значениями:

// inject the athleteService & staticDataService
constructor(private athleteService: AthleteService,
            staticDataService: StaticDataService) {
    staticDataService.countries().subscribe(
            countries => this.columnDefs = this.createColumnDefs(countries),
            error => console.log(error)
        );
}

С нашим новым введенным StaticDataService мы подписываемся на countries Observable и после завершения вызова this.createColumnDefs(countries) со значениями, которые мы только что получили.

// create some simple column definitions
private createColumnDefs(countries: Country[]) {
    return [
        {
            field: 'name',
            editable: true
        },
        {
            field: 'country',
            cellRenderer: (params) => params.data.country.name,
            editable: true,
            cellEditor: 'richSelect',
            cellEditorParams: {
                values: countries,
                cellRenderer: (params) => params.value.name
            }
        },
        {
            field: 'results',
            valueGetter: (params) => params.data.results.length
        }
    ]
}

Давайте разберем этот метод:

editable: true

Это делает столбец редактируемым.

cellRenderer: (params) => params.data.country.name,

Поскольку country data, которые мы получим от StaticDataService, является сложным значением (оно имеет как id, так и nameproperties), нам нужно сообщить ag-Grid, как это отображать.

Здесь есть множество вариантов, но простое решение - предоставить cellRenderer, как мы сделали здесь.

cellRenderer будут предоставлены данные текущей строки (вместе с другой полезной информацией - см. Документацию для получения дополнительной информации), чтобы мы могли получить доступ к данным строки (params.data), затем к столбцу страны (params.data.country) и, наконец, к значению country: params.data.country.name.

cellEditor: 'richSelect'

Мы собираемся использовать редактор выбора ag-Grid richSelect, который позволяет настраивать значения в раскрывающемся списке. Мы не делаем этого в первом проходе, но сделаем это в следующем разделе.

cellEditorParams: {
    values: countries,
    cellRenderer: (params) => params.value.name
}

Как минимум richSelect требует, чтобы значения отображались, что здесь делает values.

Однако, как и выше, список country представляет собой сложные объекты, поэтому нам нужно сообщить ag-Grid, какое значение на самом деле отображать.

На этот раз параметры содержат только значения из richSelect, но идея та же - доступ к отображаемому значению countryname.

Теперь мы можем редактировать столбцы name и country:

Сохранение наших правок

Пока мы фактически ничего не делаем с нашими правками - давайте подключимся к изменениям и сохраним их в базе данных.

<ag-grid-angular style="width: 100%; height: 500px;"
         class="ag-fresh"
         [columnDefs]="columnDefs"
         [rowData]="rowData"
         suppressHorizontalScroll
         (gridReady)="onGridReady($event)"
         (cellValueChanged)="onCellValueChanged($event)">
</ag-grid-angular>

Здесь мы подключаемся к cellValueChanged - мы будем использовать этот перехватчик для сохранения измененных значений.

onCellValueChanged(params: any) {
    this.athleteService.save(params.data)
                       .subscribe(
                           savedAthlete => {
                               console.log('Athlete Saved');
                               this.setAthleteRowData();
                           },
                           error => console.log(error)
                       )
}

Этот метод будет вызван после изменения значения ячейки. Мы вызываем AthleteService для сохранения данных строки (params.data содержит данные строки) и при успешном сохранении обновляем данные строки еще раз.

Что замечательно в этом, так это то, что для save метода требуется Athlete, а поскольку данные нашей строки состоят из массива Athlete, это сопоставление будет работать бесплатно.

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

Вы можете проверить свои изменения, выполнив принудительное обновление - вы должны обнаружить, что новые значения находятся в сетке - старые значения были перезаписаны.

Примечание. Поскольку мы используем базу данных в памяти, изменения не будут сохраняться постоянно. Если вы остановите и перезапустите приложение Java, исходные значения будут отображаться еще раз.

Мы пока не можем редактировать столбец Results - это просто сумма базовых данных. Мы вернемся к этому позже.

Обратите внимание, что мы обновляем все данные сетки каждый раз, когда изменяется значение ячейки - это очень неэффективно. Мы рассмотрим возможность использования функции ag-Grid Обновление, чтобы перерисовывать только измененные строки, а не всю сетку.

Удаление записи

Теперь давайте рассмотрим удаление записи - это, вероятно, самая простая из операций CRUD для реализации.

Мы позволим пользователям выбрать одну или несколько записей, а затем предоставим кнопку, при нажатии которой будут удалены выбранные записи.

Во-первых, давайте включим rowSelection в сетке:

<ag-grid-angular style="width: 100%; height: 500px;"
         class="ag-fresh"
         [columnDefs]="columnDefs"
         [rowData]="rowData"
         rowSelection="multiple"
         suppressRowClickSelection
         suppressHorizontalScroll
         (gridReady)="onGridReady($event)"
         (cellValueChanged)="onCellValueChanged($event)">
</ag-grid-angular>

Здесь rowSelection="multiple" позволяет выбрать одну или несколько строк, а suppressRowClickSelection предотвращает выбор строк щелчком по строке.

Какие? Зачем нам предотвращать выбор при щелчках по строке? Как мы будем выбирать строку?

А как насчет того, чтобы добавить флажок к каждой строке, сделав это нашим механизмом выбора строк?

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

Добавить флажок в столбец легко - все, что нам нужно сделать, это добавить checkboxSelection: true в определение столбца:

{
    field: 'name',
    editable: true,
    checkboxSelection: true
}

Затем давайте добавим кнопку, которую пользователь может использовать для удаления выбранных строк - мы также добавим служебный метод, который отключит кнопку удаления, если строки не выбраны:

<div>
    <button (click)="deleteSelectedRows()" [disabled]="!rowsSelected()">
            Delete Selected Row
    </button>
</div>
<div>
    <ag-grid-angular style="width: 100%; height: 500px;"
                 class="ag-fresh"
                 [columnDefs]="columnDefs"
                 [rowData]="rowData"
                 rowSelection="multiple"
                 suppressRowClickSelection
                 suppressHorizontalScroll
                 (gridReady)="onGridReady($event)"
                 (cellValueChanged)="onCellValueChanged($event)">
    </ag-grid-angular>
</div>

И соответствующие реализации методов в нашем компоненте Grid:

rowsSelected() {
    return this.api && this.api.getSelectedRows().length > 0;
}
deleteSelectedRows() {
    const selectRows = this.api.getSelectedRows();
    // create an Observable for each row to delete
    const deleteSubscriptions = selectRows.map((rowToDelete) => {
        return this.athleteService.delete(rowToDelete);
    });
    // then subscribe to these and once all done, refresh the grid data
    Observable.forkJoin(...deleteSubscriptions)
              .subscribe(results => this.setAthleteRowData())
}

rowsSelected() вернет истину, если api готов и если есть выбранные строки.

deleteSelectedRows() - захватываем все выделенные строки, затем удаляем каждую строку по очереди. Наконец, мы вызываем this.setAthleteRowData(), чтобы обновить сетку текущими данными из базы данных.

Это довольно наивная и неэффективная реализация - необходимо сделать два очевидных улучшения:

  • Пакетное удаление - пусть мидл / бэкэнд сделает всю работу
  • Используйте функцию ag-Grid Обновление, чтобы мы перерисовывали только измененные / удаленные строки, а не всю сетку.

Мы рассмотрим эти улучшения в более поздней версии.

Создание / вставка записи

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

Во-первых, давайте создадим новый компонент, который мы будем использовать как для создания новых записей, так и для обновления записей позже.

Мы воспользуемся Angular CLI, чтобы сделать это за нас:

ng g c athlete-edit-screen

g - сокращение для генерирования, а c - для компонента.

Давайте заменим содержимое шаблона () следующим образом:

<div>
    <div style="display: inline-block">
        <div style="float: left">
            Name: <input [(ngModel)]="name"/>
        </div>
        <div style="float: left; padding-left: 10px">
            Country:
            <select [(ngModel)]="country">
                <option disabled selected>Country...</option>
                <option *ngFor="let country of countries" [ngValue]="country">
                    {{ country.name }}
                </option>
            </select>
        </div>
    </div>
    <div>
        <button (click)="insertNewResult()">Insert New Result</button>
        <ag-grid-angular style="width: 100%; height: 200px;"
                         class="ag-fresh"
                         [columnDefs]="columnDefs"
                         [rowData]="rowData"
                         (gridReady)="onGridReady($event)"
                         (rowValueChanged)="onRowValueChanged($event)">
        </ag-grid-angular>
    </div>
    <div>
        <button (click)="saveAthlete()" [disabled]="!isValidAthlete()" style="float: right">
            Save Athlete
        </button>
    </div>
</div>

Может показаться, что здесь много чего происходит, но в итоге мы получим что-то вроде этого:

Вкратце, этот экран покажет нам полную информацию о Athlete. В верхнем ряду у нас есть поля name и country, а ниже мы перечислим различные result, если таковые имеются.

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

Соответствующий класс компонентов распределяется следующим образом:

constructor(staticDataService: StaticDataService) {
    staticDataService.countries().subscribe(
        countries => this.countries = countries.sort(StaticDataService.alphabeticalSort()),
        error => console.log(error)
    );
    staticDataService.sports().subscribe(
        sports => {
            // store reference to sports, after sorting alphabetically
            this.sports = sports.sort(StaticDataService.alphabeticalSort());
            // create the column defs
            this.columnDefs = this.createColumnDefs(this.sports)
        },
        error => console.log(error)
    );
}

Здесь мы получаем статические данные sport и country. Данные country будут использоваться в соответствующем раскрывающемся списке на экране редактирования, а данные sport будут переданы в код определения столбца (так же, как мы это делали в компоненте сетки выше), чтобы они были доступны в столбце richSelect.

insertNewResult() {
    // insert a blank new row, providing the first sport as a default in the sport column
    const updates = this.api.updateRowData(
        {
            add: [{
                sport: this.sports[0]
            }]
        }
    );
    this.api.startEditingCell({
        rowIndex: updates.add[0].rowIndex,
        colKey: 'age'
    });
}

Это то, что выполняется, когда пользователь хочет выбрать новый result. Мы создаем новую пустую запись (по умолчанию sport richSelect для первого доступного вида спорта) и просим сетку создать для нас новую строку, используя this.api.updateRowData.

Тот же механизм может использоваться как для обновлений, так и для удалений - см. Соответствующую документацию Обновление для получения дополнительной информации об этой мощной функциональности, которую предлагает Grid.

После того, как новая строка была вставлена, сетка предоставляет нам информацию о новой строке. Используя это, мы можем автоматически начать редактирование новой строки (используя this.api.startEditingCell) - в этом случае мы начинаем редактировать первую доступную ячейку: age.

@Output() onAthleteSaved = new EventEmitter<Athlete>();
saveAthlete() {
    const athlete = new Athlete();
    athlete.name = this.name;
    athlete.country = this.country;
    athlete.results = [];
    this.api.forEachNode((node) => {
        const {data} = node;
        athlete.results.push(<Result> {
            id: data.id,
            age: data.age,
            year: data.year,
            date: data.date,
            bronze: data.bronze,
            silver: data.silver,
            gold: data.gold,
            sport: data.sport
        });
    });
    this.onAthleteSaved.emit(athlete);
}

Это, вероятно, самая важная часть этого класса: когда пользователь щелкает Save Athlete, вызывается метод saveAthlete. Мы создаем объект Athlete и заполняем его деталями наших форм. Затем мы сообщаем родительскому компоненту (в данном случае GridComponent), что сохранение завершено, передавая новый Athlete.

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

Полный компонент AthleteEditScreenComponent здесь не описан, в первую очередь потому, что остальная его часть представляет собой либо стандартную функциональность Angular (то есть простую привязку свойств), либо уже описывалась ранее в этой части блога выше.

Чтобы завершить этот раздел, давайте посмотрим, как родительский компонент GridComponent обрабатывает операцию сохранения.

Мы обновили наш шаблон, добавив в него новый GridComponent, который отображается только в том случае, если мы установили флаг editInProgress:

<div>
    <button (click)="insertNewRow()" [disabled]="editInProgress">Insert New Row</button>
    <button (click)="deleteSelectedRows()" [disabled]="!rowsSelected()">Delete Selected Row</button>
</div>
<div>
    <ag-grid-angular style="width: 100%; height: 500px;"
                 class="ag-fresh"
                 [columnDefs]="columnDefs"
                 [rowData]="rowData"
                 rowSelection="multiple"
                 suppressRowClickSelection
                 suppressHorizontalScroll
                 (gridReady)="onGridReady($event)"
                 (cellValueChanged)="onCellValueChanged($event)">
    </ag-grid-angular>
</div>
<ng-template [ngIf]="editInProgress">
    <app-athlete-edit-screen (onAthleteSaved)="onAthleteSaved($event)"></app-athlete-edit-screen>
</ng-template>

Обратите внимание, что GridComponent ожидает завершения сохранения здесь:

(onAthleteSaved)="onAthleteSaved($event)"

А в нашем GridComponent мы обрабатываем операцию сохранения из AthleteEditScreenComponent следующим образом:

onAthleteSaved(savedAthlete: Athlete) {
    this.athleteService.save(savedAthlete)
                       .subscribe(
                           success => {
                               console.log('Athlete saved');
                               this.setAthleteRowData();
                           },
                           error => console.log(error)
                       );
    this.editInProgress = false;
}

Здесь мы передаем новый Athlete в наш AthleteService для сохранения, а один сохраненный перезагружает данные сетки.

Как и выше, это обновление всех данных сетки очень неэффективно - опять же, мы рассмотрим, как улучшить это в более поздней итерации.

Наконец, мы еще раз скрываем экран редактирования.

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

Запись обновления / редактирования

Наша последняя часть операции CRUD, которую необходимо завершить, - это часть обновления. Здесь мы возьмем существующую Athlete запись и позволим пользователю редактировать ее.

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

Во-первых, мы обновим наш GridComponent - мы удалим функцию редактирования ячейки за ячейкой, которую мы добавили ранее, и заменим ее подходом на основе всей записи (строки).

<div>
    <button (click)="insertNewRow()" [disabled]="editInProgress">Insert New Row</button>
    <button (click)="deleteSelectedRows()"
            [disabled]="!rowsSelected() || editInProgress">
            Delete Selected Row
    </button>
</div>
<div>
    <ag-grid-angular style="width: 100%; height: 500px;"
                 class="ag-fresh"
                 [columnDefs]="columnDefs"
                 [rowData]="rowData"
                 rowSelection="multiple"
                 suppressRowClickSelection
                 suppressHorizontalScroll
                 suppressClickEdit
                 (gridReady)="onGridReady($event)"
                 (rowDoubleClicked)="onRowDoubleClicked($event)">
    </ag-grid-angular>
</div>
<ng-template [ngIf]="editInProgress">
    <app-athlete-edit-screen [athlete]="athleteBeingEdited"
                         (onAthleteSaved)="onAthleteSaved($event)">
    </app-athlete-edit-screen>
</ng-template>

Выше есть ряд изменений - давайте рассмотрим их:

suppressClickEdit

Это свойство предотвращает переход ячейки в режим редактирования при двойном щелчке по ней, что является поведением по умолчанию для редактируемой ячейки. Однако в нашем приложении мы хотим использовать отдельный компонент, чтобы сделать это за нас.

(rowDoubleClicked)="onRowDoubleClicked($event)

Мы удалили обработчик редактирования ячеек, который у нас был раньше, и заменили его обработчиком на основе строк. Мы будем использовать это событие для запуска нашего AthleteEditScreenComponent.

<app-athlete-edit-screen [athlete]="athleteBeingEdited"

Когда мы отображаем наш AthleteEditScreenComponent, мы также передадим Athlete.

При создании нового Athlete эта ссылка будет null, но когда мы редактируем существующую строку, мы передаем строку (или Athlete, поскольку каждая строка данных - это Athlete) для редактирования с помощью нашего AthleteEditScreenComponent.

private athleteBeingEdited: Athlete = null;
onRowDoubleClicked(params: any) {
    if (this.editInProgress) {
        return;
    }
    this.athleteBeingEdited = <Athlete>params.data;
    this.editInProgress = true;
}

Этот обработчик будет вызываться, когда пользователь дважды щелкает строку. Если редактирование уже выполняется, мы проигнорируем запрос, но если нет, мы сохраним строку, на которую дважды щелкнули - это будет передано в AthleteEditScreenComponent через привязку свойства.

Наконец, мы устанавливаем свойство editInProgress, которое будет вызывать отображение AthleteEditScreenComponent.

onAthleteSaved(savedAthlete: Athlete) {
    this.athleteService.save(savedAthlete)
                       .subscribe(
                           success => {
                               console.log('Athlete saved');
                               this.setAthleteRowData();
                           },
                           error => console.log(error)
                       );
    this.athleteBeingEdited = null;
    this.editInProgress = false;
}

Здесь мы внесли одно небольшое изменение в onAthleteSaved - мы сбрасываем athleteBeingEdited на null после завершения операции сохранения, чтобы он был готов к следующей операции вставки или редактирования.

Теперь переключая наше внимание на AthleteEditScreenComponent, мы должны внести несколько небольших изменений, чтобы учесть существующий Athlete, переданный для редактирования:

@Input() athlete: Athlete = null;

Здесь мы сообщаем Angular, что ожидаем передачи Athlete. Здесь всегда будет передаваться что-то - либо действительный Athlete в случае операции редактирования, либо null в случае операции вставки.

ngOnInit() {
    if (this.athlete) {
        this.name = this.athlete.name;
        this.country = this.athlete.country;
        this.rowData = this.athlete.results.slice(0);
    }
}

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

saveAthlete() {
    const athlete = new Athlete();
    athlete.id = this.athlete ? this.athlete.id : null;

И, наконец, мы еще раз проверяем, редактируем ли мы Athlete, когда использование нажимает "Сохранить". Если да, то мы также устанавливаем AthleteID, чтобы, когда он достигнет нашей службы REST, мы обновили существующую запись, а не вставляли новую.

Вот и все! Если мы теперь дважды щелкнем по строке, мы увидим предварительно заполненный AthleteEditScreenComponent, готовый для редактирования.

Применив небольшой стиль, мы получим следующее:

Оптимистическая блокировка

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

Это довольно удобный механизм, но у него есть свои недостатки. Если мы попытаемся отредактировать / удалить что-то, что до нас редактировал другой пользователь, мы получим сообщение об ошибке. Это снова нормально, и в реальном приложении вы бы написали код для этого - либо сообщите пользователю, что это произошло, либо выполните проверку до того, как пользователь попытается это сделать (или и то, и другое).

Эта оптимистическая блокировка в значительной степени обрабатывается Spring JPA за счет использования управления версиями. Каждый раз, когда запись изменяется, версия будет увеличиваться, а результат будет сохранен в БД. Когда выполняется попытка изменения, Spring JPA проверяет текущую версию на соответствие версии в базе данных - если они совпадают, редактирование может продолжаться, но в противном случае возникнет ошибка.

Это управление версиями осуществляется путем добавления следующего к Entity, которые мы хотим обновить - в нашем случае нам нужно сделать это только для Athlete и Result (Country и Sport нельзя редактировать).

@Entity
public class Athlete {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Version()
    private Long version = 0L;

Здесь аннотация @Version позволяет Spring JPA знать, что мы хотим обновить этот класс - все остальное происходит автоматически!

Раздел Break!

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

Код на этот момент можно найти в ветке Part-4a в Github, но вы также можете увидеть основные классы и т. Д. Ниже.

Домашняя растяжка - финальные доработки

Мы почти на этом закончили - мы внесем еще два набора улучшений в то, что у нас есть на данный момент.

Первый касается внешнего вида / удобства использования, а второй - сокращения ненужных перерисовок Grid (и сетевых вызовов).

Удобство использования - компонент редактирования наложения

В настоящий момент AthleteEditScreenComponent отображается под приложением (в частности, под сеткой). Это работает отлично, но визуально было бы лучше, если бы он появлялся над сеткой - создавалось ощущение, что AthleteEditScreenComponent был частью самой сетки.

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

Во-первых, давайте запишем координаты сетки:

<ag-grid-angular style="width: 100%; height: 500px;"
         class="ag-fresh"
         #grid
         [columnDefs]="columnDefs"
         [rowData]="rowData"

Здесь мы берем ссылку на сетку с #grid - мы будем использовать ее, чтобы получить размеры и координаты сеток:

@ViewChild('grid', {read: ElementRef}) public grid;
private containerCoords: {} = null;
private updateContainerCoords() {
    this.containerCoords = {
        top: this.grid.nativeElement.offsetTop,
        left: this.grid.nativeElement.offsetLeft,
        height: this.grid.nativeElement.offsetHeight,
        width: this.grid.nativeElement.offsetWidth
    };
}

Здесь мы храним размеры и координаты сетки - мы обновим эту информацию непосредственно перед отображением AthleteEditScreenComponent (непосредственно перед вставкой новой строки или при двойном щелчке / редактировании существующей строки).

Мы передаем эту информацию AthleteEditScreenComponent:

<ng-template [ngIf]="editInProgress">
    <app-athlete-edit-screen [athlete]="athleteBeingEdited"
                         [containerCoords]="containerCoords"
                         (onAthleteSaved)="onAthleteSaved($event)">
    </app-athlete-edit-screen>
</ng-template>

Давайте теперь переключимся на AthleteEditScreenComponent и посмотрим, как мы его используем.

В нашем шаблоне AthleteEditScreenComponent мы сохраним ссылку на основной блок и привяжем его к верхней и левой координатам:

<div class="input-panel" [style.width]="width" [style.top]="top" [style.left]="left" #panel>
<div style="display: inline-block">

Затем в самом компоненте AthleteEditScreenComponent:

// to position this component relative to the containing component
@Input() containerCoords: any = null;
@ViewChild('panel', {read: ElementRef}) public panel;
private width: any;
private left: any;
private top: any;
ngOnInit() {
    this.setPanelCoordinates();
    ... rest of the method
}
private setPanelCoordinates() {
    // make our width 100pixels smaller than the container
    this.width = (this.containerCoords.width - 100);
    // set our left position to be the container left position plus half the difference in widths between this
    // component and the container, minus the 15px padding
    this.left = Math.floor(this.containerCoords.left + (this.containerCoords.width - this.width) / 2 - 15) + 'px';
    // set our left position to be the container top position plus half the difference in height between this
    // component and the container
    this.top = Math.floor(this.containerCoords.top + (this.containerCoords.height - this.panel.nativeElement.offsetHeight) / 2) + 'px';
    // add the px suffix back in (omitted above so that maths can work)
    this.width = this.width + 'px'
}

После этого наш AthleteEditScreenComponent будет позиционироваться в сетке, что улучшит визуальное восприятие:

Улучшения производительности

Как мы видели ранее, мы извлекаем все данные сетки и перерисовываем всю сетку каждый раз, когда вносим изменения (при создании, обновлении или удалении). Это излишне неэффективно, и грид может помочь нам уменьшить количество отрисовок сети и грид за счет использования его функции Обновить.

Во-первых, нам нужно определить функционал, который позволит сетке однозначно идентифицировать каждую строку, чтобы находить в ней данные.

Мы делаем это, определяя getRowNodeId функцию и привязываясь к ней:

<ag-grid-angular style="width: 100%; height: 500px;"
         class="ag-fresh"
         ...reset of grid definition
         [getRowNodeId]="getRowNodeId"

И в самом нашем компоненте Grid:

getRowNodeId(params) {
    return params.id;
}

У наших данных есть очевидный атрибут, который можно использовать для однозначной идентификации каждой строки - атрибут ID.

Теперь мы можем использовать функцию Grids api.updateRowData только для обновления / перерисовки измененных строк, а не всей сетки.

Когда мы удаляем строку (и):

deleteSelectedRows() {
    const selectRows = this.api.getSelectedRows();
    // create an Observable for each row to delete
    const deleteSubscriptions = selectRows.map((rowToDelete) => {
        return this.athleteService.delete(rowToDelete);
    });
    // then subscribe to these and once all done, update the grid
    Observable.forkJoin(...deleteSubscriptions)
              .subscribe(
                  results => {
                      // only redraw removed rows...
                      this.api.updateRowData(
                          {
                              remove: selectRows
                          }
                      );
                  }
              );
}

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

Когда мы создаем или вставляем строки:

onAthleteSaved(athleteToSave: Athlete) {
    this.athleteService.save(athleteToSave)
                       .subscribe(
                           savedAthlete => {
                               console.log('Athlete saved', savedAthlete.name);
                               const added = [];
                               const updated = [];
                               if (athleteToSave.id) {
                                   updated.push(savedAthlete);
                               } else {
                                   added.push(savedAthlete);
                               }
                               this.api.updateRowData(
                                   {
                                       add: added,
                                       update: updated
                                   }
                               );
                           },
                           error => console.log(error)
                       );
    this.athleteBeingEdited = null;
    this.editInProgress = false;
}

Метод onAthleteSaved вызывается, когда мы создаем новую запись или когда редактируем существующую.

Чтобы сообщить сетке, добавляются или обновляются строки, мы проверим наличие Athlete ID - если он присутствует, мы выполняем обновление, а если нет, мы выполняем добавление.

С помощью этой информации мы можем сообщить сетке, чтобы добавить или обновить обработанную строку:

this.api.updateRowData(
    {
        add: added,
        update: updated
    }
);

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

Мы только поверхностно коснулись того, что мы можем делать с помощью Grid здесь - я предлагаю вам взглянуть на документацию Обновление для получения дополнительной информации о том, что возможно.

Резюме

Что ж, это была огромная часть этой серии. Мы рассмотрели много материала и почву, но в итоге получили хороший пример того, как вы можете написать свое собственное приложение CRUD, используя ag-Grid.

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

Я надеюсь, что это было полезно - если у вас есть какие-либо вопросы, пожалуйста, дайте мне знать ниже.

Узнайте больше о AG Grid - высокопроизводительной JavaScript Data Grid.

AG Grid поддерживает несколько фреймворков: Angular, Vue, React, поэтому вы можете выбрать подходящий фреймворк для своих нужд. Сосредоточьтесь на написании лучшего приложения, которое вы можете, оставьте таблицы данных и взаимодействие с сеткой AG Grid.