0

I'm working on a project where I need to do CRUD operations on Book and Library objects. Naturally the relationship between Book and Library is Many to One, like so:

@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class LibraryDao {

    @Id
    @GeneratedValue
    Long id;

    @OneToMany(mappedBy = "library")
    List<BookDao> book = new ArrayList<>();

} 
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class BookDao {
    
    @Id
    @GeneratedValue
    private long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private LibraryDao library;

    String name;

    public BookDao(Library library, String name) {
        this.library = library;
        this.name = name;
    }
    
}

I'm trying to keep these as separate as possible in each layer which is where my question comes from. It's easy to keep the creation and getting of Library stuff separate, but I'm finding it hard to create persist Books without mixing the two up. Mainly because I need a LibraryDAO object to create a BookDAO. Please see my comment in the BookServiceImpl class.

@Service
public class LibraryServiceImpl implements LibraryService {

    @Autowired
    LibraryRepository libraryRepository;

    @Override
    public Library getLibrary(long libraryId) {
        LibraryDao libraryDao = libraryRepository.findById(libraryId).orElseThrow(LibraryNotFoundException::new);
        return new Library(libraryDao.getId(), libraryDao.getBooks().stream().map(book -> new Book(book.getId(), book.getName())).collect(Collectors.toList()));
    }

    @Override
    public Library createLibrary() {
        LibraryDao libraryDao = libraryRepository.save(new LibraryDao());
        return new Library(libraryDao.getId(), List.of());
    }

}
@Service
public class BookServiceImpl implements BookService {

    @Autowired
    BookRepository bookRepository;

    @Autowired
    LibraryService libraryService;
    

    @Override
    public Book createBook(Long libraryId, String name) {

        //Which one below is less destructive to the MVC pattern? Either I can 
        //have a LibraryRepository in my BookService class (this feels wrong)
        //or else make libraryService.getLibrary() return a DAO object? Isn't is expected
        //that DAO objects stay in their service class?
        LibraryDao libraryDao = libraryService.getLibrary(libraryId);
        LibraryDao libraryDao = libraryRepository.findById(libraryId);

        BookDao bookDao = new BookDao(libraryDao, name);
        BookingDao saved = bookRepository.save(bookDao);

        return new Book(saved.getId(), saved.getLibraryDao().getId(), saved.getName());
     }

3 Answers 3

1

I think part of your problem is in the definition of the domain. If you were to put your requirement into natural language as someone who managed a library, you probably wouldn't ever:

Create a book

But you would regularly:

Add a book to the library

Similarly, when getting a book - it will be from the library, not a book that exists in a vacuum.

So this somewhat naturally leads to domain models such as:

Library {
   int Id;
   string Name;
   void AddBook() { .. persistence call }
   Book GetBook(bookId) { .. lookup call }
}

And a repository that might look something like this:

ILibraryRepository {
    Library GetLibrary(int libraryId);
    Book GetBook(int libraryId, int bookId);
    void AddBook(int libraryId, Book);
}
0

The best way is to simply have the id rather than the object. eg

class Book
{
   string LibraryId
   string BookId
   string title....
}

class Library
{
   string Id
   string Name
}

All your problems are now trivial.

class Repo
{
    List<Book> GetBooksInLibrary(libId)
    Book GetBook(bookId)
    Library GetLibraryContainingBookId(bookId)
    etc...
    void UpsertBook(book) //error libraryid x doesnt exist
    void UpsertLibrary(library)        
}
0

The issue you're running into here here that you have multiple sources of truth. A library refers to its books, a book refers to its library. It is technically possible that library A says that it contains book B, but book B says that it belongs to library C. This is a problem.

Having two sources of truth runs the risk of your sources contradicting one another, either through your own mistake (updating one but not the other) or random error (bug, data mismatch, ...).

This problem is resolved by reducing your models to having a single source of truth. Which one you keep is up to you, you may need to consider your storage medium here:

  • For a relational database, it's better to have the "many" (book) refer to the "one" (library) by its key.
  • For a document database where you store the books nested into the library object, it's better to have the "one" (library) refer to the "many" (book) by nesting them within the "one".

However, that's specifically a persistence concern. You have to separately address the concepts of domain models and persistence entities.

For your domain models, you don't want your relationship to be one-way. Partly because it's handy to be able to traverse the relationship both ways, partly because you don't want to have to rewrite the whole domain model when you decide to change your persistence technology from a relational model to a document db (or vice versa).

Definitely have a single source of truth for your persistence entities. This is the essential pillar that allows you to recreate your data state, and therefore consistency is key.

As to the domain model, you get to choose whether you're happy just having a one-way relationship, or if it makes more sense to you to model this as a two-way relationship.
Making it a two-way relationship means that you either run the risk of contradicting sources of truth (it would require a strong test suite to cover yourself against any mistakes or forgetfulness here), or you take on the added complexity of having a one-way relationship where you can go the other way on the fly but it's calculated, not read from an existing second source of truth (this requires more effort but it helps prevent mistakes in the long run).

If you keep it a one-way relationship, pick the relationship that makes the most sense. If you do end up switching persistence technology, you'll either have to redesign your relationship (this is generally considered unclean but perfectly okay for small projects with a low chance of actually changing the storage), or you'll have to add some extra mapping to convert the new relationship to the old one (and vice versa).

Pick your poison. There's more than one road to take here.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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