Mittwoch, 13. Mai 2015

[Current state] How to use the current Version of Hibernate-Search Generic JPA

As promised I am following up with the current state of development in this blog entry.

Firstly, let's take a look at what standard Hibernate Search annotated entities would look like:
First our Game Entity:

@Indexed
@Entity
@Table(name = "Game")
public class Game {
private Long id;
private String title;
private List<Vendor> vendors;
public Game() {
}
public Game(String title) {
this.title = title;
}
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Field(store = Store.YES, index = Index.YES, analyze = Analyze.NO)
@Column
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@IndexedEmbedded(includeEmbeddedObjectId = true, targetElement = Vendor.class)
@OneToMany
public List<Vendor> getVendors() {
return vendors;
}
public void setVendors(List<Vendor> vendors) {
this.vendors = vendors;
}
}
view raw Game.java hosted with ❤ by GitHub
And the Vendor Entity:

@Entity
@Table(name = "Vendor")
public class Vendor {
private Long id;
private String name;
private List<Game> games;
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Column
@Field(store = Store.YES, index = Index.YES, analyze = Analyze.NO)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@ManyToOne(targetEntity = Game.class)
@ContainedIn
public List<Game> getGames() {
return games;
}
public void setGames(List<Game> games) {
this.games = games;
}
}
view raw Vendor.java hosted with ❤ by GitHub

To get this to work properly in the current state of my Generic JPA version an additional @InIndex is needed at the class-level:

@InIndex
@Entity
@Table(name = "Vendor")
public class Vendor {
//...
}
@InIndex
@Indexed
@Entity
@Table(name = "Game")
public class Game {
//...
}

This is a requirement when working with the general JPA specification as some implementors create subclasses for each Entity and Hibernate-Search needs to know which class in the hierarchy was the original one that was supposed to be in the Index.

At the moment we configure our Hibernate Search instance by subclassing JPASearchFactory. While this might be a bit unconvenient for production use it gets the job done easily during development. Later on this will be possible to be configured via a properties/xml file.

Note that this is an example for a SearchFactory in a Java EE application:

//make sure this is constructed at Container startup
@Singleton
@Startup
public class EJBSearchFactory extends SQLJPASearchFactory {
@Resource
private ManagedScheduledExecutorService exec;
@PersistenceUnit
private EntityManagerFactory emf;
/**
* since we can not hook into the JPA lifecycle for now
* we have to make sure the SearchFactory is started.
* This is done via super.init();
*/
@PostConstruct
public void startup() {
super.init();
//register this SearchFactory in Search
Search.setup( this );
}
/**
* since we can not hook into the JPA lifecycle for now
* we have to make sure the SearchFactory is stopped as well.
* This is done via super.shutdown();
*/
@PreDestroy
public void shutdown() {
super.shutdown();
}
@Override
public void updateEvent(List<UpdateInfo> arg0) {
//callback, only useful for custom update event processing
}
@Override
protected int getBatchSizeForUpdates() {
//how many update-information entities should be polled from
//the database at once
return 2;
}
@Override
protected String getConfigFile() {
//Hibernate Search Engine properties are configured in this file
return "/hsearch.properties";
}
@Override
protected long getDelay() {
//the delay between update polling
return 500;
}
@Override
protected TimeUnit getDelayUnit() {
//the timeunit for the delay
return TimeUnit.MILLISECONDS;
}
@Override
protected EntityManagerFactory getEmf() {
//-> the EMF used in this application
return this.emf;
}
@Override
protected List<Class<?>> getIndexRootTypes() {
//the Root types
return Arrays.asList( Game.class );
}
@Override
protected TriggerSQLStringSource getTriggerSQLStringSource() {
//the trigger source to be used to create the updates triggers
//in the database (will not be discussed in this blogpost)
return new MySQLTriggerSQLStringSource();
}
@Override
protected List<Class<?>> getUpdateClasses() {
//the updates classes (-> later in this post)
return Arrays.asList( GameUpdates.class, VendorUpdates.class, GameVendorUpdates.class );
}
@Override
protected boolean isUseJTATransaction() {
return true;
}
@Override
protected ScheduledExecutorService getExecutorServiceForUpdater() {
//this is needed to have a transaction environemtn for the updater thread
//for a JTA transaction this must be a ManagedScheduledExecutorService
return this.exec;
}
@Override
protected Connection getConnectionForSetup(EntityManager em) {
return em.unwrap(Connection.class);
}
}
The only thing that is needed to get this whole thing to work are the Updates classes. As of now the user has to create these on his own. This will hopefully not be a requirement in the future, anymore.

@Entity
@Table(name = "GameUpdates")
@Updates(tableName = "GameUpdates", originalTableName = "Game")
public class GameUpdates {
@Id
private Long id;
@IdFor(entityClass = Game.class, columns = "gameId", columnsInOriginal = "id")
@Column
private Long gameId;
@Event(column = "eventType")
@Column
private Integer eventType;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getGameId() {
return gameId;
}
public void setGameId(Long gameId) {
this.gameId = gameId;
}
public Integer getEventType() {
return eventType;
}
public void setEventType(Integer eventType) {
this.eventType = eventType;
}
}
@Entity
@Table(name = "VendorUpdates")
@Updates(tableName = "VendorUpdates", originalTableName = "Vendor")
public class VendorUpdates {
@Id
private Long id;
@IdFor(entityClass = Vendor.class, columns = "vendorId", columnsInOriginal = "id")
@Column
private Long vendorId;
@Event(column = "eventType")
@Column
private Integer eventType;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getVendorId() {
return vendorId;
}
public void setVendorId(Long vendorId) {
this.vendorId = vendorId;
}
public Integer getEventType() {
return eventType;
}
public void setEventType(Integer eventType) {
this.eventType = eventType;
}
}
@Entity
@Table(name = "Game_VendorUpdates")
@Updates(tableName = "Game_VendorUpdates", originalTableName = "Game_Vendor")
public class GameVendorUpdates {
@Id
private Long id;
@IdFor(entityClass = Game.class, columns = "gameId", columnsInOriginal = "game_ID")
@Column
private Long gameId;
@IdFor(entityClass = Vendor.class, columns = "vendorId", columnsInOriginal = "vendors_ID")
@Column
private Long vendorId;
@Event(column = "eventType")
@Column
private Integer eventType;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getGameId() {
return gameId;
}
public void setGameId(Long gameId) {
this.gameId = gameId;
}
public Integer getEventType() {
return eventType;
}
public void setEventType(Integer eventType) {
this.eventType = eventType;
}
}

Note that the user not only has to create a table for each Entity, but also one for each mapping table as well. For an existing database the user will have to index a manually for now (However: All data that is persisted AFTER the setup was done will be updated automatically):

FullTextEntityManager fem = Search.getFullTextEntityManager( this.em );
fem.beginSearchTransaction();
Game newGame = new Game( "Legend of Zelda" );
fem.index( newGame );
fem.commitSearchTransaction();
view raw IndexNew.java hosted with ❤ by GitHub
Now that we have set all this up correctly, we can search just like with normal Hibernate Search ORM:

FullTextEntityManager fem = Search.getFullTextEntityManager( this.em );
FullTextQuery fullTextQuery = fem.createFullTextQuery( new TermQuery( new Term( "title", "Legend of Zelda" ) ), Game.class );
List<Game> result = fullTextQuery.getResultList();
This is it for now. Stay tuned for further updates!

Happy Coding,
Martin

1 Kommentar: