问题
I think this may be a case where I know the answer but just don't like it.
My starting point was an earlier question, Hibernate @Entity conflict with Spring @Autowired for non-column object.
I have an @Entity which is "naturally" linked in a one-to-many relationship with another set of entities. In my example, I'm calling it an ItemEntity, and it has a (very) large price history. So large, that having Hibernate lazy-load is a performance killer because real use cases never need all of the history (hundreds of thousands of prices, vs the few hundred typically needed). So I have a PriceCache service that gets what I need on-demand.
The "natural" thing, from a normal use case, is to retrieve the ItemEntity of interest, then ask for the associated price history during some time range. As was argued in the above post, having that service inside ItemEntity is not normal, though it can be done and made to work.
In the sample code below, I've written this in a different way, by having an Item interface, with an implementation that is effectively a proxy for ItemEntity plus the PriceCache service. This is example code, and bits are missing; I think (hope) there's enough present to be clear.
My set of entities and their properties is not so large that I couldn't do this by hand for all of them; a couple dozen entities, each with 5-20 properties. That would be moderately painful and boring, but it should work.
But...is there an easier way to create what is essentially a proxy object with an extra service injected? Or maybe the question is, is there a lazier way to do this?
@Entity @Table(name="item")
public class ItemEntity {
@Id @Column(name="id")
private long id;
@Column(name="name")
private String name;
/* ... setters, getters ... */
}
@Service
public class ItemCache {
@Autowired
private ItemDAO itemDAO;
@Autowired
private PriceCache priceCache;
private Map<Long,Item> itemCache;
public ItemCache() {
itemCache = new HashMap<>();
}
public Item get(long id) {
if (itemCache.containsKey(id))
return itemCache.get(id);
ItemEntity itemEntity = itemDAO.find(id);
Item item = (itemEntity == null) ? null : new ItemImpl(itemEntity, priceCache);
itemCache.put(id, item); // caches nulls to avoid retry
return item;
}
}
@Service
public class PriceCache {
@Autowired
private PriceDAO priceDAO;
/* ... various cache/map structures to hold previous query results ... */
public PriceCache() {
/* ... initialize all those cache/map structures ... */
}
public Collection<Price> getPrices(long id, LocalDateTime begTime, LocalDateTime endTime) {
Collection<Price> results;
/* ... check the caches to see if we already have the data ... */
/* ... otherwise, use priceDAO to find it and save the results in the cache ... */
return results;
}
}
public interface Item {
public long getId();
public String getName();
public Collection<Price> getPrices(LocalDateTime begTime, LocalDateTime endTime);
}
public class ItemImpl implements Item {
public ItemImpl(ItemEntity underlying, PriceCache priceCache) { ... }
public long getId() {
return underlying.getId();
}
public String getName() {
return underlying.getName();
}
public Collection<Price> getPrices(LocalDateTime begTime, LocalDateTime endTime) {
priceCache.get(getId(), begTime, endTime);
}
}
回答1:
So...I'm guessing that everyone is being polite and doesn't want to agree that I'm looking for a lazy way out :-)
I've not done this for the above example, but I have another similar case where what I want is essentially a bean with some services added on. Rather than write proxies and the like, I made the object that provides the services a derived object. It's not an @Entity, but but a @Component which is created purely from the applicationContext.xml description.
So there are four parts; the bean which describes the object, a "real" business object that uses/extends the descriptions and provides a service which gets injected, and a cache service that finds and creates those business objects from the descriptions.
More or less like below. This is trimmed down and written on-the-fly, so it may have typos; the real code and idea works.
@Component
public class ThingDescr {
/* ... various attributes, setters, getters, just a bean ... */
}
public class Thing extends ThingDescr implements HelperService {
public Thing(ThingDescr td, HelperService svc) {
/* ... basically a copy constructor ... */
}
@Override
public void doSomething() {
/* ... whatever HelperService is supposed to do ... */
}
}
public interface HelperService {
public void doSomething();
}
@Service
public class ThingCache {
@Autowired
private HelperService svc;
@Autowired
private List<? extends ThingDescr> thingList;
private Map<String,Thing> thingMap;
private void load() {
thingMap = new HashMap<>();
for (ThingDescr td : thingList) {
Thing thing = new Thing(td, svc);
thingMap.put(thing.getName(), thing);
}
public getThing(String name) {
if (thingMap == null || thingMap.isEmpty())
load();
return thingMap.get(name);
}
}
The main advantage of this pattern is that the business object "Thing" inherits all the properties of the underlying object without having to write them. I didn't do this, but I think that if the properties are supposed to be read-only, the setters in "ThingDescr" could be made protected so "Thing" can access them, but users of the class can't.
来源:https://stackoverflow.com/questions/60856510/spring-and-hibernate-mash-up-object-that-is-proxy-of-an-entity-with-an-extra