Skip to content

Blogs de Développeurs: Aggrégateur de Blogs d'Informatique sur .NET, Java, PHP, Ruby, Agile, Gestion de Projet

Forum Logiciel

Forum Logiciel : diffusion de connaissance et d’informations sur toutes les activitĂ©s liĂ©es au dĂ©veloppement d’applications informatiques en entreprise.

Blog d’Ippon Technologies
Syndiquer le contenu
Les experts Java EE, Portail et SOA
Mis à jour : il y a 3 semaines 2 jours

RxSwift: How to bind a UITableView to a datasource

mer, 08/30/2017 - 08:30

The purpose here is to setup a simple MVVM architecture to bind ViewModel’s data to its dedicated View which will be a UITableView contained in a UITableViewController. Furthermore, we are binding a UIBarButtonItem’s tap event to the ViewModel which will handle the datasource populating, informing the view that there is a new item to display. Every bindings will be made available through RxSwift.

This how-to is not a RxSwift tutorial and will not cover the keys and principles of Rx. There is a lot of great resources on the web if you’d like to get started with RxSwift, like the following :

For this how-to, we’ll use a Swift Playground generator which is able to include any ‘pod’ (dependency) to it.

https://github.com/neonichu/ThisCouldBeUsButYouPlaying

You’ll need a computer running macOS to run the Playground.

This tool needs Ruby to run, you can install it through HomeBrew.

To generate a RxSwift, RxCocoa Playground, follow those steps:

  • First, install the tool
$ gem install cocoapods-playgrounds

 

  • Then generate an RxSwift and RxCocoa capable Playground
$ pod playgrounds RxSwift,RxCocoa

 

That’s it, your Playground should be named ‘RxSwiftPlayground’, open that directory then open the ‘RxSwift.xcworkspace’ file and build the ‘RxSwiftPlayground’ scheme. You are now ready to get into this how-to!

Playground setup

Open the ‘RxSwift.playground’ then remove its content. We’ll need some imports to get started. Add these imports to the top of the Playground file.

import RxSwift
import RxCocoa
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

 

UITableViewController setup

First, we’ll setup a classic UITableViewController which automatically handles a UITableView bounds to its edges.

class TableViewController: UITableViewController {

	//1
	private let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: nil)
	private let cellIdentifier = "Cell"
	
	//2
	override func viewDidLoad() {
		super.viewDidLoad()
		setupAddButton()
		setupTableView()
	}

	//3
	private func setupAddButton() {
		navigationItem.setRightBarButton(addButton, animated: true)
	}

	//4
	private func setupTableView() {
		//This is necessary since the UITableViewController automatically set his tableview delegate and dataSource to self
		tableView.delegate = nil
		tableView.dataSource = nil

		tableView.tableFooterView = UIView() //Prevent empty rows
		tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)
	}
}

 

  1. Let’s define some properties, first a UIBarButtonItem with .add SystemItem which represents a ‘+’ sign. It will be the trigger button to add items to our UITableView. Second, is the cell identifier used by our UITableView to register and reuse UITableViewCell.
  2. Inside the UITableViewController‘s viewDidLoad method we are calling two basic setup methods, one to add the addButton to the UITableViewController‘s UINavigationBar, the other to setup the UITableViewController‘s UITableView
  3. The addButton is added to the UINavigationBar right part
  4. We are setting up the UITableView, by removing UITableView‘s delegate and dataSource to let RxSwift works by handling it itself, then we are registering UITableViewCell type with the previously defined cellIdentifier.
Playground’s live view setup

In order to make the TableViewController visible in the Xcode Assistant Editor, we need to create a TableViewController instance embedded in a UINavigationController, then resize the UINavigationController to a real iPhone dimension.

let rootViewController = TableViewController()
rootViewController.title = "RxSwift TableView Binding"
let navigationController = UINavigationController(rootViewController: rootViewController)
// Let  resize the navigationController to look like a real iPhone size (iPhone SE in this case)
navigationController.view.frame = CGRect(x: 0, y: 0, width: 320, height: 568)

 

Then just add the newly created UINavigationController.view to the current Playground’s live view.

PlaygroundPage.current.liveView = navigationController.view

 

The last step is to make the live preview visible, just click on View > Assistant Editor > Show Assistant Editor in the Xcode top menu. You should see the following render in your Assistant Editor

ViewModel setup

The ViewModel’s purpose here is to provide a dataSource of String items and add an item to it wherever the plus button is tapped. To achieve this, we’ll need to declare a privateDataSource property of type Variable<[String]> in the ViewModel. The Variable type is a simple container that can be observed. Here we declare a container of type: Array of String.

Then we’ll need to declare the exposed dataSource which is an Observable<[String]>, this is the actual dataSource the View will bind to.

We finally need to declare a DisposeBag property. A DisposeBag is a bag within which we will add our Disposables. This bag will handle their lifecycle. On DisposeBag deinit, every Disposable get disposed to ensure that resources are properly released when not needed anymore. In our case, the DisposeBag deinit will occur on ViewModel deinit.

class TableViewViewModel {

	// MARK: Private properties
	private let privateDataSource: Variable<[String]> = Variable([])
	private let disposeBag = DisposeBag()

	// MARK: Outputs
	public let dataSource: Observable<[String]>
}

 

Last requirement is to implement the ViewModel’s init method which takes one parameter of type Driver<Void> and represents the addButton tapped event sequence. On every tap, we’ll need to append a String item to the privateDataSource Variable through its .value property. The .value property here is an Array of String.

The Driver type is a perfect fit for UI interactions since it never fails and it delivers events on the MainThread.

The job is done for the privateDataSource, we just need to expose the public dataSource as an Observable of the privateDatasource.

	init(addItemTap: Driver<Void>) {

		// Make the output dataSource an Observable of the privateDataSource
		self.dataSource = privateDataSource.asObservable()

		//Register addButton tap to append a new "Item" to the dataSource on each tap -> onNext
		addItemTap.drive(onNext: { [unowned self] _ in
			self.privateDataSource.value.append("Item")
		})
		.addDisposableTo(disposeBag)
	}

 

⚠ Note: [unowned self] is used to bring self to the closure unretained to prevent retain cycle also known as memory leak.

We now have a View and a ViewModel ready to use, let’s bind them!

View and ViewModel binding

In order to make the View and the ViewModel working together, we’ll need to bind them.

Declare a viewModel property of  TableViewViewModel type inside the TableViewController class

In the viewDidLoad method we’ll instantiate the viewModel property to set the addButton rx.tap property as Driver to the TableViewViewModel constructor. The addButton is now bound to the viewModel.

	private var viewModel: TableViewViewModel!
	
	override func viewDidLoad() {
		super.viewDidLoad()
		setupViewModel()
		setupTableView()
		setupAddButton()
	}
	
	private func setupViewModel() {
		self.viewModel = TableViewViewModel(addItemTap: addButton.rx.tap.asDriver())
	}

 

The very last step is to bind the viewModel’s dataSource of String items to our UITableView, this binding will be stored in a  DisposeBag.

	private let disposeBag = DisposeBag()

	private func setupTableViewBinding() {

		viewModel.dataSource
			.bind(to: tableView.rx.items(cellIdentifier: cellIdentifier, 
				  						cellType: UITableViewCell.self)) {  row, element, cell in
				cell.textLabel?.text = "\(element) \(row)"
			}
			.addDisposableTo(disposeBag)
	}

 

Finally we need to tell the UITableView that every dataSource item must be represented by a UITableViewCell dequeued from the previously defined cellIdentifier on which we are setting a text value equals to the item value concatenated with the current row number.

You can now tap on the addButton in the UINavigationBar and see items being added to the UITableView.

Here is the complete Playground content:

import RxSwift
import RxCocoa
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

class TableViewViewModel {

	// MARK: Private properties
	private let privateDataSource: Variable<[String]> = Variable([])
	private let disposeBag = DisposeBag()

	// MARK: Outputs
	public let dataSource: Observable<[String]>

	init(addItemTap: Driver<Void>) {

		// Make the output dataSource an Observable of the privateDataSource
		self.dataSource = privateDataSource.asObservable()

		//Register addButton tap to append a new "Item" to the dataSource on each tap -> onNext
		addItemTap.drive(onNext: { [unowned self] _ in
			self.privateDataSource.value.append("Item")
		})
		.addDisposableTo(disposeBag)
	}
}

class TableViewController: UITableViewController {

	private let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: nil)
	private var viewModel: TableViewViewModel!
	private let cellIdentifier = "Cell"
	private let disposeBag = DisposeBag()

	override func viewDidLoad() {
		super.viewDidLoad()
		setupViewModel()
		setupTableView()
		setupAddButton()
		setupTableViewBinding()
	}

	private func setupViewModel() {
		self.viewModel = TableViewViewModel(addItemTap: addButton.rx.tap.asDriver())
	}

	private func setupAddButton() {
		navigationItem.setRightBarButton(addButton, animated: true)
	}

	private func setupTableView() {

		//This is necessary since the UITableViewController automatically set his tableview delegate and dataSource to self
		tableView.delegate = nil
		tableView.dataSource = nil

		tableView.tableFooterView = UIView() //Prevent empty rows
		tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)
	}

	private func setupTableViewBinding() {

		viewModel.dataSource
			.bind(to: tableView.rx.items(cellIdentifier: cellIdentifier, cellType: UITableViewCell.self)) {  row, element, cell in
				cell.textLabel?.text = "\(element) \(row)"
			}
			.addDisposableTo(disposeBag)
	}
}


let rootViewController = TableViewController()
rootViewController.title = "RxSwift TableView Binding"

let navigationController = UINavigationController(rootViewController: rootViewController)
navigationController.view.frame = CGRect(x: 0, y: 0, width: 320, height: 568)
PlaygroundPage.current.liveView = navigationController.view

 

And the expected result:

Conclusion

We’ve quickly seen how ViewModels and Views are acting together in a MVVM architecture powered by RxSwift through event/data stream in a declarative way. This is achieved without implementing any of the UITableViewDelegate and UITableViewDataSource dedicated methods.

L’article RxSwift: How to bind a UITableView to a datasource est apparu en premier sur Le Blog d'Ippon Technologies.

Catégories: Blog Société

DĂ©veloppeur full stack ? Oui
 mais


lun, 08/07/2017 - 08:56

Lorsque nous demandons Ă  un dĂ©veloppeur s’il est full stack, sa rĂ©ponse est gĂ©nĂ©ralement “Oui ! Mais
 plutĂŽt spĂ©cialisĂ© dans 
 telle partie”.

Dans le cadre d’un projet informatique Agile, nous recherchons souvent des dĂ©veloppeurs full stack. Mais, que signifie ce terme et que peut-il apporter au projet ?

Nous allons voir dans un premier temps ce que signifie stack et les raisons mises en avant pour la composition d’une Ă©quipe de personnes full stack. Puis, nous verrons sa mise en place dans le cadre d’un projet informatique au travers d’un exemple.

Qu’est ce qu’une “Stack” ?

Une “stack” est une pile comprenant l’ensemble des couches nĂ©cessaires Ă  la rĂ©alisation d’un projet informatique. Chacun de ces Ă©lĂ©ments ou couche constitue donc la “stack”. On peut par exemple parler de stack technique, si nous ne faisons rĂ©fĂ©rence qu’aux couches techniques d’un projet.

TrĂšs souvent, quand nous parlons de dĂ©veloppeur full stack, nous sommes tentĂ©s de privilĂ©gier uniquement les couches techniques et notamment les couches “front” et “back”. Mais il serait dommage de se limiter aux seuls aspects techniques. En effet, les compĂ©tences dites “soft skills” sont Ă©galement importantes et particuliĂšrement en milieu agile.

Une liste (non exhaustive) d’élĂ©ments composant une “stack” pourrait ĂȘtre :

L’idĂ©e d’une Ă©quipe full stack est donc que chaque membre de l’équipe de dĂ©veloppement puisse intervenir sur chaque couche de la “stack” au sein d’un projet informatique. En consĂ©quence, dans le cas d’un projet Agile (oĂč les dĂ©veloppeurs sont auto-organisĂ©s et ont une responsabilitĂ© plus large que sur un projet cycle en V), un dĂ©veloppeur peut donc :

  • traiter une User Story de bout en bout en cours de sprint (comprĂ©hension fonctionnelle, dĂ©veloppement des diffĂ©rentes parties techniques, etc.) ;
  • relire et valider le travail d’un autre dĂ©veloppeur.
  • intervenir sur les sujets fonctionnels
Avantages supposĂ©s d’une Ă©quipe full stack

Maintenant que nous avons défini les éléments, essayons de comprendre les raisons de ce choix.

Sur le papier, cette mĂ©thode offre beaucoup d’avantages dont les principaux sont :

  • la simplification de l’organisation, car chaque dĂ©veloppeur peut intervenir sur chacun des sujets ;
  • la possibilitĂ© d’accroĂźtre simplement la charge de dĂ©veloppement en augmentant le nombre de dĂ©veloppeurs ;
  • une meilleure implication des dĂ©veloppeurs qui auront en charge des User Stories complĂštes, permettant ainsi une meilleure visibilitĂ© fonctionnelle grĂące Ă  la rĂ©alisation de tĂąches techniques dans des domaines diffĂ©rents (et non pas un seul).

Mais tout cela est-il si simple ? Qu’en est-il en pratique ?

Pour rĂ©pondre Ă  ces questions, il est nĂ©cessaire d’aborder ce qu’on entend par “intervenir sur chaque couche”.

Un dĂ©veloppeur full stack doit pouvoir intervenir sur chacune des couches d’une “stack” ?

Qu’entendons-nous par cette assertion ? Un dĂ©veloppeur doit-il travailler sur chaque sujet ? Si oui, doit-il avoir un niveau de compĂ©tence Ă©quivalent sur chaque couche ?

La rĂ©ponse est non. Il est trĂšs compliquĂ© pour un dĂ©veloppeur d’ĂȘtre compĂ©tent et autonome sur chaque couche.

Quand nous parlons de profil full stack, cela signifie que le dĂ©veloppeur est spĂ©cialisĂ© dans certains domaines, tout en ayant des connaissances sur d’autres sujets. En gĂ©nĂ©ral, nous considĂ©rons un dĂ©veloppeur full stack comme maĂźtrisant au moins 3-4 sujets. Mais cela ne couvre pas l’ensemble des besoins.

AprĂšs avoir analysĂ© ces points, nous comprenons bien les rĂ©ticences des dĂ©veloppeurs Ă  se considĂ©rer full stack car chacun a ses prĂ©fĂ©rences, ses domaines de prĂ©dilection et peut difficilement maĂźtriser toutes les couches d’un projet informatique.

Prenons un exemple

Maintenant que nous avons passé en revue ces différents éléments, intéressons-nous à sa mise en place dans un projet informatique.

Pour illustrer ces propos, prenons l’exemple d’un projet informatique qui dĂ©marre avec la mĂȘme stack que prĂ©cĂ©demment :

Le schĂ©ma ci-dessous permet de visualiser les compĂ©tences d’un dĂ©veloppeur sur l’ensemble de cette stack :

Dans cette situation, nous voyons bien que le développeur est full stack car il a des connaissances dans 90% des sujets et en maßtrise au moins 4.

Cependant, cela ne permet aucunement d’en conclure qu’il est capable d’intervenir sur des sujets pointus dans chaque domaine. Par exemple, il ne sera pas en mesure – ou Ă©prouvera des difficultĂ©s – Ă  rĂ©pondre Ă  une demande complexe concernant la partie front.

Maintenant, imaginons que l’équipe soit composĂ©e de deux dĂ©veloppeurs et que nous souhaitons couvrir, au mieux, tous les sujets du projet. Nous obtiendrions, par exemple, le schĂ©ma suivant (en bleu, le premier dĂ©veloppeur et en jaune le second) :

Dans cette hypothĂšse, les deux dĂ©veloppeurs full stack se complĂštent et la combinaison de leurs compĂ©tences permet de couvrir l’ensemble des besoins du projet. De plus, ces deux dĂ©veloppeurs ont la capacitĂ© de revoir leurs codes respectifs et de monter en compĂ©tence sur des sujets qu’ils maĂźtrisent moins.

Si nous nous projetons sur une équipe plus importante, quel serait le résultat attendu ?

Nous pouvons rĂ©sumer cela par cette phrase : “Pour chaque couche de la stack, avoir au moins deux personnes qui la maĂźtrisent”

Pourquoi deux dĂ©veloppeurs ? Tout simplement pour que l’équipe ne soit pas bloquĂ©e  lorsque l’un des deux est absent (congĂ©s, arrĂȘt, maladie
).

Une digression reste cependant possible. En effet, les besoins existants dans chaque projet informatique n’impliquent pas le mĂȘme niveau de complexitĂ© sur chaque couche. Nous pouvons imaginer dans notre exemple que la partie “API” est trĂšs lĂ©gĂšre et “standard”, donc qu’elle n’aura pas besoin de fortes compĂ©tences. On sera donc moins exigeant vis Ă  vis de l’équipe sur ce point (en terme de niveau de compĂ©tence ou de backup).

Résumé

Comme nous venons de le voir, la composition d’une Ă©quipe full stack est plus complexe qu’associer uniquement des dĂ©veloppeurs dits full stack. Il est nĂ©cessaire d’analyser les spĂ©cialitĂ©s de chaque dĂ©veloppeur qui constitue l’équipe afin d’avoir une cohĂ©rence face aux challenges du projet.

Le risque de n’avoir pas une Ă©quipe aux compĂ©tences suffisamment variĂ©es, est que la rĂ©ponse technique sera potentiellement le fruit de l’expĂ©rience des membres plutĂŽt que la meilleure rĂ©ponse possible. Par exemple, s’il manque un expert UX, les dĂ©veloppeurs pourraient ĂȘtre tentĂ©s de trouver des solutions techniques Ă  un problĂšme d’ergonomie.

Une autre remarque importante : il est difficile d’avoir une organisation idĂ©ale dĂšs les premiĂšres itĂ©rations du projet. N’avoir qu’une personne maĂźtrisant un sujet au dĂ©but peut ĂȘtre un risque acceptĂ© si des montĂ©es en compĂ©tences sont prĂ©vues au sein de l’équipe.

Conclusion

Notons Ă©galement que les derniĂšres annĂ©es montrent une explosion des nouvelles technologies / librairies et qu’il devient de plus en plus difficile pour un dĂ©veloppeur d’ĂȘtre full stack. C’est le cas sur la partie front qui Ă©volue rapidement, mais aussi avec les problĂ©matiques spĂ©cifiques du Big Data qui nĂ©cessite une connaissance pointue sur les outils du domaine et les bonnes pratiques. La spĂ©cialisation sera donc toujours nĂ©cessaire et devrait s’accentuer Ă  l’avenir.

Quant au mot full stack, il serait alors utilisĂ© comme un terme marketing auquel le dĂ©veloppeur essaie de se conformer. En d’autres termes, un dĂ©veloppeur se dĂ©clarant full stack le fait-il plus par obligation ou par choix ?

L’article DĂ©veloppeur full stack ? Oui… mais… est apparu en premier sur Le Blog d'Ippon Technologies.

Catégories: Blog Société

Vue.js 2.0 : petit tutoriel (volume 5)

lun, 07/24/2017 - 08:30

Dans la suite de ce tutoriel nous abordons le routage dans Vue.js avec le module vue-router.

Si vous avez manqué le début :

  • Volume 1 : pour crĂ©er un projet, crĂ©er un composant et transmettre des donnĂ©es d’un composant parent Ă  un composant enfant ;
  • Volume 2 : pour apprendre Ă  modifier les donnĂ©es d’un composant et de quelle façon invoquer un service REST ;
  • Volume 3 : pour transmettre des donnĂ©es d’un composant enfant à un composant parent et dĂ©couvrir la communication par Ă©vĂ©nements ;
  • Volume 4 : pour dĂ©couvrir le pattern gestionnaire d’état et une implĂ©mentation Vue.js officielle avec Vuex.

Le code source est accessible ici. Tout ce qui suit fait référence au contenu des répertoires vuejs-tuto/src/pages/chap9/ et vuejs-tuto/src/components/chap9/

Routage et SPA

Vue.js peut ĂȘtre utilisĂ© pour crĂ©er une “classique” Multi Page Application (MPA) comme une Single Page Application (SPA) voire une “application hybride” en n’utilisant Vue.js que pour quelques fonctionnalitĂ©s.

L’utilisation d’une SPA signifie que l’application n’est constituĂ©e que d’une seule page, une seule ressource identifiĂ©e par une unique URL : mĂȘme si visuellement on peut avoir le sentiment de changer de page (en sĂ©lectionnant un menu, un onglet ou autre) , il n’y a pas en fait de vĂ©ritablement navigation dans l’application au sens oĂč on l’entend habituellement, avec un changement d’URL et un appel Ă  chaque clic pour rĂ©cupĂ©rer une nouvelle page construite cĂŽtĂ© serveur.

Les SPAs permettent en particulier de proposer aux utilisateurs d’une application web une expĂ©rience proche de celle d’une application mobile, tout en facilitant par exemple la diffusion de l’application puisqu’on est alors libĂ©rĂ© des contraintes des stores et de leur processus de livraison ou encore de l’obligation de mettre Ă  jour l’application. Cette “meilleure” expĂ©rience se traduit notamment par le fait qu’il n’y a plus besoin de faire de nombreux appels serveurs pour mettre Ă  jour la page dĂšs qu’une action est effectuĂ©e dans l’application.

Pour Ă©viter de recharger la SPA inutilement Ă  chaque clic ou encore permettre qu’un favori enregistrĂ© dans le navigateur par l’utilisateur l’amĂšne directement Ă  la “page” correspondante dans la SPA, on peut “simuler” plusieurs ressources et leur url au travers d’un fragment identifier (le caractĂšre #) ajoutĂ© dans l’url : c’est le “hashmode”.

Pour permettre l’indexation par les moteurs de recherche, faciliter les redirections, etc. on prĂ©fĂ©rera l’utilisation d’URL classiques Ă  travers l’API History.js d’HTML5.

Les frameworks JavaScript permettant de crĂ©er des SPA proposent un module additionnel pour mettre en place cette navigation. C’est justement le cas de Vue.js avec son module vue-router qui dĂ©coupe une SPA en diffĂ©rentes routes selon le mode “hashmode” ou le mode “history”.

vue-router

Pour installer ce module, cela se résume à la commande npm :

npm i –save vue-router

Pour dĂ©finir les diffĂ©rentes routes de l’application, il suffit de crĂ©er une instance de VueRouter en spĂ©cifiant le mode Ă  utiliser (celui par dĂ©faut, basĂ© sur hashmode, ou “history” si on souhaite manipuler de vĂ©ritables URLs) ainsi que la liste des chemins d’accĂšs et des composants Ă  afficher lorsque ce chemin est accĂ©dĂ©, le tout associĂ© au nom de la route dĂ©finie :

src/pages/chap9/AppRoutes.js

src/pages/chap9/AppRoutes.js

Pour utiliser ces routes dans l’application, il n’y a qu’à l’indiquer à l’instance principale de Vue :

src/main.js

src/main.js

Pour permettre d’afficher le composant associĂ© Ă  un chemin donnĂ©, on passe par le composant router-view :

src/App.vue

src/App.vue

RĂ©sultat si on accĂšs Ă  l’url http://localhost:8083/vuejs-tuto/chap1 :

Si on tape http://localhost:8083/vuejs-tuto/chap2 :

AccĂ©der Ă  l’URL courante

L’utilisation de vue-router met Ă  disposition l’objet $route, c’est-Ă -dire l’URL de la ressource accĂ©dĂ©e. Comme pour toute URL, il est possible de transmettre des paramĂštres qu’on peut rĂ©cupĂ©rer dans le composant affichĂ© via this.$route.query.[NOM_DU_PARAMETRE] comme dans l’exemple suivant :

src/components/chap9/Hello.vue

src/components/chap9/Hello.vue

AprÚs avoir ajouté la route qui va bien,

src/pages/chap9/AppRoutes.js

src/pages/chap9/AppRoutes.js

en allant sur http://localhost:8083/vuejs-tuto/chap9?lastname=Simpson&firstname=Maggie on obtient alors :

Si on souhaite manipuler des URL dynamiques, on doit commencer par identifier dans la dĂ©finition de la route quelles portions de l’URL sont variables. Par exemple dans le chemin /vuejs-tuto/chap9/:id, :id pourra ĂȘtre remplacĂ© par n’importe quelle valeur et quelle que soit cette valeur c’est la mĂȘme instance de composant qui sera utilisĂ©e :

src/pages/chap9/AppRoutes.js

src/pages/chap9/AppRoutes.js

Dans le composant, on accùde à la valeur dynamique de l’url via this.$route.params.[NOM_ELEMENT_DYNAMIQUE]

src/components/chap9/Hello.vue

src/components/chap9/Hello.vue

En allant sur http://localhost:8083/vuejs-tuto/chap9/123456789?lastname=Simpson&firstname=Maggie on obtient alors :

Si on rĂ©cupĂšre la valeur dynamique en accĂ©dant explicitement Ă  l’URL dans le composant, on limite son utilisation puisqu’on ne pourra pas l’inclure simplement dans une page en transmettant la valeur dynamique Ă  travers un attribut de l’élĂ©ment HTML comme vu dans le premier volet de ce tutoriel : pour pallier cette limitation il suffit d’ajouter dans la dĂ©finition de la route props: true afin que la portion dynamique de l’URL soit accĂ©dĂ©e via les propriĂ©tĂ©s du composant.

src/pages/chap9/AppRoutes.js

src/pages/chap9/AppRoutes.js

 

src/components/chap9/Hello.vue

src/components/chap9/Hello.vue

Naviguer dans l’application

La navigation entre les pages passe par l’utilisation du composant router-link. Si on ajoute un composant Menu.vue affichant une liste de liens pour atteindre les diffĂ©rentes pages de l’application :

src/components/chap9/Menu.vue

src/components/chap9/Menu.vue

On obtient :

Notez qu’on peut spĂ©cifier l’URL cible soit via le chemin soit via le nom de la route dĂ©finie.

Lazy loading

PlutĂŽt que de charger l’ensemble des composants de l’application en une fois au premier accĂšs, il est prĂ©fĂ©rable en termes de performance de ne charger que les composants effectivement utilisĂ©s dans la page actuellement consultĂ©e, en rĂ©cupĂ©rant leur dĂ©finition de façon asynchrone, uniquement lorsque c’est nĂ©cessaire.

Vue.js permet de charger des composants Ă  la volĂ©e Ă  travers une fonctionnalitĂ© de composant asynchrone (dans laquelle on expose une fonction retournant une promesse important le composant), fonctionnalitĂ© que l’on peut associer Ă  celle de code splitting de webpack (qui permet de dĂ©couper le code d’une application en plusieurs modules JavaScript) : au final, lorsque l’unique fichier constituant la SPA est rĂ©cupĂ©rĂ© au premier accĂšs Ă  l’application, ce fichier ne contient pas le code javascript permettant de construire le(s) composant(s) devant ĂȘtre chargĂ©(s) de façon diffĂ©rĂ©e. Lorsqu’on ouvre une page devant afficher un composant dont la dĂ©finition ne fait pas partie du fichier principal de l’application, une requĂȘte supplĂ©mentaire est lancĂ©e auprĂšs du serveur pour rĂ©cupĂ©rer le module JavaScript nĂ©cessaire Ă  la construction du composant manquant.

Dans la définition des routes, il suffit de spécifier le composant à utiliser de la façon suivante :

src/pages/chap9/AppRoutes.js

src/pages/chap9/AppRoutes.js

Avec cette configuration, si on observe les diffĂ©rentes requĂȘtes qui sont dĂ©clenchĂ©es Ă  partir du navigateur, on peut voir qu’une requĂȘte supplĂ©mentaire est lancĂ©e lorsque le lazy loading est activĂ© :

RequĂȘtes effectuĂ©es SANS lazy loading

RequĂȘtes effectuĂ©es SANS lazy loading

 

RequĂȘtes effectuĂ©es AVEC lazy loading

RequĂȘtes effectuĂ©es AVEC lazy loading

Hooks

Vue-router offre la possibilitĂ© de facilement intervenir Ă  toutes les Ă©tapes constituant la navigation d’une page Ă  une autre, Ă  travers les hooks :

  • beforeEach et afterEach qui s’appliquent Ă  une instance de vue-router et qui s’exĂ©cutent Ă  tous les changements d‘URL, juste avant le dĂ©but et/ou juste aprĂšs la fin de la navigation ; on les dĂ©clarera au mĂȘme endroit que la dĂ©finition des routes ; c’est par exemple Ă  ce niveau qu’on peut tester si un utilisateur a le droit d’accĂ©der ou non Ă  la page ciblĂ©e ;
  • beforeEnter, qui s’applique directement sur une route pour exĂ©cuter une portion de code spĂ©cifique Ă  la route en question ;
  • beforeRouteEnter, beforeRouteUpdate et beforeRouteLeave qui sont disponibles au niveau de chaque composant ; on passera par exemple par beforeRouteEnter lorsqu’on souhaite bloquer la navigation tant qu’on n’a pas reçu de la part du serveur toutes les donnĂ©es nĂ©cessaires Ă  l’affichage des composants de la page cible,  beforeRouteUpdate pour effectuer certaines opĂ©rations sur une instance de composant rĂ©-utilisĂ©e lorsqu’une URL dynamique est ouverte, beforeRouteLeave pour par exemple afficher un message prĂ©venant l’utilisateur qu’il perdra les informations saisies s’il ne les sauvegarde pas avant de quitter la page courante.

Tous ces hooks prennent en argument la route de destination (to), la route de provenance (from), et une fonction Ă  appeler explicitement pour sortir du hook un fois celui-ci rĂ©solu (next, Ă  l’exception de afterEach qui ne dispose pas de cette fonction puisqu’il est le dernier hook de la chaĂźne Ă  s’exĂ©cuter). Par dĂ©faut, cette fonction permet simplement d’exĂ©cuter le hook suivant de la chaĂźne mais selon le paramĂštre transmis elle permet aussi d’effectuer une redirection vers une autre page ou tout simplement d’annuler la navigation.

L’exĂ©cution des hooks s’enchaĂźne selon la chronologie suivante :

Intégrons les hooks à notre application :

Définition des hooks globaux et spécifiques aux routes dans src/pages/chap9/AppRoutes.js

Définition des hooks globaux et spécifiques aux routes dans src/pages/chap9/AppRoutes.js

 

Définition des hooks spécifiques à un composant dans src/components/chap9/Hello.vue

Définition des hooks spécifiques à un composant dans src/components/chap9/Hello.vue

Ajoutons à présent dans notre composant Menu.vue un second lien vers la page pagechap9 mais en lui transmettant des paramÚtres différents :

src/components/chap9/Menu.vue

src/components/chap9/Menu.vue

Observons ce qui se passe :

  1. AccĂšs Ă  http://localhost:8083/vuejs-tuto/chap1 : seuls les hooks globaux sont invoquĂ©s, puisqu’aucun hook spĂ©cifique Ă  la route accĂ©dĂ©e ou spĂ©cifique au composant affichĂ© n’a Ă©tĂ© paramĂ©trĂ©.
  2. AccĂšs Ă  http://localhost:8083/vuejs-tuto/chap2 Ă  partir de http://localhost:8083/vuejs-tuto/chap1 : mĂȘme comportement que prĂ©cĂ©demment, pour les mĂȘmes raisons.
  3. AccÚs à http://localhost:8083/vuejs-tuto/chap9/123123123?firstname=Maggie&lastname=Simpson à partir de http://localhost:8083/vuejs-tuto/chap2 : les hooks globaux sont exécutés ainsi que ceux spécifiques à la route pagechap9 ; dans les hooks spécifiques au composant affiché seul beforeRouteEnter est appelé puisque le composant est nouvellement activé.
  4. AccĂšs Ă  http://localhost:8083/vuejs-tuto/chap9/555555555?firstname=Montgomery&lastname=Burns Ă  partir de http://localhost:8083/vuejs-tuto/chap9/123123123?firstname=Maggie&lastname=Simpson : les hooks globaux sont une fois de plus exĂ©cutĂ©s mais comme on reste sur la mĂȘme page ceux spĂ©cifiques Ă  la route ne le sont pas ; les paramĂštres de l’URL changent et comme on affiche la mĂȘme instance de composant que celle dĂ©jĂ  utilisĂ©e dans la page prĂ©cĂ©dente seul le hook beforeRouteUpdate du composant est invoquĂ©.
  5. AccĂšs Ă  http://localhost:8083/vuejs-tuto/chap2 Ă  partir de http://localhost:8083/vuejs-tuto/chap9/555555555?firstname=Montgomery&lastname=Burns : les hooks globaux sont invoquĂ©s mais juste aprĂšs le beforeRouteLeave du composant de la page d’origine, exĂ©cutĂ© puisqu’on change de route.
To be continued…

Dans ce cinquiĂšme volet vous avez pu constater (enfin, je l’espĂšre &#x1f609; ) qu’il est trĂšs simple de gĂ©rer la navigation applicative avec vue-router : la fonctionnalitĂ© de lazy loading apporte un vrai bĂ©nĂ©fice tout en Ă©tant particuliĂšrement simple Ă  mettre en place, les hooks offrent un accĂšs aisĂ© Ă  toutes les Ă©tapes de rĂ©solution de la navigation. Vous avez Ă  prĂ©sent l’essentiel des connaissances nĂ©cessaires pour mettre en oeuvre une application d’entreprise avec Vue.js. À la prochaine ! &#x1f609;

 

L’article Vue.js 2.0 : petit tutoriel (volume 5) est apparu en premier sur Le Blog d'Ippon Technologies.

Catégories: Blog Société

Boostez les performances de votre application Spring Data JPA

mer, 07/19/2017 - 08:30

Spring Data JPA fournit une implĂ©mentation de la couche d’accĂšs aux donnĂ©es pour une application Spring. C’est une brique trĂšs pratique car elle permet de ne pas rĂ©inventer la roue de l’accĂšs aux donnĂ©es Ă  chaque nouvelle application et donc de se concentrer sur la partie mĂ©tier. Il y a quelques bonnes pratiques Ă  respecter lors de l’utilisation de Spring Data JPA : par exemple, limiter le chargement des objets inutiles pour optimiser les performances.

Dans ce but, cet article donnera quelques conseils afin de rĂ©duire les aller-retours vers la base de donnĂ©es, de ne pas rĂ©cupĂ©rer l’ensemble des Ă©lĂ©ments de la base de donnĂ©es et de ce fait ne pas impacter les performances globales de l’application. Pour cela, nous verrons dans un premier temps les diffĂ©rents outils que Spring Data JPA nous offre pour amĂ©liorer le contrĂŽle de l’accĂšs aux donnĂ©es, ainsi que quelques bonnes pratiques afin de rĂ©duire l’impact qu’aura la rĂ©cupĂ©ration des donnĂ©es sur notre application. Ensuite, je vous ferai part d’un retour d’expĂ©rience qui sera un exemple concret de l’amĂ©lioration des performances d’une application Spring en jouant sur ces diffĂ©rents aspects et en rĂ©duisant ainsi les problĂšmes sous-jacents.

Le chargement des relations d’entitĂ©s Un peu de thĂ©orie : les types de chargement EAGER et LAZY

Lors de la crĂ©ation d’une application utilisant Spring Data JPA (et plus globalement Hibernate), les dĂ©pendances d’un objet (par exemple l’auteur d’un livre) peuvent ĂȘtre chargĂ©es de façon automatique (chargement EAGER) ou manuelle (chargement LAZY).

Dans le cas d’une dĂ©pendance de type EAGER, Ă  chaque fois que l’on va charger un objet, les objets en relation seront Ă©galement chargĂ©s : lorsque l’on demande les informations d’un livre, les informations de l’auteur seront Ă©galement rĂ©cupĂ©rĂ©es. Dans le cas d’une dĂ©pendance de type LAZY, seules les informations de l’objet demandĂ© seront chargĂ©es : on ne rĂ©cupĂ©rera pas les donnĂ©es de l’auteur.

Dans Spring Data JPA, une relation entre deux objets du domaine possĂšde l’une ou l’autre de ces mĂ©thodes de chargement des donnĂ©es. Par dĂ©faut, la mĂ©thode sera dĂ©terminĂ©e par le type de relation. Voici un rappel des diffĂ©rents types de relation avec la mĂ©thode de chargement par dĂ©faut :

@OneToOne

Pour chaque instance de l’entitĂ© A, une et une seule instance de l’entitĂ© B est associĂ©e. B n’est Ă©galement liĂ© qu’à une seule instance de l’entitĂ© A. Un exemple typique est la relation entre un patient et son dossier patient :

@Entity
public class Patient implements Serializable {
    @OneToOne
    private DossierPatient dossier;
}

Pour ce type de relation, la mĂ©thode de chargement de la relation par dĂ©faut est EAGER : Ă  chaque fois que l’on demande les donnĂ©es du patient, on aura donc Ă©galement les donnĂ©es de son dossier.

@ManyToOne

Pour chaque instance de l’entitĂ© A, une et une seule instance de l’entitĂ© B est associĂ©e. En revanche, B peut ĂȘtre liĂ© Ă  plusieurs instances de l’entitĂ© A. Un exemple typique est la relation entre un produit et sa catĂ©gorie associĂ©e :

@Entity
public class Produit implements Serializable {
    @ManyToOne
    private CategorieProduit categorie;
}

Pour ce type de relation, la mĂ©thode de chargement de la relation par dĂ©faut est EAGER : Ă  chaque fois que l’on demande les donnĂ©es d’un produit, on aura donc Ă©galement les donnĂ©es de sa catĂ©gorie.

@OneToMany

Pour chaque instance de l’entitĂ© A, zĂ©ro, une, ou plusieurs instances de l’entitĂ© B sont associĂ©es. En revanche, B n’est liĂ© qu’à une seule instance de l’entitĂ© A. Il s’agit de l’inverse de la relation @ManyToOne, nous pouvons donc prendre l’exemple d’une catĂ©gorie de produit avec sa liste de produits associĂ©s :

@Entity
public class CategorieProduit implements Serializable {
    @OneToMany
    private Set<Produit> produits = new HashSet<>();
}

Pour ce type de relation, la mĂ©thode de chargement de la relation par dĂ©faut est LAZY : Ă  chaque fois que l’on demande les donnĂ©es d’une catĂ©gorie, la liste de ses produits ne sera pas chargĂ©e.

@ManyToMany

Pour chaque instance de l’entitĂ© A, zĂ©ro, une, ou plusieurs instances de l’entitĂ© B sont associĂ©es. L’inverse est Ă©galement vrai, B peut ĂȘtre liĂ© Ă  zĂ©ro, une, ou plusieurs instances de l’entitĂ© A. Un exemple typique est la relation entre un article et sa liste de thĂšmes abordĂ©s :

@Entity
public class Article implements Serializable {
    @ManyToMany
    private Set<Theme> themes = new HashSet<>();
}

Pour ce type de relation, la mĂ©thode de chargement de la relation par dĂ©faut est Ă©galement LAZY : Ă  chaque fois que l’on demande les donnĂ©es d’un article, la liste de ses thĂšmes ne sera pas chargĂ©e.

Minimiser l’utilisation des relations EAGER

Le but est de ne rĂ©cupĂ©rer en base de donnĂ©es que les donnĂ©es nĂ©cessaires Ă  ce que l’on a demandĂ©. Par exemple, si l’on souhaite afficher la liste des noms des auteurs prĂ©sents dans notre application, on ne souhaite pas rĂ©cupĂ©rer l’ensemble de leurs relations : les livres qu’ils ont Ă©crits, leur adresse, etc.

Une bonne pratique est donc de minimiser les relations chargĂ©es de façon automatique. En effet, plus on a des relations EAGER, plus on charge des objets qui ne nous sont pas forcĂ©ment utiles. Cela va se traduire par une augmentation du nombre d’allers retours nĂ©cessaires vers la base de donnĂ©es et une augmentation du temps de traitement dĂ©diĂ© au mapping des tables de la base de donnĂ©es vers les entitĂ©s de notre application. De ce fait, il peut ĂȘtre intĂ©ressant d’utiliser au maximum les relations LAZY et de charger les donnĂ©es des relations manquantes seulement lorsqu’elles sont souhaitĂ©es.

ConcrĂštement, il est conseillĂ© de ne conserver un chargement EAGER que pour les relations dont nous sommes sĂ»rs que les donnĂ©es liĂ©es seront toujours utiles (et je pense que ce cas n’est pas si courant). Cela implique de laisser les mĂ©thodes de rĂ©cupĂ©ration par dĂ©faut des relations @OneToMany et @ManyToMany et de forcer le chargement LAZY pour les relations @OneToOne et @ManyToOne. Ceci se matĂ©rialise par l’attribut “fetch” de la relation :

@Entity
public class Produit implements Serializable {
    @ManyToOne(fetch = FetchType.LAZY)
    private CategorieProduit categorie;
}

Cela demande un travail supplĂ©mentaire d’ajustement pour chaque entitĂ© et chaque relation car il va ĂȘtre primordial de crĂ©er de nouvelles mĂ©thodes qui nous permettront de charger l’ensemble des donnĂ©es nĂ©cessaires Ă  une action en un minimum de requĂȘtes. En effet, si on veut afficher l’ensemble des donnĂ©es d’un auteur (ses informations, la liste de ses livres, son adresse, etc.), il sera intĂ©ressant de rĂ©cupĂ©rer l’objet et ses relations en une seule requĂȘte, en utilisant donc des jointures en base de donnĂ©es.

Comment contrĂŽler quelles sont les requĂȘtes effectuĂ©es

Spring Data JPA effectue l’accĂšs aux donnĂ©es Ă  notre place. Cependant, il faut avoir conscience de comment va ĂȘtre effectuĂ© ce traitement. Afin de vĂ©rifier quelles requĂȘtes sont exĂ©cutĂ©es pour rĂ©cupĂ©rer les donnĂ©es en base, il faut activer les logs Hibernate. Pour ce faire, plusieurs solutions sont Ă  notre disposition. Tout d’abord, une option peut ĂȘtre activĂ©e dans spring :

spring:
    jpa:
        show-sql: true

Ou alors, il est possible de le configurer au niveau du fichier de conf du logger :

<logger name="org.hibernate.SQL" level="DEBUG"/>

NB : Dans ces logs, les arguments passĂ©s aux requĂȘtes ne sont pas affichĂ©s (ils sont remplacĂ©s par “?”), mais cela n’empĂȘche pas de voir quelles sont les requĂȘtes exĂ©cutĂ©es.

Comment optimiser la récupération des objets LAZY

Spring Data JPA fournit la possibilitĂ© de spĂ©cifier quels seront les relations chargĂ©es lors d’une requĂȘte de sĂ©lection dans la base de donnĂ©es. Nous allons voir avec diffĂ©rentes mĂ©thodes le mĂȘme exemple : comment rĂ©cupĂ©rer un article avec ses thĂšmes abordĂ©s en une seule requĂȘte.

Méthode n°1 : récupération et chargement des objets avec @Query

L’annotation @Query permet d’écrire une requĂȘte de sĂ©lection grĂące au langage JPQL. On peut ainsi utiliser le mot-clĂ© JPQL “fetch” positionnĂ© sur la jointure des relations de la requĂȘte dans le but de charger ces relations. Dans le repository de l’entitĂ© Article, il est ainsi possible de crĂ©er une mĂ©thode “findOneWithThemesById” en indiquant que la liste des thĂšmes devra ĂȘtre chargĂ©e dans l’instance de l’article rĂ©cupĂ©rĂ© :

@Repository
public interface ArticleRepository extends JpaRepository<Article,Long> {
    @Query("select article from Article article left join fetch article.themes where article.id =:id")
    Article findOneWithThemesById(@Param("id") Long id);
}
Méthode n°2 : Récupération et chargement des objets avec @EntityGraph

Depuis Spring Data JPA 1.10, nous avons la possibilitĂ© d’utiliser l’annotation @EntityGraph afin de crĂ©er des graphes de relations Ă  charger en mĂȘme temps que l’entitĂ© lorsque demandĂ©. Cette annotation s’utilise Ă©galement dans les repositories JPA. La dĂ©finition quant Ă  elle peut se faire soit directement sur la requĂȘte du repository, soit sur l’entitĂ©.

DĂ©finition du graphe sur la requĂȘte

On dĂ©finit sur la requĂȘte les relations qui seront chargĂ©es avec l’entitĂ© grĂące au mot-clĂ© “attributePaths” qui reprĂ©sente la liste de relations (ici une liste d’un seul Ă©lĂ©ment) :

@Repository
public interface ArticleRepository extends JpaRepository<Article,Long> {
    @EntityGraph(attributePaths = "themes")
    Article findOneWithThemesById(Long id);
}
DĂ©finition du graphe sur l’entitĂ©

On peut Ă©galement dĂ©finir ces graphes sur l’entitĂ© grĂące Ă  la notion de NamedEntityGraph arrivĂ© avec JPA 2.1. Le principal avantage est qu’il est possible de se servir de la dĂ©finition de ce graphe dans plusieurs requĂȘtes. Dans ce cas, on spĂ©cifie la liste des relations chargĂ©es grĂące au mot-clĂ© “attributeNodes” qui sont une liste de @NamedAttributeNode. Voici comment l’implĂ©menter sur l’entitĂ© Article :

@Entity
@NamedEntityGraph(name = "Article.themes", attributeNodes = @NamedAttributeNode("themes"))
public class Article implements Serializable {
     ...
}

Puis il est possible de s’en servir de la façon suivante :

@Repository
public interface ArticleRepository extends JpaRepository<Article,Long> {
    @EntityGraph(value = "Article.themes")
    Article findOneWithThemesById(Long id);
}

De plus, il est possible de spĂ©cifier le type de chargement des relations non spĂ©cifiĂ©es avec l’attribut “type” : chargement LAZY de toutes les relations non spĂ©cifiĂ©es ou chargement par dĂ©faut (EAGER ou LAZY suivant le type indiquĂ© sur la relation). Il est Ă©galement possible de crĂ©er des sous-graphes et ainsi travailler de maniĂšre hiĂ©rarchique afin d’ĂȘtre le plus fin possible.

Limitation liĂ©e Ă  l’utilisation de @EntityGraph

Pour ces 2 mĂ©thodes liĂ©es aux graphes d’entitĂ©s, on ne peut, Ă  ma connaissance, pas rĂ©cupĂ©rer une liste contenant l’ensemble des entitĂ©s avec des relations. En effet, pour cela, on aimerait crĂ©er une mĂ©thode qui se dĂ©finirait par exemple comme “findAllWithThemes()” avec comme nƓuds du graphe “themes”. Or ce n’est pas possible, il faut forcĂ©ment utiliser une restriction de recherche (synonyme d’un “where” dans une requĂȘte “select” en base de donnĂ©es).

Pour pallier cette limitation, une solution est de crĂ©er une mĂ©thode “findAllWithThemesByIdNotNull()” : l’identifiant n’étant jamais “null”, toutes les donnĂ©es seront retournĂ©es. L’autre solution est de faire cette requĂȘte avec jointure Ă  l’aide de la premiĂšre mĂ©thode, car l’annotation @Query n’a pas cette limitation.

Ajout de l’information non optionnelle si besoin

Lorsqu’une relation @OneToOne et @ManyToOne a un caractĂšre obligatoire, c’est-Ă -dire que l’entitĂ© possĂšde forcĂ©ment sa relation associĂ©e, il est important de spĂ©cifier Ă  Spring Data JPA que cette relation n’est pas optionnelle. Nous pouvons prendre l’exemple suivant : une personne possĂšde obligatoirement une adresse, qui peut elle-mĂȘme ĂȘtre partagĂ©e par plusieurs personnes. Ainsi, la dĂ©finition de la relation est la suivante :

@Entity
public class Personne implements Serializable {
    @ManyToOne(optional = false)
    @NotNull
    private Adresse adresse;
}

L’ajout de l’information “optional = false” va permettre Ă  Spring Data JPA d’ĂȘtre plus efficace dans la crĂ©ation de ses requĂȘtes de sĂ©lection car il saura qu’il a forcĂ©ment une adresse associĂ©e Ă  une personne. Par consĂ©quent, une bonne pratique est de toujours spĂ©cifier cet attribut lors de la dĂ©finition d’une relation obligatoire.

Points d’attention Attention aux consĂ©quences

Suite au refactoring du code d’une application pour apporter ces fonctionnalitĂ©s, c’est-Ă -dire la modification du chargement des relations de EAGER vers LAZY, certaines rĂ©gressions et bugs peuvent apparaĂźtre. Voici deux exemples trĂšs courants.

Potentielle perte d’information

Le premier effet de bord peut ĂȘtre la perte d’information, par exemple lors de l’envoi d’entitĂ©s via Web Service.

En effet, lorsque l’on modifie par exemple la relation entre Personne et Adresse de EAGER vers LAZY, il faut revoir les requĂȘtes de sĂ©lection des entitĂ©s Personne afin d’ajouter le chargement explicite de leur adresse (avec l’une des mĂ©thodes vues ci-dessus). Sans quoi, il se peut que le Web Service de rĂ©cupĂ©ration d’une personne fournisse uniquement les donnĂ©es propres Ă  la personne et que les donnĂ©es de l’adresse aient disparues. Cela est problĂ©matique car le web service peut ne plus respecter son contrat d’interface, cela peut par exemple impacter l’affichage sur une page web : on s’attend Ă  ce que l’adresse soit renvoyĂ©e car on en a besoin pour l’afficher dans notre vue HTML.

Afin d’éviter ce problĂšme, il est intĂ©ressant d’utiliser des DTO (Data Transfer Object) plutĂŽt que directement renvoyer les entitĂ©s aux clients. En effet, le mapper transformant l’entitĂ© en DTO va charger l’ensemble des relations dont il a besoin en allant rĂ©cupĂ©rer en base de donnĂ©es les relations qui n’ont pas encore Ă©tĂ© chargĂ©es lors de la requĂȘte initiale : il s’agit du Lazy Loading. Ainsi, mĂȘme si l’on ne refactore pas la rĂ©cupĂ©ration des entitĂ©s, le web service continuera de renvoyer les mĂȘmes donnĂ©es.

Potentiel problĂšme de transaction

Le deuxiĂšme effet de bord peut ĂȘtre l’apparition de l’exception “LazyInitializationException”.

Cette exception apparaĂźt lorsque l’on essaye de charger une relation en dehors d’une transaction, le Lazy Loading ne peut alors pas se faire car l’objet est dĂ©tachĂ© : il ne fait pas partie d’une session Hibernate. Cela peut apparaĂźtre aprĂšs les modifications du type de chargement des relations car avant ces modifications, les relations Ă©taient chargĂ©es automatiquement Ă  la rĂ©cupĂ©ration des donnĂ©es en base de donnĂ©es.

L’exception peut dans ce cas ĂȘtre levĂ©e pour 2 principales raisons. La premiĂšre cause peut ĂȘtre que vous n’ĂȘtes pas dans une transaction Hibernate. Dans ce cas, il faut soit rendre le traitement transactionnel (grĂące Ă  l’annotation Spring @Transactional Ă  positionner sur la mĂ©thode ou sur sa classe), soit appeler un service transactionnel qui pourra s’occuper de charger les dĂ©pendances. La deuxiĂšme cause peut ĂȘtre que vous ĂȘtes sortis de la transaction dans laquelle votre entitĂ© avait Ă©tĂ© attachĂ©e, et que l’entitĂ© n’est pas attachĂ©e Ă  votre nouvelle transaction. Dans ce cas, il faut soit charger les relations dans la premiĂšre transaction, soit rĂ©attacher l’objet dans la seconde.

SpĂ©cificitĂ©s des requĂȘtes paginĂ©es

Lorsque l’on souhaite crĂ©er une requĂȘte paginĂ©e devant contenir des informations d’une ou plusieurs relation(s), le chargement des donnĂ©es appartenant aux relations dĂšs la requĂȘte de sĂ©lection est une (trĂšs) mauvaise idĂ©e. Par exemple, lorsque l’on rĂ©cupĂšre la premiĂšre page des articles de l’application avec leurs thĂšmes, il est prĂ©fĂ©rable de ne pas charger directement l’ensemble des donnĂ©es articles+thĂšmes, mais d’abord les donnĂ©es concernant les articles puis les donnĂ©es thĂšmes de ces articles dans un second temps. En effet, sans cela, l’application sera obligĂ©e de rĂ©cupĂ©rer l’ensemble de donnĂ©es de la jointure entre les 2 tables, de les stocker en mĂ©moire, pour ensuite sĂ©lectionner seulement les donnĂ©es de la page demandĂ©e. Ceci est directement liĂ© au fonctionnement des bases de donnĂ©es, elles doivent sĂ©lectionner l’ensemble des donnĂ©es de la jointure mĂȘme si on ne souhaite qu’un fragment des donnĂ©es : que ce soit une page, un min/max ou le top x des rĂ©sultats.

Dans le cas d’un chargement d’une page avec ses relations, un message explicite apparaüt dans les logs pour vous en avertir :

HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!

Plus la volumĂ©trie des 2 tables est importante, plus l’impact sera Ă©levĂ© : avec des tables contenant des millions d’entrĂ©es, cela peut se traduire par un traitement extrĂȘmement coĂ»teux pour l’application.

Pour pallier ce problĂšme, il est donc indispensable de charger dans un premier temps les entitĂ©s sans leurs relations puis de charger celles-ci dans un second temps. Pour charger les donnĂ©es de l’entitĂ© de la page demandĂ©e, on peut utiliser la mĂ©thode findAll(Pageable pageable) du repository JPA (hĂ©ritĂ© de la classe PagingAndSortingRepository). Ensuite, pour charger les donnĂ©es des relations, vous pouvez utiliser le Lazy Loading en appelant directement le getter des relations Ă  rĂ©cupĂ©rer pour chacune des entitĂ©s.

Cette opĂ©ration va ĂȘtre coĂ»teuse car elle va engendrer beaucoup de requĂȘtes. En effet, on aura pour chaque entitĂ© autant de requĂȘtes de sĂ©lection qu’il y a de relations Ă  charger : ce problĂšme est connu sous le nom du “problĂšme du N+1 avec Hibernate”. Si on prend l’exemple d’un chargement d’une page de 20 articles avec une relation Ă  charger, cela se traduira par 21 requĂȘtes : 1 pour la page et 20 pour les thĂšmes de chacun des articles.

Afin de rĂ©duire ce coĂ»t, il est possible d’utiliser l’annotation @BatchSize(size = n) sur la relation @OneToMany ou @ManyToMany entre les deux entitĂ©s. Cela permet d’indiquer Ă  Hibernate d’attendre d’avoir un nombre suffisant (n) de relations Ă  rĂ©cupĂ©rer avant de faire la requĂȘte de sĂ©lection dans la base de donnĂ©es. Ce nombre n est Ă  associer Ă  la taille des pages (cela signifie nĂ©anmoins d’avoir une taille des pages par dĂ©faut car n est dĂ©fini sur l’entitĂ© et donc constant). Ainsi, dans l’exemple prĂ©cĂ©dent, on peut spĂ©cifier le nombre minimal Ă  20 :

@Entity
public class Article implements Serializable {
    @ManyToMany
    @BatchSize(size = 20)
    private Set<Theme> themes = new HashSet<>();
}

Dans ce cas, le nombre de requĂȘtes pour charger la page passera de 21 Ă  2 : 1 pour la page et 1 pour l’ensemble des thĂšmes.

NB : si la page contient moins de 20 Ă©lĂ©ments (donc infĂ©rieur Ă  n), les thĂšmes seront quand mĂȘme bien chargĂ©s.

Aller plus loin

Voici d’autres points intĂ©ressants Ă  creuser pour ne pas voir les performances de son application chuter.

Utilisation d’un cache

Pour booster les performances de votre application, il peut ĂȘtre intĂ©ressant d’utiliser un systĂšme de cache. Il existe tout d’abord le cache de second niveau d’Hibernate. Cela permet de garder en mĂ©moire les entitĂ©s du domaine ainsi que leur relation et donc de rĂ©duire le nombre d’accĂšs Ă  la base de donnĂ©es. Il existe Ă©galement le cache de requĂȘte. En complĂ©ment du cache de second niveau, il est souvent plus utile. Il permet de conserver en mĂ©moire les requĂȘtes effectuĂ©es et leur rĂ©sultat. Il faut cependant porter attention Ă  quelques points lors de l’utilisation de celui-ci :

  • le cache de second niveau est utile uniquement lors des accĂšs aux entitĂ©s par l’id (et par les bonnes mĂ©thodes JPA) ;
  • dans une application distribuĂ©e, il est obligatoire d’utiliser Ă©galement un cache distribuĂ©, ou de ne l’utiliser que sur des objets en lecture seule qui changent rarement ;
  • la mise en place est plus compliquĂ©e lorsque la base de donnĂ©es peut-ĂȘtre altĂ©rĂ©e par d’autres Ă©lĂ©ments (c’est-Ă -dire lorsque l’application n’est pas le point central d’accĂšs aux donnĂ©es).
CrĂ©ation d’index

La crĂ©ation d’index est un passage obligĂ© pour amĂ©liorer les performances d’accĂšs Ă  la base de donnĂ©es. Ceci permet des recherches de sĂ©lection plus rapides et moins coĂ»teuses. Une application dĂ©pourvue d’index sera donc beaucoup moins performante. Quelques prĂ©cisions sur la crĂ©ation d’index :

  • plus la volumĂ©trie de la table est importante, plus l’index portera ses fruits ;
  • un index est gĂ©nĂ©ralement utile que si les donnĂ©es sont suffisamment rĂ©parties (un index sur le sexe avec 50% M et 50% F n’est pas trĂšs efficace) ;
  • un index qui accĂ©lĂšre la lecture va Ă©galement ralentir l’écriture ;
  • les SGBD n’ont pas tous les mĂȘmes politiques d’indexation qui devront donc ĂȘtre Ă©tudiĂ©es, un index peut par exemple ĂȘtre crĂ©Ă© automatiquement ou non Ă  l’ajout d’une clĂ© Ă©trangĂšre suivant le SGBD.
Gestion des transactions

La gestion des transactions est aussi un levier important d’optimisation : il est nĂ©cessaire de bien les utiliser si l’on ne veut pas voir apparaĂźtre des problĂšmes de performance. Par exemple, leur mise en place Ă©tant coĂ»teuse pour l’application, il est prĂ©fĂ©rable de ne pas en utiliser dans les cas oĂč elles ne sont pas nĂ©cessaires. Par dĂ©faut, Hibernate va attendre la fin de la transaction Spring avant de mettre Ă  jour l’ensemble des donnĂ©es dans la base (sauf dans les cas oĂč un flush de persistance intermĂ©diaire lui est nĂ©cessaire). Pendant ce temps, il va stocker les mises Ă  jour en mĂ©moire. Si une transaction concerne une volumĂ©trie de donnĂ©es trĂšs importante, on peut rencontrer des problĂšmes de performances. Dans ce cas, il est prĂ©fĂ©rable de dĂ©couper le traitement en plusieurs transactions. De plus, lorsque la transaction ne fait que de la lecture, il faut indiquer Ă  Spring qu’elle est en lecture seule afin d’amĂ©liorer les performances :

@Transactional(readOnly = true)
Retour d’expĂ©rience

On m’a rĂ©cemment demandĂ© de travailler sur les performances d’une application web. MalgrĂ© un premier travail sur certaines parties pour amĂ©liorer les performances globales, l’application continuait de provoquer une saturation des serveurs sur lesquels elle Ă©tait installĂ©e (surtout en charge CPU), les dĂ©lais d’attente pour chaque requĂȘte Ă©taient trĂšs Ă©levĂ©s et on semblait avoir atteint la volumĂ©trie maximale que l’application pouvait supporter. En remarquant le nombre de requĂȘtes effectuĂ©es constamment vers la base de donnĂ©es (Ă  l’activation des logs Hibernate) et en regardant dans le code comment Ă©taient produites ces requĂȘtes, j’ai dĂ©cidĂ© d’appliquer les quelques conseils de cet article afin de donner un peu d’air Ă  l’application.

Procédé utilisé

L’application web sur laquelle je suis intervenu a la particularitĂ© de fonctionner beaucoup plus sous la forme de batch (actions faites toutes les 1 Ă  60 secondes selon les jobs) que d’actions utilisateur. Afin de rĂ©duire ces problĂšmes de performance, j’ai essayĂ© de procĂ©der de maniĂšre itĂ©rative, en me prĂ©occupant d’un traitement Ă  la fois dans l’ordre dĂ©croissant de la frĂ©quence des jobs : d’abord ceux qui sont effectuĂ©s chaque seconde jusqu’à ceux effectuĂ©s chaque minute.

Pour chaque job, je comptais le nombre de requĂȘte Hibernate de sĂ©lection effectuĂ©es pour rĂ©aliser le traitement et j’essayais de voir si chacune Ă©tait nĂ©cessaire ou s’il s’agissait de donnĂ©es rĂ©cupĂ©rĂ©es Ă  tort. DĂšs que je trouvais une incohĂ©rence dans ce qui Ă©tait chargĂ© depuis la base de donnĂ©es, je modifiais le type de relation (EAGER Ă  LAZY), je crĂ©ais des requĂȘtes adaptĂ©es dans les repositories et je favorisais les critĂšres de sĂ©lection sur la requĂȘte (contrairement Ă  un filtre appliquĂ© Ă  posteriori sur la liste des donnĂ©es rĂ©cupĂ©rĂ©es).

Le but Ă©tait de rĂ©duire le nombre de requĂȘte effectuĂ©es Ă  un nombre minimum (une seule si possible) et de ne charger que les objets nĂ©cessaires Ă  chaque traitement.

Afin de vĂ©rifier si la charge sur la base de donnĂ©es (et le chargement des donnĂ©es) diminuait, j’ai dĂ©cidĂ© de me baser sur 2 mĂ©triques : le nombre de requĂȘtes de sĂ©lection et le nombre d’objets chargĂ©s par minute. Pour cela, j’ai utilisĂ© une mĂ©thode assez simpliste : je redirigeais les logs de l’application sur 1 minutes, et de ce fichier, je comptais :

  • le nombre de requĂȘtes de sĂ©lection : nombre d’occurrences de “SELECT 
” ;
  • le nombre d’objets chargĂ©s : nombre d’occurrences de “SELECT 
” additionnĂ© au nombre d’occurrences de “… join fetch 
”.
RĂ©sultats

AprĂšs quelques jours de travail sur ce sujet et en appliquant seulement ces quelques procĂ©dures, j’ai rĂ©ussi Ă  diviser environ par 5 le nombre de requĂȘtes effectuĂ©es en base de donnĂ©es et par environ 7 le nombre total d’objets chargĂ©s.

Avec la nouvelle version de l’application en production, la charge du serveur s’est vraiment amoindrie. Pour preuve, voici l’état du serveur avant la nouvelle version :

On voit une utilisation importante et presque inquiĂ©tante de l’ensemble des ressources du serveur. AprĂšs le passage Ă  la nouvelle version, voici l’état du serveur :

L’usage global du serveur a Ă©tĂ© considĂ©rablement diminuĂ© grĂące Ă  quelques petits ajustements, simples Ă  mettre en place. Il est donc nĂ©cessaire pour moi de garder Ă  l’esprit ces problĂ©matiques lors du dĂ©veloppement d’une application.

Conclusion

AprĂšs avoir Ă©noncĂ© les diffĂ©rents types de relations entre entitĂ©s et leur mĂ©thode de chargement par dĂ©faut, nous avons compris pourquoi il Ă©tait important de favoriser au maximum un chargement LAZY des relations entre entitĂ©s. Nous avons appris comment rĂ©cupĂ©rer un maximum de donnĂ©es en un minimum de requĂȘtes, notion d’autant plus importante lors de l’utilisation des chargements LAZY, ceci grĂące Ă  quelques outils fournis par Spring Data JPA. Avec un exemple sous la forme d’un retour d’expĂ©rience, nous avons vu Ă  quel point il est important de respecter ces quelques conseils si on ne veut pas voir apparaĂźtre des chutes de performances importantes dans nos applications.

Ce qui est important de retenir est que chaque demande d’informations Ă  la base de donnĂ©es doit ĂȘtre justifiĂ©e, qu’il ne faut pas rĂ©cupĂ©rer de donnĂ©es qui ne nous sont pas utiles, et qu’il est prĂ©fĂ©rable de minimiser le nombre de requĂȘte de sĂ©lection pour chaque demande d’informations (une seule si possible par demande).
Ce travail est continu, il doit ĂȘtre effectuĂ© au fil de l’eau lors de la rĂ©alisation de l’application. Sinon, il nous faudra nous pencher sur ces problĂ©matiques lorsque les problĂšmes de performances apparaĂźtront, au risque de connaĂźtre des rĂ©gressions lors des tentatives de rĂ©solution. Il ne faut pas non plus tomber dans l’excĂšs, certaines actions ne nĂ©cessitent pas forcĂ©ment un investissement de temps consĂ©quent. Ce travail est surtout primordial lorsqu’une action est rĂ©alisĂ©e trĂšs souvent, et lorsque l’on rĂ©cupĂšre des grappes de donnĂ©es imposantes.

Ressources :

Blog JPA :

L’article Boostez les performances de votre application Spring Data JPA est apparu en premier sur Le Blog d'Ippon Technologies.

Catégories: Blog Société

Kafka 0.11.0 == ♄

mar, 07/11/2017 - 09:55

Le 22 juin dernier Kafka sortait sa derniÚre version : la 0.11. Je vais revenir sur les nouveautés de cette version, car elle introduit des concepts trÚs intéressants voire innovants pour un outil distribué comme Kafka !

PS : pour ceux n’ayant pas de background avec Kafka je vous conseille de regarder la vidĂ©o suivante qui constitue une bonne introduction, quelques modifications ont nĂ©anmoins eu lieu avec la nouvelle version ! #AutoPub

Headers de message

Allez, on commence doucement avec cette premiĂšre fonctionnalitĂ©. La possibilitĂ© d’ajouter des headers Ă  un message Kafka. La spec provient de la KIP-82. Comment ça marche ? L’ajout de headers se fait via une modification du protocole d’envoi vers le broker Kafka ainsi que d’ajouts d’interfaces de manipulation de ces headers pour les clients (producer / consumer). On a donc maintenant un champ spĂ©cifique comme dĂ©crit ci-dessous :

Message =>

       Length => varint

       Attributes => int8

       TimestampDelta => varlong

       OffsetDelta => varint

       KeyLen => varint

       Key => data

       ValueLen => varint

       Value => data

       Headers => [Header] <------------ NEW Added Array of headers

Header =>

       Key => string (utf8) <- NEW UTF8 encoded string (uses varint length)

       Value => bytes  <- NEW header value as data (uses varint length)

C’est assez pratique pour passer des informations en plus de votre contenu pour une utilisation par le consommateur par exemple. Ce n’est pas trĂšs rĂ©volutionnaire car commun pour les queues de messaging. Mais c’est un ajout sympathique. Je suis personnellement intĂ©ressĂ© par cette feature Ă  des fins de tracing de requĂȘtes asynchrones
 Je dĂ©rive &#x1f642;

Exactly once

Bon maintenant, on rentre dans le vif du sujet. Cet article est un peu une excuse pour vous parler de la fonctionnalitĂ© phare de cette version : le support de la garantie de dĂ©livrance d’un message une fois exactement (ça rend pas bien en français donc je parlerai de “exactly once”).

Avant, il y a bien 1 semaine, “exactly once” ça semblait ĂȘtre de la fiction. De nombreuses personnes considĂ©raient la fonctionnalitĂ© impossible dans un environnement distribuĂ©. En effet, c’est un des problĂšmes trĂšs complexes de garantir l’envoi unique d’un message dans un outil distribuĂ©. Illustrons cela :

J’envoie un message Ă  Kafka, le broker le reçoit, mais met trop de temps Ă  rĂ©pondre. Typiquement c’est le genre de panne que l’on peut attendre du rĂ©seau (CF “Fallacies_of_distributed_computing” ). Kafka prĂ©voyait le coup et permettait (via des options) d’envoyer des messages de retry lorsque l’ACK n’arrivait pas. Ainsi on pouvait choisir entre le fait d’avoir “at most once” si on ne rĂ©essaie  pas et “at least once” si on rĂ©essaie jusqu’à la rĂ©ussite. Pourquoi “at most once” me direz vous ? Et bien si on reçoit un timeout lors de l’ACK, simplement Ă  cause d’une lenteur rĂ©seau et que l’on a rĂ©ellement Ă©crit le message dans le commit log, le producer renvoie et Badaboum, un doublon.

Alors comment contournent-ils cela ?

Idempotence

Ça veut dire que si on envoie deux fois la mĂȘme chose via un producer, on a tout le temps le mĂȘme rĂ©sultat. Le producer va avoir un petit ID dans ses batchs de message permettant de les dĂ©doublonner. Tant que l’on n’a pas de rĂ©ponse de Kafka on renvoie et si Kafka reçoit des doublons il gĂšre ! Pour l’activer rien de plus simple, il suffit de mettre le paramĂštre enable.idempotence=true. En plus de tout ça, les fameux IDs sont Ă©crits dans un log, du coup c’est rĂ©silient en cas de panne.

C’est beau.

Support des transactions

Ça y est, on rentre dans des choses intĂ©ressantes. Kafka, c’est un log distribuĂ© : OK ! On peut produire et lire des messages dans un topic donnĂ© (streaming ou non). Avant on produisait un message et on continuait nos traitements sans avoir de garantie, en cas de prĂ©cĂ©dence entre nos messages, que tous seraient envoyĂ©s. Avec le support des transactions, on a maintenant une syntaxe proche des transactions de nos bases de donnĂ©es :

producer.initTransactions();
try {
  producer.beginTransaction();
  producer.send(record1);
  producer.send(record2);
  producer.commitTransaction();
} catch(ProducerFencedException e) {
  producer.close();
} catch(KafkaException e) {
  producer.abortTransaction();
}
kafka-transactions.java affichage brut

On a donc la possibilitĂ© d’effectuer des Ă©critures atomiques sur diffĂ©rentes partitions d’un topic donnĂ©. C’est plutĂŽt cool et cela apporte de nouveaux use-cases pour Kafka. Vous savez Kafka Streams fait typiquement du “read => transform => send”. Tu fais ça dans une transaction et tu as du streaming en exactly once ! Encore une fois une simple propriĂ©tĂ© permet d’assurer cette garantie : processing.guarantee=exactly-once.

Performances ?

Le support de la fonctionnalitĂ© n’est pas automatique. Du coup si vous ne l’utilisez pas, mĂȘme avec l’ajout des headers, vous aurez de meilleures performances. Un travail a Ă©tĂ© effectuĂ© de ce cĂŽtĂ©-lĂ  notamment au niveau de la compression des messages Kafka sur le rĂ©seau et sur disque. Du coup, on parle quand mĂȘme de 50% d’augmentation en termes de dĂ©bit !

Et lĂ  vous vous demandez, “et pour le exactly once ?”. En fait il y a bien un overhead sur l’utilisation de la fonctionnalitĂ©. On parle d’environ 3% ou 20% pour l’utilisation du ”exactly once” (en comparaison de “at-least“ ou “at-most” respectivement). Mais pas besoin d’ĂȘtre triste, en fait le but avouĂ© est de faire du “exactly once” la norme et donc de l’activer par dĂ©faut. C’est possible grĂące au gain de performance des autres parties du protocole dĂ©crit plus haut ! Alors mangez-en !

Pour conclure : Idempotence + Transactions multi partitions = Exactly once pour tous les usages de Kafka = ♄

Pour une description complĂšte des nouvelles features, des amĂ©liorations ou des bug corrigĂ©s, les releases-notes sont disponibles ici : http://www.mirrorservice.org/sites/ftp.apache.org/kafka/0.11.0.0/RELEASE_NOTES.html. Les articles de Confluent, dont je me suis grandement inspirĂ©, rentrent bien plus dans le dĂ©tail que moi concernant le exactly-once et peuvent ĂȘtre trouvĂ©s dans la KIP-98 (attention c’est long), dans un article de lancement de @nehanarkhede ou dans un blog de @jaykreps pour les septiques.

L’article Kafka 0.11.0 == ♄ est apparu en premier sur Le Blog d'Ippon Technologies.

Catégories: Blog Société

Partagez la connaissance

Partagez BlogsdeDeveloppeurs.com sur les réseaux sociaux