I've read a lot and also tried a lot already and I'm confused and stuck so I will probably add to much information... sorry upfront.
I'm working with micronaut and want to build a backend for a small website. I decided to package by feature mostly so my package structure looks like:
site
└───server
│ Application.java
│
├───api
│ LayoutLinkController.java
│
├───configuration
│ DatabaseConfiguration.java
│ DatabaseConfigurationProperties.java
│
├───layout
├───dao
│ LinkRepository.java
│ LinkRepositoryImpl.java
│
└───model
Link.java
LinkType.java
My Controller directly relies on Link and LinkRepository where I use the Model Class Link as body parameter for my post and put calls, respectively saving and updating the same through the repository, like so:
@ExecuteOn(TaskExecutors.IO)
@Controller(ApiUriGenerator.LAYOUT_URI)
@Secured(SecurityRule.IS_ANONYMOUS)
public class LayoutLinkController {
private static Logger LOGGER = LoggerFactory.getLogger(LayoutLinkController.class);
/**
* the suffix of this endpoint
*/
private static final String ENDPOINT_NAME = "/links";
private final LinkRepository linkRepository;
LayoutLinkController(LinkRepository linkRepository) {
this.linkRepository = linkRepository;
}
/**
* Gets the link with the given id
*
* @author deckerdent
* @since 0.0.1
* @version 0.0.1
*
* @param id a link id
* @return a link
*/
@Get(ENDPOINT_NAME + "({id})")
@Secured(SecurityRule.IS_ANONYMOUS)
HttpResponse<?> getById(long id) {
Link link = linkRepository.findById(id).orElse(null);
return link != null ? HttpResponse.ok(link) : HttpResponse.notFound();
}
/**
* Gets a list of all available footer links
*
* @author deckerdent
* @since 0.0.1
* @version 0.0.1
*
* @return an array of links
*/
@Get(ENDPOINT_NAME)
@Secured(SecurityRule.IS_ANONYMOUS)
HttpResponse<?> getAll() {
return HttpResponse.ok(linkRepository.findAll());
}
/**
* Creates a link
*
* @author deckerdent
* @since 0.0.1
* @version 0.0.1
*
* @return the created link
*/
@Post(ENDPOINT_NAME)
@Secured(SecurityRule.IS_ANONYMOUS)
HttpResponse<?> create(@Body @Valid Link link) {
if (link.getId() != 0) {
return HttpResponse.badRequest(new Exception("nope").getMessage());
}
link = linkRepository.save(link);
return HttpResponse.created(link, URI.create(ApiUriGenerator.LAYOUT_URI + ENDPOINT_NAME + "/" + link.getId()));
}
/**
* Updates a link
*
* @author deckerdent
* @since 0.0.1
* @version 0.0.1
*
* @return the updated link
*/
@Put(ENDPOINT_NAME)
@Secured(SecurityRule.IS_ANONYMOUS)
HttpResponse<?> update(@Body @Valid Link link) {
link = linkRepository.update(link);
return HttpResponse.ok(link);
}
/**
* Deletes a link
*
* @author deckerdent
* @since 0.0.1
* @version 0.0.1
*
* @param id the id of the link
* @return 200
*/
@Delete(ENDPOINT_NAME)
@Secured(SecurityRule.IS_ANONYMOUS)
HttpResponse<?> delete(long id) {
linkRepository.deleteById(id);
return HttpResponse.ok();
}
}
This is the Model
@Entity
@Table(name = "layout_links")
public class Link {
@Id
@GeneratedValue(strategy = AUTO)
private long id;
@NotNull
@Column(name = "title", nullable = false, unique = true)
private String title;
@NotNull
@Column(name = "type", nullable = false)
private LinkType type;
@NotNull
@Column(name = "url", nullable = false)
private String url;
public Link() {
}
public Link(String title, LinkType type, String url) throws MalformedURLException, URISyntaxException {
boolean isValid = this.validateUrl(url);
if (isValid) {
this.title = title;
this.type = type;
this.url = url;
}
}
public long getId() {
return id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public LinkType getType() {
return type;
}
public void setType(LinkType type) {
this.type = type;
}
public String getUrl() {
return url;
}
public void setUrl(String url) throws MalformedURLException, URISyntaxException {
boolean isValid = this.validateUrl(url);
if (isValid) {
this.url = url;
}
}
private boolean validateUrl(String url) throws MalformedURLException, URISyntaxException {
new URL(url).toURI();//throws exception if url is not valid.
return true;
}
}
Now I get hibernate exceptions as responses in postman if I e.g. send a post request with an ID given (PersistentObjectException) or if one of the unique columns already hold the value I try to insert/update (ContraintViolationException). And I'm trying to catch these exceptions but couldn't and I guess for the same reason as here. I don't want the exception message be printed as a response with http code 500 but respond with more speaking messages.
I didn't wrap the repository into a service class yet as I thought it's just not more than persisting and reading data and there will no additional logic be in the service which only means redundand code. Also, if I go with a service class this should be my single API to everything else in the package and then using the model class Link in my controller which is in another package would be breaking this pattern, which means I would have to either create a dto class for my Link class outside my layout feature package which I feel is not a good practice or I'd have to put the controller within my feature package which I don't like as well. But I'm unsure about what good architecture is here and this is actually why I started the project. Learning patterns, architecture and stuff as I already now how to write code. I could easily find work arounds with additional database calls (e.g. a select to test if an object exists and if it equals the object to update to avoid Constraint Violation) but this is all hackish and I want to work on quality.
So what's the recommended way to go here? Is there a way to catch the hibernate exceptions right in the controller? Should I wrap the repository into a service class first even if it seems unnecessary only to then be able to catch the exceptions? Should I manually test e.g. with additional database calls?