#{extends '../main.html'/}# #{set title:'Webpieces QuickStart'/}# #{set tab:'management'/}# #{renderTagArgs 'docHome.html'/}# #{renderTagArgs 'quickStartList.html'/}#
Last updated: Sept 14th, 2020
Before we start, let's checkout the in-memory db plugin for webpieces. Go to http://localhost:8080/@db and for the JDBC URL, change it to 'jdbc:h2:mem:test' so it hits the in-memory database. All other settings are fine. Then click the Connect button. Notice there is a few tables from the examples generated from webpieces template creation. We will revisit this later in this example. In this quickstart, we'll be creating a table called "CAR" that you will able to access in this in-memory database.
Ideally however in a dynamic web application, we would like to let people persist data for a later time and modify that data. A Hibernate plugin is installed by default for rapid prototyping though you can remove the hibernate plugin from the build files if you desire and use something else. In the meantime, welcome to our first hibernate entity bean. Create a class "CarDbo.java" in the org.webpieces.helloworld.db package
It MUST be in the db package as the hibernate plugin is setup to scan from the location of DbSettingsProd.java all directories and subdirectories for entities. This makes scanning 'much' faster as it only scans a subset of your project for database entities
package org.webpieces.helloworld.db;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
//NOTE: More can be found in the hibernate documenation
//This tells hibernate this is an entity bean and will be stored
@Entity
//@Table informs hibernate of more details on the table such as name
@Table(name="CAR")
//@NamedQueries is where we define queries we will make into the database
@NamedQueries({
//For more info on the query language, see the hibernate HQL (Hibernate Query Language)
//for details
@NamedQuery(name = "findByName", query = "select u from CarDbo as u where u.name = :name"),
})
public class CarDbo {
//@Id tell hibernate this is the primary key
@Id
//This annotation says grab 10 ids at a time in memory on this host so we don't hit the database on every create row to
//populate the id. The name needs to match the generator below. It is good idea to always rename these so be
//careful of cut/paste
@SequenceGenerator(name="car_id_gen",sequenceName="car_sequence" ,initialValue=1,allocationSize=10)
//This tells hibernate to just generate the ids in sequence
@GeneratedValue(strategy=GenerationType.SEQUENCE,generator="car_id_gen")
private Integer id; //You almost always want to use Integer over int and Long over long so it can be null so you can tell if it has been persisted
//Use @Column to further define the column like forcing it to be unique
@Column(unique = true)
private String name;
private String style;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getStyle() {
return style;
}
public void setStyle(String style) {
this.style = style;
}
//This method re-uses the @NamedQuery we defined above
public static CarDbo findByName(EntityManager mgr, String name) {
Query query = mgr.createNamedQuery("findByName");
query.setParameter("name", name);
try {
return (CarDbo) query.getSingleResult();
} catch (NoResultException e) {
return null;
}
}
}
Next, let's add our RouteIds in MyRouteId for displaying a page to create a car, displaying a page to edit a car, and finally for actually posting a car to the database:
package org.webpieces.helloworld.web.myapp;
import org.webpieces.router.api.routes.RouteId;
public enum MyRouteId implements RouteId {
HELLO_WORLD, DYNAMIC_HELLO_WORLD,
GET_CREATE_CAR_PAGE, GET_EDIT_CAR_PAGE, POST_CAR
}
Then, let's add our new routes for the route ids we just added
package org.webpieces.helloworld.web.myapp;
import static org.webpieces.ctx.api.HttpMethod.GET;
import static org.webpieces.ctx.api.HttpMethod.POST;
import org.webpieces.router.api.routes.Port;
import org.webpieces.router.api.routes.Routes;
import org.webpieces.router.api.routebldr.DomainRouteBuilder;
import org.webpieces.router.api.routebldr.RouteBuilder;
public class MyMainRoutes implements Routes {
@Override
public void configure(DomainRouteBuilder bldr) {
RouteBuilder b = bldr.getAllDomainsRouteBuilder();
b.addRoute(Port.BOTH, GET, "/helloworld", "MyMainController.helloWorld", MyRouteId.HELLO_WORLD);
b.addRoute(Port.BOTH, GET ,"/helloworld/{name}/{id}", "MyMainController.dynamicHelloWorld", MyRouteId.DYNAMIC_HELLO_WORLD);
//route to render the page of creating a new user
b.addRoute(Port.BOTH, GET ,"/car/new", "MyMainController.carAddEdit", MyRouteId.GET_CREATE_CAR_PAGE);
//route to render the page of editing an existing user
b.addRoute(Port.BOTH, GET ,"/car/edit/{name}", "MyMainController.carAddEdit", MyRouteId.GET_EDIT_CAR_PAGE);
//route to save a new or existing user
b.addRoute(Port.BOTH, POST,"/car/post", "MyMainController.postSaveCar", MyRouteId.POST_CAR);
}
}
Notice that here we call carAddEdit for both creating a car and editing one. If someone types in http://localhost:8080/car/new, then the name variable will equal null when calling carAddEdit method telling us it is a brand new car. Next, let's add the carAddEdit method to our controller
public Action carAddEdit(String name) {
if(name == null) {
return Actions.renderThis("entity", new CarDbo());
}
CarDbo car = CarDbo.findByName(Em.get(), name);
if(car == null)
throw new NotFoundException("car does not exist");
return Actions.renderThis("entity", car);
}
You will need to add three new imports:
and add a page called carAddEdit.html that will be rendered as well
*[
*{
a comment where tags don't run ${invalidParamThatWouldThrowExceptionIfNotCommented}$
AND these comments are stripped from html and 'not' delivered to clients where users can read your comments
}*
#{form action:@[POST_CAR]@, class:'form-horizontal', style:'min-width:500px;max-width:800px;margin: 0 auto'}#
Car
#{field 'entity.name', label:'Name'}##{/field}#
#{field 'entity.style', label:'Style'}##{/field}#
#{/form}#
]*
Ok, so there is a lot going on here that we need to explain.
*[COMMENT tag - The *{ }* is how you 'always' should comment in templates. Rather than use html commenting style (), using *{ }* will prevent tags from running in your comments. An html comment will not do this. Also, html comments are sent back to browsers so any user can read your comments which sometimes is a bit dangerous. Always comment with *{}* in webpieces. ]*
*[FORM tag - The first tag #{form}# will generate the form html for you with the correct url. Any params like class and style in the form tag are converted as well into the final form html element.]*
*[Hidden input - Then there is a hidden input which will have the id of the database entity if it is being edited. This allows us to re-use the same html and java controller code for adds and edits(quite nice). This will be blank if we are doing an add.]**[FIELD tag - Next, we have the #{field}# tags which do a ton of work under the covers so you can style every field in every location of your application consistently with where the error for that field goes, tooltip help and even i18n stuff. Lastly, the #{field}# tag also helps with not blowing away what the user typed in when the page refreshes due to errors so you don't do extra work for that either.]*
*[ Now that this is all setup, as long as you did not stop your development server, go to http://localhost:8080/car/new and you should see a small form on the page with a save button that will not work and will return a 5XX when you click it displaying 'You encountered a bug in our server'. Click the save button, and then look at your logs and scroll up to the logs in red(YES YES!! errors and warnings are in red so they are damn easy to find and are typically very detailed as to what is wrong!). If you stopped your development server however, it will not startup since there is a route with a method that does not exist. Either way, we need to fix the POST of the Car so let's do that next. Add the following method to the controller]*
public Redirect postSaveCar(CarDbo entity) {
if(entity.getName() == null) {
Current.validation().addError("entity.name", "name is required");
} else if(entity.getName().length() < 3) {
Current.validation().addError("entity.name", "name not long enough");
}
if(entity.getStyle() == null) {
Current.validation().addError("entity.style", "style is required");
} else if(entity.getStyle().length() < 3) {
Current.validation().addError("entity.style", "style not long enough");
}
//all errors are grouped and now if there are errors redirect AND fill in
//the form with what the user typed in along with errors
//we redirect so the back button always works
if(Current.validation().hasErrors()) {
FlashAndRedirect redirect = new FlashAndRedirect(Current.getContext(), "Errors in form below");
redirect.setIdFieldAndValue("id", entity.getId());
return Actions.redirectFlashAll(
MyRouteId.GET_CREATE_CAR_PAGE, MyRouteId.GET_EDIT_CAR_PAGE, redirect);
}
Current.flash().setMessage("User successfully saved");
Current.flash().keep(true);
Current.validation().keep(true);
Em.get().merge(entity);
Em.get().flush();
return Actions.redirect(MyRouteId.HELLO_WORLD);
}
Add the proper imports through the IDE you are using. In the case of multiple same-named imports, select the ones from webpieces package.
We first validate each thing we need to and keep adding errors to each field that is wrong. Then, we check if validation hasErrors() and if it does, we redirect back to the edit or add page depending on if we are editing or adding the entity. If there are no errors, we process to call Em.get().merge(entity) will save or update the CarDbo in our database. Em.get().flush() ensures all changes are persisted such that you can call persist, delete and then at the end if you discover some sort of validation went wrong, don't flush and none of it will be saved.
Follow these next steps exactly so we can demonstrate some real power of details.
Next, let's use the @db plugin to view the in-memory database. Go to http://localhost:8080/@db and for the JDBC URL, change it to 'jdbc:h2:mem:test' so it hits the in-memory database. All other settings are fine. Then click the Connect button. Click on the 'CAR' table which inserts a select statement in the GUI and then click 'Run'. Now, all rows in the table are shown, which in this case is the single row with ferrari.
Lastly, let's test out updating our ferrari so go to http://localhost:8080/car/edit/ferrari and you can then update your ferrari and re-query the database
Next, try http://localhost:8080/car/edit/notexist and we will see that we get the Development only NotFound page, HOWEVER we didn't want to leave you hanging sooooo, if you scroll all the way down, you will see the page that will be displayed in production
Now, this example was very crude(CRUDe? - dad jokes) so instead let's now give you a complete CRUD example with listing entities and everything and how little code is required in webpieces to do such a thing. Not only that, this is the de-facto pattern such that code generation can be added to a development plugin which is our goal since after all, go through the plugin's wizard, it generates code, and you don't even restart the server
Next Basic CRUD