简体   繁体   中英

Spring JPA multi-table relationships

I'm trying to create a product catalogue that has Product -> Attributes -> Options

So the data would look something like this:

product_one
  attribute_one
    option_one
    option_two
  attribute_two
    option_one
    option_two

The code is available on GitHub https://github.com/ccsalway/prod_info_mngr

I have created a Class for each Entity:

@Entity
class Product {
  @Id
  @Generatedvalue
  private Long id;
  private String name;

  // getters and setters
}

@Entity
class Attribute {
  @Id
  @Generatedvalue
  private Long id;
  private String name;
  @ManyToOne(cascade = CascadeType.REMOVE)
  private Product product;

  // getters and setters
}

@Entity
class Option {
  @Id
  @Generatedvalue
  private Long id;
  private String name;
  @ManyToOne(cascade = CascadeType.REMOVE)
  private Attribute attribute;

  // getters and setters
}

I have created a Repository for each Entity:

@Repository
public interface ProductRepository extends PagingAndSortingRepository<Product, Long> {
}

@Repository
public interface AttributeRepository extends PagingAndSortingRepository<Attribute, Long> {
}

@Repository
public interface OptionRepository extends PagingAndSortingRepository<Option, Long> {
}

I have created a Service for each Entity:

@Service
public class ProductService {

    // Autowired Repositories

    // methods
}

@Service
public class AttributeService {

    // Autowired Repositories

    // methods
}

@Service
public class OptionService {

    // Autowired Repositories

    // methods
}

I have created a Controller for each Entity:

@Controller
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    //methods
}

@Controller
@RequestMapping("/product/{prod_id}/attribute")
public class AttributeController{

    @Autowired
    private AttributeService attributeService;

    //methods
}

@Controller
@RequestMapping("/product/{prod_id}/attribute/{attr_id}")
public class OptionController {

    @Autowired
    private OptionService optionService;

    //methods
}

And (finally) I have created several views for each Controller (I won't add them here).

What I am trying to do in the product_view.jsp View is show a list of attributes and their associated options, something like this:

<table id="attrTable">
    <thead>
    <tr>
        <th>Name</th>
        <th>Options</th>
    </tr>
    </thead>
    <tbody>
    <c:forEach items="${attributes}" var="attr">
        <tr data-id="${attr.id}">
            <td>${fn:htmlEscape(attr.name)}</td>
            <td><c:forEach items="${attr.options}" var="opt" varStatus="loop">
                ${opt.name}<c:if test="${!loop.last}">,</c:if>
            </c:forEach></td>
        </tr>
    </c:forEach>
    </tbody>
</table>

So the table would look something like this

product_one
  attribute_one    option_one, option_two
  attribute_two    option_one, option_two

The first step was to create a @RequestMapping in ProductController :

@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public String view(Model model, @PathVariable Long id) {
    Product product = productService.getProduct(id);
    List<Attribute> attributes = productService.getAttributes(product);
    model.addAttribute("product", product);
    model.addAttribute("attributes", attributes);
    return "products/product_view";
}

but ${attr.options} in the View doesn't recognise the options key so how do I go about making this work?

I tried to add a @OneToMany association in the Product Entity but this created a table in the database with product_id|attribute_id which you then have to save the attribute, and then update the product with the new attribute, which also means when you select a product, you are pulling ALL attributes and ALL options which prevents Paging on those.

@Entity
class Product {
  @Id
  @Generatedvalue
  private Long id;
  private String name;

  @OneToMany
  List<Attribute> attributes;

 // getters and setters
}

I found a solution:

I added OneToMany and ManyToOne relationships as follows. This method also allowed the repository.delete(id) method to cascade delete.

FetchType.LAZY tells Spring to only fetch the underlying item(s) when requested. For example, when a request for a Product is made, the id and name will be fetched, but because the attributes are @OneToMany which is, by default, LAZY, the attributes won't be fetched from the DB until a specific call to product.getAttributes() is made.

@Entity
public class Product {

    @Id
    @GeneratedValue
    private Long id;
    private String name;

    // OneToMany is, by default, a LAZY fetch
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "product")
    private Set<Attribute> attributes = new HashSet<>();

    // getters and setters
}

@Entity
public class Attribute {

    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id", nullable = false)
    private Product product;

    // OneToMany is, by default, a LAZY fetch
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "attribute")
    private Set<Option> options = new HashSet<>();

    // getters and setters
}

@Entity
public class Option {

    @Id
    @GeneratedValue
    private Long id;

    @NotEmpty
    @Size(min = 1, max = 32)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "attribute_id", nullable = false)
    private Attribute attribute;

    // getters and setters
}

In the ProductController , I separate the Attributes from the Product so that I can use Paging on the Attributes (whereas if I just called product.getAttributes() , it would fetch ALL the Attributes)

@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public String view(Model model, @PathVariable Long id, @RequestParam(name = "page", defaultValue = "0") int page) throws ProductNotFoundException {
    Product product = productService.getProduct(id);
    model.addAttribute("product", product);
    // requesting attributes separately (as opposed to using LAZY) allows you to use paging
    Page<Attribute> attributes = productService.getAttributes(product, new PageRequest(page, 10));
    model.addAttribute("attributes", attributes);
    return "products/product_view";
}

Then in the view, I remember to loop over the attributes and not the product.attributes as explained above.

Because the options property is set to LAZY in the Attribute Entity, when the loop calls attr.options , Spring will make a request to the DB for the Options for the current Attribute .

<table id="attrTable" class="table is-hoverable is-striped is-fullwidth" style="cursor:pointer;">
    <thead>
    <tr>
        <th>Name</th>
        <th>Options</th>
    </tr>
    </thead>
    <tbody>
    <c:forEach items="${attributes}" var="attr">
        <tr data-id="${attr.id}">
            <td>${fn:htmlEscape(attr.name)}</td>
            <td>
                <c:forEach items="${attr.options}" var="opt" varStatus="loop">
                    ${opt.name}<c:if test="${!loop.last}">,</c:if>
                </c:forEach>
            </td>
        </tr>
    </c:forEach>
    </tbody>
</table>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM