探索前沿技术
      展示技术风采

SpringBoot实战: 集成ag-Grid构建CRUD应用程序 – 第4部分

在本系列的第4部分中,将前端连接到数据库,包括支持所有CRUD操作(创建,读取,更新和删除)。

系列章节
第1部分:简介和初始设置:Maven,Spring和JPA /后端(数据库)
第2部分:中间层:使用REST服务公开我们的数据
第3部分:前端 – 初步实施
第4部分:前端 – 网格功能和CRUD(创建,读取,更新和删除)

在这一部分中,我们将构建到目前为止的简单网格上,并将简单的CRUD操作连接到后端。

简单的内联编辑

让我们从最简单的CRUD操作开始 – 更新。 具体来说,我们将从“简单”值的内联编辑开始 – 包含单个值的值(即不是复杂对象)。

在我们当前显示的值中,只有namecountry属于此类别,所以让我们从这些开始。

首先,我们需要在中间层创建一个新的Spring Controller,我们可以使用它来访问静态数据。 我不会过多地使用此服务,因为我们已经在本系列的前一部分中使用了控制器:

/**
 *
 * @author 醉探索戈壁
 * @create 2018/7/15 上午10:52
 * @since 1.0.0
 */
@CrossOrigin(origins = "http://localhost:4200")
@RestController
public class StaticDataController {

    private final CountryRepository countryRepository;
    private final 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();
    }
    
}
@Injectable()
export class StaticDataService {
    private apiRootUrl = 'http://localhost:8080';
    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');
    }
}
// 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
        }
    ]
}

SpringBoot实战: 集成ag-Grid构建CRUD应用程序 – 第4部分

到目前为止,我们实际上并没有对我们的编辑做任何事情 – 让我们挂钩更改并将它们保存到数据库中。

<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包含行数据),并在成功保存时再次更新行数据。

您可以通过强制刷新来测试更改 – 您应该发现新值在网格中 – 旧值已被覆盖。

注意:由于我们使用的是内存数据库,因此不会永久保留更改。 如果停止并重新启动Java应用程序,将再次显示原始值。

我们还无法编辑“结果”列 – 这只是基础数据的总和。 我们稍后会谈到这个。

请注意,每次单元格值更改时,我们都会刷新整个网格数据 – 这非常低效。 我们将考虑使用ag-Grid Update功能,以便我们只重绘已更改的行,而不是整个网格。

删除记录

接下来让我们看看记录删除 – 这可能是最容易实现的CRUD操作。

我们将允许用户选择一个或多个记录,然后提供一个按钮,单击该按钮将删除所选记录。

首先,让我们在Grid中启用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
}

SpringBoot实战: 集成ag-Grid构建CRUD应用程序 – 第4部分

接下来,让我们添加一个用户可以用来删除所选行的按钮 – 我们还将添加一个实用程序方法,如果没有选择行,它将禁用删除按钮:

<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())
}

如果api准备好并且选择了行,rowsSelected()将返回true。

deleteSelectedRows() – 我们抓取所有选定的行,然后依次删除每一行。 最后,我们调用this.setAthleteRowData()来使用数据库中的当前数据刷新网格。

这是一个相当天真和低效的实现 – 有两个明显的改进:

批量删除 – 让中间/后端完成工作

使用ag-Grid Update功能,以便我们只重绘已更改/删除的行,而不是整个网格

我们将在稍后的迭代中介绍这些改进。

记录创建/插入

好吧,让我们看一下更具挑战性的事情 – 插入新的运动员数据。 将新数据添加到网格本身很容易,但是为了添加一个完整的运动员,包括结果信息,在Angular方面需要比我们目前使用的更多的工作。

首先,让我们创建一个新组件,我们将用它来创建新记录,以及稍后记录更新。

我们将使用Angular CLI为我们执行此操作:

ng g c athlete-edit-screen

g是生成的简写,c是组件的简写。

让我们替换template()的内容,如下所示:

<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>

看起来这里可能会发生很多事情,但我们最终会看到这样的事情:

SpringBoot实战: 集成ag-Grid构建CRUD应用程序 – 第4部分

简而言之,此屏幕将向我们展示运动员的全部细节。 在第一行,我们有name和countryfields,在下面我们将列出各种结果,如果有的话。

显示结果的网格可能已被提取到它自己的组件中,但为了方便起见,我选择让它更简单一些。

相应的组件类分解如下:

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'
    });
}

这是当用户想要选择新结果时执行的操作。 我们创建一个新的空记录(将sport richSelect默认为第一个可用的运动)并要求网格使用this.api.updateRowData为我们创建一个新行。

相同的机制可用于更新和删除 – 有关Grid提供的这一功能强大的功能的更多信息,请参阅相应的更新文档。

插入新行后,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)知道保存完成,传入新的运动员。

注意:我们的实现中当然缺少一些东西 – 我们只执行最少的验证,表单也可以使用取消按钮。 由于这仅用于说明目的,这样做,但在实际应用中,您可能希望进一步充实。

这里没有描述完整的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;
}

在这里,我们将新的运动员传递给我们的运动员服务进行持久化,并保存一个重新加载网格数据。

如上所述,整个网格数据的刷新效率非常低 – 再次,我们将在稍后的迭代中考虑改进这一点。

最后,我们再次隐藏编辑屏幕。

如上所述,在实际应用程序中,将在此处进行某种验证,以确保返回有效数据 – 再次,我们为了易读性而跳过此类。

更新/编辑记录

我们完成的CRUD操作的最后一部分是更新部分 – 在这里我们将采用现有的运动员记录并允许用户编辑它。

我们将使用上面创建的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时,我们现在也会传递一名运动员。

当创建一个新的运动员时,这个反射将为空,但是当我们编辑现有的行时,我们将通过我们的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上,我们做了一些小的改动,以考虑到现有的运动员被编辑:

@Input() athlete: Athlete = null;

在这里,我们让Angular知道我们期待一Athlete被传入。这里总会传递一些东西 – 在编辑操作的情况下是有效的Athlete,在插入操作的情况下是无效的。

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,供我们编辑。

SpringBoot实战: 集成ag-Grid构建CRUD应用程序 – 第4部分

应用了一点造型,我们最终会得到:

SpringBoot实战: 集成ag-Grid构建CRUD应用程序 – 第4部分

乐观锁

我们在这里采用了一种乐观的锁定策略 – 我们假设我们可以编辑/删除某些东西,并且没有其他人修改我们在我们之前编辑的内容。

这是一个相当使用的机制,但它有它的缺点。 我们尝试编辑/删除其他用户在我们之前编辑过的内容,我们会收到错误。 这也是正常的,在实际的应用程序中,您可以为此编写代码 – 让用户知道这已经发生,或者在用户尝试之前进行检查(或两者)。

Spring JPA通过使用版本控制为我们提供了大量的乐观锁定。 每次更改记录时,版本都会被提升,结果将存储在数据库中。 当计划进行更改时,Spring JPA将根据数据库中的版本检查当前版本 – 如果它们相同,则编辑可以继续,但如果不是,则会引发错误。

这个版本控制是通过将以下内容添加到我们想要版本的实体来完成的

/**
 *
 * @author 醉探索戈壁
 * @create 2018/7/10 下午7:33
 * @since 1.0.0
 */
@Entity
@Cacheable(false)
@Data
@ToString
@EqualsAndHashCode
public class Athlete {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Version()
    private Long version = 0L;

在这里@Version注释让我们的Spring JPA知道我们想要对这个类进行版本化 – 其余的只是自动发生!

springboot实战开发 和 ag-grid中文实战 第四部分到这里已经结束

 

×

感谢您的支持,我们会一直保持!

扫码支持
请土豪扫码随意打赏

打开支付宝扫一扫,即可进行扫码打赏哦

分享从这里开始,精彩与您同在

赞(0) 打赏
未经允许不得转载:醉探索戈壁 » SpringBoot实战: 集成ag-Grid构建CRUD应用程序 – 第4部分
分享到: 更多 (0)
标签:

给戈壁浇点水

支付宝扫一扫打赏

微信扫一扫打赏