How to handle relationships between entities: One to many

Introduction

In the past tutorials we handled the suppliers: we learned how to file storage to our project. Now, it is time to focus on handling the products. We will apply the same principles in order to practice what we’ve learned so far. However, in addition, we need to find out how to manage a new type of relationship between two entities: one to many.

Lemonade stand entities relationships

What do we create?

The first step is to revisit our given problem:

 …
A product has a name, a quantity stock, a price, and is provided by a supplier.

Our application should manage the deposit by adding, removing, modifying, and listing our products
..

It looks similar to what we did for suppliers, but a key question arises – how do we establish the relationship between a product and a supplier?

Every product should be associated with a supplier, while a supplier can have multiple products. This type of relationship is called a one to many relationship.

It is a concept that helps us organize and think about the relationships between different entities, whether it is in daily life or in technical fields like databases and programming. It reflects the idea that one item (like a supplier) can be associated with many others (like products), but each of those others is linked back to only that single original item.

In real-world programming, there are various types of relationships between entities, but in this tutorial, we will focus solely on this kind.

aggregation diagram 2

How do we create it ?

Let’s start coding!

In our Domain package, we will add a new class: Product. We can refer to our newly created diagram to identify the attributes that a product should have.

				
					public class Product {
    
    private int id;
   
    private String name;
    
    private String description;
    
    private int price;
    
    private int quantity;
    
    private Supplier supplier;
   
    ...
}
				
			

The remaining parts of the class, such as constructors, setters and getters, can be extended below.

				
					public class Product {
    
    private int id;
    
    private String name;
    
    private String description;
    
    private int price;
    
    private int quantity;
    
    private Supplier supplier;

    public Product(int id, String name, String description, int price, int quantity, Supplier supplier) {
        this.id = id;
        this.name = name;
        this.description = description;
        this.price = price;
        this.quantity = quantity;
        this.supplier = supplier;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }

    public Supplier getSupplier() {
        return supplier;
    }

    public void setSupplier(Supplier supplier) {
        this.supplier = supplier;
    }

    @Override
    public String toString() {
        return "Product{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", description='" + description + '\'' +
                ", price=" + price +
                ", quantity=" + quantity +
                '}';
    }

}
				
			

Next, as we learned in this tutorial, we need to create the specialized layers: Repository (responsible for storage) and Service (responsible for Business Logic). The UI layer remains unchanged; we only need to add some methods to the products’ menu.

First, we create a simple Repository:

				
					public class ProductRepository {

    private Map<Integer, Product> products;

    public ProductRepository() {
        products = new HashMap<>();
    }

    public Product save(Product product) throws IDNotUniqueException {
        if (products.containsKey(product.getId())) {
            throw new IDNotUniqueException("The id is not unique ");
        }

        products.put(product.getId(), product);
        return product;
    }

    public Product update(Product product) {
        if (products.containsKey(product.getId())) {
            products.put(product.getId(), product);
        }

        return product;
    }

    public void delete(int productId) {
        products.remove(productId);
    }

    public Iterable<Product> findAll() {
        return products.values();
    }

    public Product findById(int productId) {
        return products.get(productId);
    }

}

				
			

In this tutorial we discussed about file storage. Let’s see how we can represent products in files.
If we have these suppliers:

				
					1,Fresh lemons,freshlemons@email.com
2,Sweet Sugar,sweetsugar@email.com
3,Cold Ice,coldice@email.com
				
			

We can add these products:

				
					1,Sweet lemons,Some sweet lemons,5,2,1
2,Sour lemons,Some sour lemons,4,2,1
3,Sweet sugar,Some sugar packets,2,10,2
				
			

For suppliers, we only need their IDs to maintain the relationship. The rest of the data can be found in the suppliers file.

With this in mind, we can create a ProductFileRepository:

				
					public class ProductFileRepository extends ProductRepository {

    private String filename;

    public ProductFileRepository(String filename) throws IDNotUniqueException {
        super();
        this.filename = filename;
        loadProductsFromFile();
    }

    @Override
    public Product save(Product product) throws IDNotUniqueException {
        Product savedProduct = super.save(product);
        writeToFile();
        return savedProduct;
    }

    @Override
    public Product update(Product product) {
        Product updatedProduct = super.update(product);
        writeToFile();
        return updatedProduct;
    }

    @Override
    public void delete(int productId) {
        super.delete(productId);
        writeToFile();
    }

    public List<Product> readProductsFromFile() {
        List<Product> products = new ArrayList<>();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(filename));
            String line;
            while ((line = reader.readLine()) != null) {
                String[] elems = line.split(",");

                int id = Integer.parseInt(elems[0]);
                String name = elems[1];
                String description = elems[2];
                int price = Integer.parseInt(elems[3]);
                int quantity = Integer.parseInt(elems[4]);
                int supplierId = Integer.parseInt(elems[5]);

                Supplier supplier = new Supplier();
                supplier.setId(supplierId);

                Product product = new Product(id, name, description, price, quantity, supplier);
                products.add(product);
            }

            reader.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return products;
    }

    private void loadProductsFromFile() throws IDNotUniqueException {
        List<Product> products = readProductsFromFile();
        for (Product product : products) {
            this.save(product);
        }
    }

    private void writeToFile() {
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(filename));
            Iterable<Product> products = findAll();
            for (Product product : products) {
                String line = product.getId() + "," + product.getName() + "," + product.getDescription() + "," + product.getPrice() + "," + product.getQuantity() + "," + product.getSupplier().getId();
                writer.write(line);
                writer.newLine();
            }
            writer.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

				
			

We can also add some validations:

				
					public class ProductValidator {

    public void validateProduct(Product product) throws ValidationException {
        StringBuilder stringBuilder = new StringBuilder();
        if (product.getName().length() < 3 || product.getName().length() > 100) {
            stringBuilder.append("The name should be between 3 and 100 chars. \n");
        }

        if (product.getDescription().length() < 3 || product.getDescription().length() > 250) {
            stringBuilder.append("The description should be between 3 and 100 chars. \n");
        }

        if (product.getPrice() < 1 || product.getPrice() > 1000) {
            stringBuilder.append("The price should be between 1 and 1000. \n");
        }

        if (product.getQuantity() < 0 || product.getQuantity() > 25) {
            stringBuilder.append("The quantity should be between 1 and 25. \n");
        }

        if (!stringBuilder.isEmpty()) {
            throw new ValidationException(stringBuilder.toString());
        }
    }
}
				
			

To proceed, we need to create the service class. The main difference is that we require the SupplierRepository as an attribute.

				
					public class ProductService {

    private ProductRepository productRepository;

    private ProductValidator productValidator;

    private SupplierService supplierService;
    
    public ProductService(ProductRepository productRepository, ProductValidator productValidator, SupplierService supplierService) {
        this.productRepository = productRepository;
        this.productValidator = productValidator;
        this.supplierService = supplierService;
    }
    
    ...
}
				
			

For instance, when saving one Product, the user should provide an ID. Using the given ID, we retrieve the corresponding supplier:

				
					   public Product saveProduct(int id, String name, String description, int price, int quantity, int supplierId) throws ValidationException, IDNotUniqueException {
        Supplier supplier = supplierService.findById(supplierId);
        Product product = new Product(id, name, description, price, quantity, supplier);

        productValidator.validateProduct(product);
        Product savedProduct = productRepository.save(product);
        return savedProduct;
    }

				
			

We will apply a similar approach for the remaining operations, such as: deleting, updating, and getting all products. The rest of the class will look as follows:

				
					public class ProductService {

    private ProductRepository productRepository;

    private ProductValidator productValidator;

    private SupplierService supplierService;

    public ProductService(ProductRepository productRepository, ProductValidator productValidator, SupplierService supplierService) {
        this.productRepository = productRepository;
        this.productValidator = productValidator;
        this.supplierService = supplierService;
    }

    public Product saveProduct(int id, String name, String description, int price, int quantity, int supplierId) throws ValidationException, IDNotUniqueException {
        Supplier supplier = supplierService.findById(supplierId);
        Product product = new Product(id, name, description, price, quantity, supplier);

        productValidator.validateProduct(product);
        Product savedProduct = productRepository.save(product);
        return savedProduct;
    }

    public void removeProduct(int id) {
        productRepository.delete(id);
    }

    public Product updateProduct(int id, String newName, String newDescription, int newPrice, int newQuantity, int newSupplierId) throws ValidationException {
        Supplier supplier = supplierService.findById(newSupplierId);
        Product productToUpdate = new Product(id, newName, newDescription, newPrice, newQuantity, supplier);

        productValidator.validateProduct(productToUpdate);
        Product updatedProduct = productRepository.update(productToUpdate);
        return updatedProduct;
    }

    public Iterable<Product> getAll() {
        return productRepository.findAll();
    }
}
				
			

It is considered a good practice to test the newly added classes and methods. Therefore, we should add test classes for ProductFileRepository and ProductService.

				
					public class ProductFileRepositoryTest {

    private String filename;

    public ProductFileRepositoryTest(String filename) {
        this.filename = filename;
    }

    private void clearFile() {
        try (FileOutputStream fos = new FileOutputStream(filename)) {
            // the file is emptied
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void shouldSaveToFileOneElement_whenSaveIsCalled() throws IDNotUniqueException {
        clearFile();
        ProductFileRepository fileRepository = new ProductFileRepository(filename);
        Supplier supplier = new Supplier(1, "Sugar supplier", "supplier@email.com");
        Product firstProductToSave = new Product(1, "Sugar", "Sweet sugar", 10, 10, supplier);

        Product firstSavedSupplier = fileRepository.save(firstProductToSave);

        List<Product> products = fileRepository.readProductsFromFile();
        Product savedProduct = fileRepository.findById(1);

        assert products.size() == 1;
        assert savedProduct != null;
        assert savedProduct.getSupplier() != null;

        clearFile();
    }

    public void testAllProductFileRepository() throws IDNotUniqueException {
        shouldSaveToFileOneElement_whenSaveIsCalled();
    }
}
				
			
				
					public class ProductServiceTest {

    private SupplierService supplierService;

    private ProductService productService;

    private void setUp() {
        SupplierRepository supplierRepository = new SupplierRepository();
        SupplierValidator supplierValidator = new SupplierValidator();
        supplierService = new SupplierService(supplierRepository, supplierValidator);

        ProductRepository productFileRepository = new ProductRepository();
        ProductValidator productValidator = new ProductValidator();
        productService = new ProductService(productFileRepository, productValidator, supplierService);

    }

    public void shouldSaveProduct_whenSavedMethodCalled() throws ValidationException, IDNotUniqueException {
        setUp();

        Supplier supplier = supplierService.saveSupplier(1, "Sugar supplier", "supplier@email.com");
        Product savedProduct = productService.saveProduct(1, "Sugar", "Sweet sugar", 10, 10, supplier.getId());

        assert savedProduct != null;
        assert savedProduct.getId() == 1;
        assert savedProduct.getName().equals("Sugar");
    }

    public void shouldUpdateProduct_whenUpdateMethodCalled() throws ValidationException, IDNotUniqueException {
        setUp();

        Supplier supplier = supplierService.saveSupplier(1, "Sugar supplier", "supplier@email.com");
        Product savedProduct = productService.saveProduct(1, "Sugar", "Sweet sugar", 10, 10, supplier.getId());
        Product updatedProduct = productService.updateProduct(1, "Sugar", "Brown sugar", 10, 10, supplier.getId());

        assert updatedProduct != null;
        assert updatedProduct.getId() == 1;
        assert updatedProduct.getDescription().equals("Brown sugar");
    }

    public void shouldRemoveProduct_whenRemoveMethodCalled() throws ValidationException, IDNotUniqueException {
        setUp();

        Supplier supplier = supplierService.saveSupplier(1, "Sugar supplier", "supplier@email.com");
        Product savedProduct = productService.saveProduct(1, "Sugar", "Sweet sugar", 10, 10, supplier.getId());
        productService.removeProduct(1);


        assert !productService.getAll().iterator().hasNext();
    }

    public void testAllProductService() throws ValidationException, IDNotUniqueException {
        shouldSaveProduct_whenSavedMethodCalled();
        shouldUpdateProduct_whenUpdateMethodCalled();
        shouldRemoveProduct_whenRemoveMethodCalled();

    }


}
				
			

Lastly, we need to create new methods for the UI layer. To accomplish this, we need to pass the ProductService instance in the constructor.

				
					public class UserInterface {

    private ProductService productService;

    private SupplierService supplierService;

    private final Scanner scanner = new Scanner(System.in);

    public UserInterface(ProductService productService, SupplierService supplierService) {
        this.productService = productService;
        this.supplierService = supplierService;
    }
    
    ...
    
}
				
			

The class will appear as follows:

				
					public class UserInterface {

    private ProductService productService;

    private SupplierService supplierService;

    private final Scanner scanner = new Scanner(System.in);

    public UserInterface(ProductService productService, SupplierService supplierService) {
        this.productService = productService;
        this.supplierService = supplierService;
    }

    private void showMenu() {
        System.out.println("Welcome to the Lemonade Stand Administration App.");
        System.out.println("The Menu:");
        System.out.println("1. Manage suppliers");
        System.out.println("2. Manage products");
        System.out.println("3. Manage lemonades recipes");
        System.out.println("4. Create an order");
        System.out.println("5. Daily sales report");
        System.out.println("6. Empty products stock report");
        System.out.println("7. Exit");
        System.out.println("What do you want to do? ");
    }

    private void showSuppliersMenu() {
        System.out.println("Welcome to the Lemonade Stand Administration App.");
        System.out.println("The Suppliers menu:");
        System.out.println("1. Add a supplier");
        System.out.println("2. Update a supplier");
        System.out.println("3. Remove a supplier");
        System.out.println("4. Display all suppliers");
        System.out.println("5. Back to main menu");
        System.out.println("What do you want to do? ");
    }

    private void showProductsMenu() {
        System.out.println("Welcome to the Lemonade Stand Administration App.");
        System.out.println("The Product menu:");
        System.out.println("1. Add a product");
        System.out.println("2. Update a product");
        System.out.println("3. Remove a product");
        System.out.println("4. Display all products");
        System.out.println("5. Back to main menu");
        System.out.println("What do you want to do? ");
    }


    public void runMenu() {
        Scanner scanner = new Scanner(System.in);
        int option = -1;
        while (option != 7) {
            showMenu();
            option = scanner.nextInt();

            switch (option) {
                case 1:
                    runSuppliersMenu(scanner);
                    break;
                case 2:
                    runProductsMenu(scanner);
                    break;
                case 3, 4, 5, 6:
                    System.out.println("Not implemented yet!");
                    break;
                case 7:
                    break;

            }
        }
        scanner.close();
    }

    public void runProductsMenu(Scanner scanner) {
        int option = -1;
        while (option != 5) {
            showProductsMenu();
            option = scanner.nextInt();

            switch (option) {
                case 1:
                    handleAddProduct(scanner);
                    break;
                case 2:
                    handleRemoveProducts(scanner);
                    break;
                case 3:
                    handleUpdateProduct(scanner);
                    break;
                case 4:
                    handleShowProducts();
                    break;
                case 5:
                    break;

            }
        }
    }

    public void runSuppliersMenu(Scanner scanner) {
        int option = -1;
        while (option != 5) {
            showSuppliersMenu();
            option = scanner.nextInt();

            switch (option) {
                case 1:
                    handleAddSupplier(scanner);
                    break;
                case 2:
                    handleRemoveSuppliers(scanner);
                    break;
                case 3:
                    handleUpdateSupplier(scanner);
                    break;
                case 4:
                    handleShowSuppliers();
                    break;
                case 5:
                    break;

            }
        }
    }

    private void handleAddSupplier(Scanner scanner) {
        System.out.print("ID: ");
        int id = scanner.nextInt();

        System.out.print("Name: ");
        String name = scanner.next();

        System.out.print("Contact email: ");
        String contactEmail = scanner.next();

        try {
            Supplier savedSupplier = supplierService.saveSupplier(id, name, contactEmail);
            System.out.printf("The supplier with ID=%s has been saved \n", savedSupplier.getId());
        } catch (ValidationException | IDNotUniqueException e) {
            System.out.println("Error with saving the supplier: " + e.getMessage());
        }

    }

    private void handleUpdateSupplier(Scanner scanner) {
        System.out.print("The ID of the supplier to be updated: ");
        int id = scanner.nextInt();

        System.out.print("New name: ");
        String name = scanner.next();

        System.out.print("New contact email: ");
        String contactEmail = scanner.next();

        Supplier updatedSupplier = supplierService.updateSupplier(id, name, contactEmail);
        System.out.printf("The supplier with ID=%s has been updated \n", updatedSupplier.getId());
    }

    private void handleRemoveSuppliers(Scanner scanner) {
        System.out.print("The ID of the supplier to be removed: ");
        int supplierIdToRemove = scanner.nextInt();

        supplierService.removeSupplier(supplierIdToRemove);
        System.out.printf("The product with ID=%s has been removed \n", supplierIdToRemove);
    }

    private void handleShowSuppliers() {
        Iterable<Supplier> supplierList = supplierService.findAll();
        displaySuppliers(supplierList);
    }

    private void displaySuppliers(Iterable<Supplier> suppliers) {
        for (Supplier supplier : suppliers) {
            System.out.println(supplier);
        }
    }

    private void handleAddProduct(Scanner scanner) {
        System.out.print("ID: ");
        int id = scanner.nextInt();

        System.out.print("Name: ");
        String name = scanner.next();

        System.out.print("Description: ");
        String description = scanner.next();

        System.out.print("Price: ");
        int price = scanner.nextInt();

        System.out.print("Quantity: ");
        int quantity = scanner.nextInt();

        System.out.print("Supplier id: ");
        int supplierId = scanner.nextInt();

        try {
            productService.saveProduct(id, name, description, price, quantity, supplierId);
        } catch (ValidationException | IDNotUniqueException e) {
            System.out.println("Error with saving the product: " + e.getMessage());
        }

    }

    private void handleRemoveProducts(Scanner scanner) {
        System.out.print("The ID of the product to be removed: ");
        int id = scanner.nextInt();

        productService.removeProduct(id);
    }

    private void handleUpdateProduct(Scanner scanner) {
        System.out.print("The ID of the product to be updated: ");
        int id = scanner.nextInt();

        System.out.print("New name: ");
        String name = scanner.next();

        System.out.print("New description: ");
        String description = scanner.next();

        System.out.print("New price: ");
        int price = scanner.nextInt();

        System.out.print("New quantity: ");
        int quantity = scanner.nextInt();

        System.out.print("Supplier id: ");
        int supplierId = scanner.nextInt();

        try {
            productService.updateProduct(id, name, description, price, quantity, supplierId);
        } catch (ValidationException e) {
            System.out.println("Error with saving the product: " + e.getMessage());
        }
    }

    private void handleShowProducts() {
        Iterable<Product> productList = productService.getAll();
        for (Product product : productList) {
            System.out.println(product);
        }
    }

}
				
			

 

Finally, the Main class:

				
					public class Main {

    public static void main(String[] args) throws ValidationException, IDNotUniqueException {
        SupplierFileRepository supplierRepository = new SupplierFileRepository("suppliers.csv");
        SupplierValidator supplierValidator = new SupplierValidator();
        SupplierService supplierService = new SupplierService(supplierRepository, supplierValidator);

        ProductFileRepository productFileRepository = new ProductFileRepository("products.csv");
        ProductValidator productValidator = new ProductValidator();
        ProductService productService = new ProductService(productFileRepository, productValidator, supplierService);

        UserInterface userInterface = new UserInterface(productService, supplierService);

        MainTest mainTest = new MainTest();
        mainTest.runAllTest();
        userInterface.runMenu();
    }
    
}
				
			

Time to practice

Try adding a new menu item so that the user can filter the products. The user will be required to enter a string and the application will return only the products whose supplier’s name contains the string.

Which layer should be updated to handle the filtering?

Key points

  • usually there are relationships between entities which can be represented in different ways
  • the relationships can be analyzed using diagrams
  • a one to many relationship occurs when a single object of one class is associated with multiple objects of another class
  • the already created layers remain the same when we add new entities

Conclusion

In this tutorial, we introduced the idea of relationships between elements. There are different types of relationships, but we handled only the one to many relationship. In our given problem, we have another type of relationship, many-to-many, which we will handle differently.

In the next tutorial, we will learn how to improve our application by using the following Java features: interfaces, generics, and inheritance.