1
\$\begingroup\$

The following is the story of a UserCredentialsDto that arrived at one of my endpoints and how it finds its way through all the layers namely

  1. REST Layer
  2. Service Layer
  3. Repository Layer
  4. Persistence Layer

The example below is very simple and one could argue that anything more complex goes towards over engineering but I know for a fact that things get more complicated over time. I have two major issues for which I do not have a "clean" solution:

  1. I am mapping DTOs to Models and vice versa in ther Service Layer
  2. The "business logic" gets somehow mixed up within the Service Layer

I have thought about different solutions but none of them make the current state simpler.

So, once upon a time there was a

public class AppUserCredentialsDto {
    // ...
}

which found it's way into the


REST Controller Layer

The first layer is very straight forward. Receiving the DTOs, pass it to the Service Layer and handle exceptions in case there are some.

@RestController
@RequestMapping("/pub")
public class AppUserController {

    @Autowired
    private AppUserService appUserService;

    @RequestMapping(value = "/register", method = RequestMethod.POST)
    public AppUserDto register(@RequestBody AppUserCredentialsDto appUserCredentials) {
        return this.appUserService.register(appUserCredentials);
    }
}

Service Layer

In the Service Layer it gets messier. The DTO gets mapped into a Model (Hibernate managed POJO), some other business logic happens there (hash password) and it's getting passed to the Repository Layer where, in this case, the data gets persisted.

@Service
public class AppUserService {

    @Autowired
    private AppUserRepository appUserRepository;

    @Transactional
    public AppUserDto register(AppUserCredentialsDto appUserCredentialsDto) {

        String salt = BCrypt.gensalt();
        AppUserModel appUserModel = new AppUserModel();
        appUserModel.setUsername(appUserCredentialsDto.getUsername());
        appUserModel.setPassword(BCrypt.hashpw(appUserCredentialsDto.getPassword(), salt));
        appUserModel.setPasswordSalt(salt);

        this.appUserRepository.add(appUserModel);

        appUserModel = this.appUserRepository.findByUsername(appUserCredentialsDto.getUsername());

        return AppUserDto.fromModel(appUserModel);
    }

}

Repository Layer

Here, Model objects are received and e.g. persisted of returned based on certain criteria.

@Repository
public class AppUserRepository extends AbstractRepository {

    public void add(AppUserModel appUser) {
        Session session = this.getCurrentSession();
        session.save(appUser);
    }

    public AppUserModel findByUsername(String username) {

        Session session = this.getCurrentSession();

        CriteriaBuilder cb = session.getCriteriaBuilder();

        CriteriaQuery<AppUserModel> query = cb.createQuery(AppUserModel.class);

        Root<AppUserModel> appUserTable = query.from(AppUserModel.class);

        CriteriaQuery<AppUserModel> appUserQuery = query.select(appUserTable)
                .where(cb.equal(appUserTable.get("username"), username));

        AppUserModel appUserModel = session.createQuery(appUserQuery).getSingleResult();

        return appUserModel;
    }

}

Persistence Layer

There's nothing to say about this layer since it is entirely managed by Hibernate. Let me call these objects Data Access Objects and please correct me if that terminology is wrong because I am not 100% sure if @Entity @Table objects can actually referred to as DAOs.

@Entity
@Table(name = "app_user")
public class AppUserModel extends AbstractTimestampEntity {

    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable=false, unique = true)
    private String username;

    // ..
}

Although this is just a short example it already shows some of the issue my current design carries with it. As mentioned before, I have no clean solution for the mapping part yet. In this example it's just a few lines but it's getting messier of course if I have larger collections of data. I do not want to end up having multiple nested loops in the Service Layer, and maybe more complex logic, in order to map a DTO into a Model. I know one could start writing dedicated Mapper classes which provide static functions in order to do the job:

public final class AppUserMapper {
    private AppUserMapper() {}

    public static AppUserDto toDto(AppUserModel appUserModel) {
        // ..
    }

    public static AppUserModel toDto(AppUserDto appUserDto) {
        // ..
    }
}

but this can also become rather ugly over time.


Current "Solution"

One way I could move this code out of the Service Layer would be to introduce my missing Business Layer. I think my problem here is that I don't know at this point how this layer is supposed to communicate with the others. Imagine a business/domain object like this:

public class AppUser extends DomainObject {

    @Autowired
    private AppUserRepository appUserRepository;

    private AppUserModel appUserModel = new AppUserModel();

    public AppUser(AppUserCredentialsDto userCredentialsDto) {
        String salt = BCrypt.gensalt();
        this.appUserModel.setUsername(userCredentialsDto.getUsername());
        this.appUserModel.setPassword(BCrypt.hashpw(userCredentialsDto.getPassword(), salt));
        this.appUserModel.setPasswordSalt(salt);
    }

    public AppUser(String username) {
        this.appUserModel = this.appUserRepository.findByUsername(username);
    }

    public void create() {
        this.appUserRepository.add(this.appUserModel);
    }

    public void update() {
        this.appUserRepository.update(this.appUserModel);
    }

    public void setUsername(String username) {
        this.appUserModel.setUsername(username);
        this.appUserRepository.update(this.appUserModel);
    }

    @Override
    public AppUserDto toDto(AppUserModel appUserModel) {
        AppUserDto appUser = new AppUserDto();
        appUser.setUsername(appUserModel.getUsername());
        appUser.setActivated(appUserModel.getActivated());
        return appUser;
    }

}

which would simplify the Service Layer to:

@Service
public class AppUserService {

    @Transactional
    public AppUserDto register(AppUserCredentialsDto appUserCredentialsDto) {
        AppUser appUser = new AppUser(appUserCredentialsDto);
        appUser.create();
        return appUser.toDto();
    }
}

and although this looks nice so far I am not sure whether I am over complicating things more than necessary, not seeing a significant flaw with this design and run straight ahead in massive issues in the long run or whether both of those statements are true.

So, I really hope somebody takes a look at this example and can offer me some advice on how I can improve things.

If something is not entirely clear please let me know and I'm happy to provide further information if it helps.

\$\endgroup\$
1
\$\begingroup\$

It's a lot to explain here, but in a nutshell, I would simplify all of this using spring-data-rest and do the following

  1. Get rid of the REST/persistence/dao layers you have
  2. Refactor your services to use the Spring Data events So you're public AppUserDto register(AppUserCredentialsDto appUserCredentialsDto) would become an @HandleBeforeSave handler for the AppUser looking like this:

    @RepositoryEventHandler
    public class AppUserService {
    
    @Autowired
    private AppUserRepository appUserRepository;
    
    @HandleBeforeSave
    public void register(AppUserCredentialsDto appUserCredentialsDto) {
    
        String salt = BCrypt.gensalt();
        AppUserModel appUserModel = new AppUserModel();
        appUserModel.setUsername(appUserCredentialsDto.getUsername());
        appUserModel.setPassword(BCrypt.hashpw(appUserCredentialsDto.getPassword(), salt));
        appUserModel.setPasswordSalt(salt);
    
    }
    
    }
    

You don't need to explicitly call save or any such non-sense. When it exists the event handler function(s) it will save for you

You @Entity would stay the same but your Repository would become a spring CrudRespository - the implementation is trivial.

You can actually look here for an example.

With this, you can:

  1. Delete all of your controllers. The endpoints are created and the content-types managed by spring data rest for you. Read more about @Repository for details on that
  2. Delete all your DAL code

And literally the only thing you need to do is implement business specific event code.

The caveat here is that, by default, spring is going to produce a data format called application/hal+json - if you have a requirement to certain output formats you can create and register a HttpMessageConverter and register it for application/json and remove the one defined for hal+json

\$\endgroup\$
6
  • \$\begingroup\$ This looks pretty nice and I think it would make sense if I change my code and follow this example. What is interesting is that this gets rid of DTO objects? This is a bit weird because as can be seen here the entity Shop gets serialized. In my case this means that the resulting JSON contains fields like salt (from AppUserModel) which is of course not what I would want - so what about that? Wouldn't I run again into that mapping isue? \$\endgroup\$ – Stefan Falk Nov 15 '17 at 22:53
  • \$\begingroup\$ You can actually either apply transient which I believe will prevent it from serializing (jackson will ignore it). Or, you can use @JsonIgnore on the field. Or you add a mixin to the Jackson ObjectMapper And put a @JsonIgnore in the mixin. Infact, that's probably ideal way to go. With a mixin, you basically create a class that will be overlayed on the class to marshal and it will your mixin annotations to serialize to json \$\endgroup\$ – Christian Bongiorno Nov 16 '17 at 0:05
  • \$\begingroup\$ Although I kind of like the solution there are two things that kind of bother me: First is the fact that I am going to mix endpoint serialization logic with persistence. I don't know why @Transient is a good idea but at least this only affects the entity w.r.t. persistence, however, with @JsonIgnore the object becomes now a Model and a DTO. Further, the entire mapping logic stays still in the Service Layer. Instead of another class AppUserMapper I just use @HandleBeforeSave which still pollutes the service object with mapping logic and additionally adds some more Spring magic. \$\endgroup\$ – Stefan Falk Nov 16 '17 at 17:10
  • \$\begingroup\$ I see the advantage of CrudRepository but that will only get rid of save() and findByName() etc. Except from that I am not 100% sure if your solution would make things easier. E.g. what about situations where I have to access multiple tables in order to create the data for an aggregate DTO - in that case I could not do it this way and would still have all that logic in my Service Layer. \$\endgroup\$ – Stefan Falk Nov 16 '17 at 17:16
  • \$\begingroup\$ The existinence of CrudRepository will actually generate endpoints for you. You can ditch your controllers. So, if you have an Entity of User and a respository of UserRepository then you will, by default have /users/:id for an end point that accepts/return hal+json The pattern your current code follows is as common as raindrops in Seattle. Take the code sample I sent, run with it, and you can submit something for a review. I am telling you, you can ditch everything except business logic which you would put in handlers. I can't write the code for you. \$\endgroup\$ – Christian Bongiorno Nov 18 '17 at 0:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.