I have a enum of few status value
NEW, REVIEWD, PUBLISHED, PENDING, UPDATED, SPAM, DUPLICATE, IRRELEVANT, UNPUBLISHED
I don't want to use them as enumerated so created one entity for that. For convenient I want to keep a column in entity to initialize status from enum and convert that enumerated value to a Object of status entity. for this..
I have two entity. I want to refer a column with value from another entity.
Basically I want to initialize a object with formula.
Entities are
#Entity
#Table(name = "event_status")
public class EventStatus {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="eventStatusId")
private Integer eventStatusId;
#Enumerated(EnumType.STRING)
#Column(unique = true,name="eventStatusType")
private EventStatusType eventStatusType;
public EventStatus() {
this(EventStatusType.NEW);
}
public EventStatus(EventStatusType eventStatusType) {
super();
this.eventStatusType = eventStatusType;
}
public Integer getEventStatusId() {
return eventStatusId;
}
public EventStatusType getEventStatusType() {
return eventStatusType;
}
public void setEventStatusId(Integer eventStatusId) {
this.eventStatusId = eventStatusId;
}
public void setEventStatusType(EventStatusType eventStatusType) {
this.eventStatusType = eventStatusType;
}
}
I have another entity in which I am referring object of this entity
#Entity
#Table(name = "event_")
#Inheritance(strategy = InheritanceType.JOINED)
public abstract class Event implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "id_")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Transient
public EventStatusType eventStatusType = EventStatusType.NEW;
#ManyToOne(fetch = FetchType.EAGER, targetEntity = EventStatus.class)
#Formula("select * from event_status where eventStatusId= 1")
private EventStatus status;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public EventStatus getStatus() {
System.out.println("Event.getStatus() " + status);
return status;
}
public void setStatus(EventStatus status) {
System.out.println("Event.setStatus()");
this.status = status;
}
}
This is not giving any exception but not initializing this value.
Is it possible to initialize this EntityStatus with value of eventStatusType in Event entity
I would like to explain that based on the documentation:
5.1.4.1.5. Formula
Sometimes, you want the Database to do some computation for you rather than in the JVM, you might also create some kind of virtual column. You can use a SQL fragment (aka formula) instead of mapping a property into a column. This kind of property is read only (its value is calculated by your formula fragment).
#Formula("obj_length * obj_height * obj_width")
public long getObjectVolume()
The SQL fragment can be as complex as you want and even include subselects.
...
5.1.7.1. Using a foreign key or an association table
...
Note
You can use a SQL fragment to simulate a physical join column using the #JoinColumnOrFormula / #JoinColumnOrformulas annotations (just like you can use a SQL fragment to simulate a property column via the #Formula annotation).
#Entity
public class Ticket implements Serializable {
#ManyToOne
#JoinColumnOrFormula(formula="(firstname + ' ' + lastname)")
public Person getOwner() {
return person;
}
...
}
Also, we should use insertable = false, updatable = false, because such mapping is not editable
Related
There is a given database structure and graphql schema.
Fortunately they have a lot in common but unfortunately there are some difference.
Let's say there are entities in java to match the following database structure.
SQL:
TABLE ANIMAL
+ID NUMBER(19)
+NR_OF_LEGS NUMBER(19)
TABLE SHEEP
+ID NUMBER
+LAST_TIME_SHEARED DATETIME
+ANIMAL_ID NUMBER(19)
TABLE COW
+MILK_IN_L NUMBER(3)
+ANIMAL_ID NUMER(19)
Java:
#Entity
#Table(name = "ANIMAL")
public class Animal
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(name="nrOfLegs", nullable=false)
private long nrOfLegs;
}
#Entity
#Table(name = "SHEEP")
public class SheepE
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(name="lastTimeSheared", nullable=false)
private Datetime lastTimeSheared;
#ManyToOne(targetEntity = AnimalE.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "animalId", nullable = false, insertable = false, updatable = false)
private Animal animal;
}
#Entity
#Table(name = "COW")
public class CowE
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(name="milkInL", nullable=false)
private int milkInL;
#ManyToOne(targetEntity = AnimalE.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "animalId", nullable = false, insertable = false, updatable = false)
private Animal animal;
}
The existing GraphQl schema is considered to be like this:
type Sheep{
id: int!
lastTimeSheard: String!
nrOfLegs: int!
}
type Cow {
id: int!
milkInL: int!
nrOfLegs: int
}
The project uses graphql-java in version 11.0 (guess we should update soon)
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>11.0</version>
</dependency>
The graphql works fine and isimplemented like this:
#Component
public class GraphQLProvider {
#Autowired
GraphQLDataFetchers graphQLDataFetchers;
private GraphQL graphQL;
#PostConstruct
public void init() {this.graphQL = /*init;*/null;}
private RuntimeWiring buildWiring() {
RuntimeWiring.Builder b = RuntimeWiring.newRuntimeWiring()
.type(TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("freightCarrier", graphQLDataFetchers.getCow()))
.type(TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("personCarrier", graphQLDataFetchers.getSheep())));
return b.build();
}
}
#Component
public class GraphQLDataFetchers {
#AutoWired
private CowRepository cowRepo;
#AutoWired
private sheepRepository sheepRepo;
public DataFetcher getCow() {
DataFetcher dataFetcher = (DataFetchingEnvironment dfe) -> {
int id = dfe.getArgument("id");
return getGraphQlCowFromCowEntity(cowRepo.getById(id));//dirty!
};
return dataFetcher;
}
public DataFetcher getCow() {
DataFetcher dataFetcher = (DataFetchingEnvironment dfe) -> {
int id = dfe.getArgument("id");
return getGraphQlSheepFromSheepEntity(cowRepo.getById(id));//dirty!
};
return dataFetcher;
}
private Cow getGraphQlCowFromCowEntity(CowE ce){//dirty!
return new Cow(ce.getId(), ce.getMilkInL(),ce.getLegs());
}
private Sheep getGraphQlSheepFromSheepEntity(SheepE se){//dirty!
return new Sheep(se.getId(), se.getLastTime(),se.getLegs());
}
public class Sheep
private long id;
private Datetime lastTimeSheared;
private int nrOfLegs;
public Sheep(long id, DateTime lasttimeSheared, int nrOfLegs){
//u know what happens here
}
}
public class Cow
private long id;
private int milkInL;
private int nrOfLegs;
public Sheep(long id, int milkInL, int nrOfLegs){
//u know what happens here
}
}
So how to get rid of getGraphQlCowFromCowEntity and getGraphQlSheepFromSheepEntity. It double ups the code and also is in direct conflict to what graphql is suppose to be abstraction of the data. With this design here each time all fields are loaded through jpa and not only requested fields.
Imagine this is a way more complex environment with more fields.
The graphql schema can't be changed as it's not my responsibility, changing the entire back-end to match schema is also not what I want to archive.
Kind regards
You should use DTO. Retrieving and sending entity object is bad practice as you do not want your grahql api to change every time you refactor you database model, or in your case. Your Sheep and Cow objects are DTO, but you will need some way to convert your entity to DTO (getGraphQlCowFromCowEntity is fine, but you could use polymorphism - CowEntity.toDTO() - or have a service layer do the conversion, there are plenty of way to do this).
To answer your concerns about loading only the requested data, you want your DTO object to only be populated with the requested fields. One way to do this is, instead of populating all fields, have the DTO own a reference to the entity object and retrieve the data from the entity object only when requested.
public class Sheep {
private SheepE entity;
public Sheep(SheepE entity){
this.entity=entity;
}
public getId() {
return entity.getId();
}
public getLastTimeSheared() {
return entity.getLastTimeSheared();
}
...
}
Please see this answer I wrote to a similar question: Graphql Tools: Map entity type to graphql type
I am using Spring-Boot with JPA and a MySQL backend. Now I got quite confused about the repositories Spring-Boot provides. I know these are quite powerful (and seem to be quite useful since they can shorten your code a lot). Still, I do not understand how to represent Joins within them, since the result-set should be a combination of specified attributes in the select of a few Entities.
Now let's assume we have three tables Book, Author, AuthorOfBook, where the last one is simply connecting Book and Author by a combined Primary key. I guess we had the following Java-Classes:
Entity Book:
#Entity
#Table(name="BOOK")
public class Book {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private int id;
#Column(name = "TITLE")
private String title;
}
Entity Author
#Entity
#Table(name="AUTHOR")
public class Author {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private int id;
#Column(name = "LASTNAME")
private String lastname;
#Column(name = "FIRSTNAME")
private String firstname;
//Let's assume some getters and setters and a constructor
}
Entity AuthorOfBook:
#Entity
#Table(name="BOOK")
public class Book {
#EmbeddedId
private AuthorOfBookId pk;
}
An Embedded ID
#Embeddable
public class AuthorOfBookId implements Serializable {
private int authorId;
private int bookId;
}
Repository
#Repository
public interface AuthorOfBookRepository extends JpaRepository<,AuthorOfBookId> {
}
Now how would I represent that query:
SELECT b.name, a.firstname, a.lastname from AuthorOfBook ab inner join Book b on b.id = ab.book_id inner join Author a on a.id = ab.author_id where a.lastname = :lastname;
in my repository? I know the signature would need to be like
#Query([the query string from above])
public (...) findAuthorAndBookByAuthorLastname(#Param("lastname") String lastname);
but I cannot make out what Type the return would be like. What is that method returning? (simply AuthorOfBook would not work I guess)
You don't want AuthorOfBook as a separate Entity. Book should have a field of type Author as a #ManyToOne relationship. That way, given any Book, you can find the author's details.
If you want to handle audits fields you can do something like this:
Audit class
#Embeddable
public class Audit {
#Column(name = "created_on")
private Timestamp createdOn;
#Column(name = "updated_on")
private Timestamp updatedOn;
#Column(name = "is_deleted")
private Boolean isDeleted;
//getters and setters
}
AuditListener to update automatically audits fields
public class AuditListener {
private Long loggedUser = 1001L;
/**
* Method to set the fields createdOn, and isDeleted when an entity is persisted
* #param auditable
*/
#PrePersist
public void setCreatedOn(Auditable auditable) {
Audit audit = auditable.getAudit();
if (audit == null) {
audit = new Audit();
auditable.setAudit(audit);
}
audit.setIsDeleted(Boolean.FALSE);
audit.setCreatedOn(Timestamp.from(Instant.now()));
}
/**
* Method to set the fields updatedOn and updatedBy when an entity is updated
* #param auditable
*/
#PreUpdate
public void setUpdatedOn(Auditable auditable) {
Audit audit = auditable.getAudit();
audit.setUpdatedOn(Timestamp.from(Instant.now()));
}
}
And add this to the entities
#EntityListeners(AuditListener.class)
public class Book implements Auditable {
#Embedded
private Audit audit;
Is it possible to use Enums as a type of a field (column) in custom JPA entities? Here is an example:
#Getter #Setter
#Entity
#Table(name = "payments")
public class PaymentEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(name = "status")
private Integer statusId;
public PaymentStatuses getStatus() {
return PaymentStatuses.valueOf(statusId);
}
public PaymentEntity setStatus(PaymentStatuses status) {
statusId = status == null ? null : status.getId();
return this;
}
}
public enum PaymentStatuses {
CREATED(1),
COMPLETED(2),
CANCELED(3);
private Integer id;
private PaymentStatuses(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public static PaymentStatuses valueOf(Integer id) {
for (PaymentStatuses value : values())
if (value.getId().equals(id))
return value;
return null;
}
}
Code above works fine, but approach with statusId and getStatus setStatus looks ugly a little bit.
I wanna use PaymentStatuses as a type of the field in my entity. Like this:
#Getter #Setter
#Entity
#Table(name = "payments")
public class PaymentEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(name = "status")
private PaymentStatuses status;
}
Tell me please, is it possible?
Using #Enumerated(EnumType.ORDINAL) will not work because the ORDINAL mode starts to 0.
So the 3 first values of the enum will be represented in DB with the 0, 1 and 2 values.
while the id field in the enum to represent it in the DB goes from 1 to 3 :
CREATED(1),
COMPLETED(2),
CANCELED(3);
Besides, this way would correlate the order of elements defined in the enum with the way to represent them in database. Which not a good thing as enum values could be added/removed in the future.
A better way to address your issue is defining a javax.persistence.AttributeConverter and using it with #Convert.
So create a AttributeConverter implementation to indicate how to convert from the DB to the enum and the enum to the DB.
Then declare a PaymentStatuses status in your entity and annotate it with #Convert by specifying the AttributeConverter implementation class.
#Getter #Setter
#Entity
#Table(name = "payments")
public class PaymentEntity {
...
#Convert(converter = PaymentStatusesConverter.class)
private PaymentStatuses status;
...
}
public class PaymentStatusesConverter implements AttributeConverter<PaymentStatuses, Integer> {
#Override
public Integer convertToDatabaseColumn(PaymentStatuses status) {
return status.getId();
}
#Override
public PaymentStatuses convertToEntityAttribute(Integer status) {
return PaymentStatuses.valueOf(status);
}
}
Yes, but when you save to the database it will persist the current index of the enum value (in your case 0 for CREATED, 1 for COMPLETED, etc.) which will give you trouble if you change the enum values. To avoid this you can use the #Enumerated annotation like:
#Getter #Setter
#Entity
#Table(name = "payments")
public class PaymentEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(name = "status")
#Enumerated(EnumType.ORDINAL) // There is also EnumType.STRING, or you can define a custom EnumType
private PaymentStatuses status;
}
You can use #Enumerated(EnumType.STRING) if you want your data to be stored in the db like the name of the Java enum name (CREATED, COMPLETED, CANCELED)
I have following kinds of classes for hibernate entity hierarchy. I am trying to have two concrete sub classes Sub1Class and Sub2Class. They are separated by a discriminator column (field) that is defined in MappedSuperClass. There is a abstract entity class EntitySuperClass which is referenced by other entities. The other entities should not care if they are actually referencing Sub1Class or Sub2Class.
It this actually possible? Currently I get this error (because column definition is inherited twice in Sub1Class and in EntitySuperClass) :
Repeated column in mapping for entity: my.package.Sub1Class column: field (should be mapped with insert="false" update="false")
If I add #MappedSuperClass to EntitySuperClass, then I get assertion error from hiberante: it does not like if a class is both Entity and a mapped super class. If I remove #Entity from EntitySuperClass, the class is no longer entity and can't be referenced from other entities:
MappedSuperClass is a part of external package, so if possible it should not be changed.
My classes:
#MappedSuperclass
public class MappedSuperClass {
private static final String ID_SEQ = "dummy_id_seq";
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = ID_SEQ)
#GenericGenerator(name=ID_SEQ, strategy="sequence")
#Column(name = "id", unique = true, nullable = false, insertable = true, updatable = false)
private Integer id;
#Column(name="field", nullable=false, length=8)
private String field;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getField() {
return field;
}
public void setField(String field) {
this.field = field;
}
}
#Entity
#Table(name = "ACTOR")
#Inheritance(strategy=InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name="field", discriminatorType=DiscriminatorType.STRING)
abstract public class EntitySuperClass extends MappedSuperClass {
#Column(name="description", nullable=false, length=8)
private String description;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
#Entity
#DiscriminatorValue("sub1")
public class Sub1Class extends EntitySuperClass {
}
#Entity
#DiscriminatorValue("sub2")
public class Sub2Class extends EntitySuperClass {
}
#Entity
public class ReferencingEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Integer id;
#Column
private Integer value;
#ManyToOne
private EntitySuperClass entitySuperClass;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getValue() {
return value;
}
public void setValue(Integer value) {
this.value = value;
}
public EntitySuperClass getEntitySuperClass() {
return entitySuperClass;
}
public void setEntitySuperClass(EntitySuperClass entitySuperClass) {
this.entitySuperClass = entitySuperClass;
}
}
In my project it is done this way:
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "field", discriminatorType = DiscriminatorType.STRING)
#DiscriminatorValue("dummy")
public class EntitySuperClass {
// here definitions go
// but don't define discriminator column here
}
#Entity
#DiscriminatorValue(value="sub1")
public class Sub1Class extends EntitySuperClass {
// here definitions go
}
And it works. I think your problem is that you needlessly define discriminator field in your superclass definition. Remove it and it will work.
In order to use a discriminator column as a normal property you should make this property read-only with insertable = false, updatable = false. Since you can't change MappedSuperClass, you need to use #AttributeOverride:
#Entity
#Table(name = "ACTOR")
#Inheritance(strategy=InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name="field", discriminatorType=DiscriminatorType.STRING)
#AttributeOverride(name = "field",
column = #Column(name="field", nullable=false, length=8,
insertable = false, updatable = false))
abstract public class EntitySuperClass extends MappedSuperClass {
...
}
You can map a database column only once as read-write field (a field that has insertable=true and/or updatable=true) and any number times as read-only field (insertable=false and updatable=false). Using a column as #DiscriminatorColumn counts as read-write mapping, so you can't have additional read-write mappings.
Hibernate will set value specified in #DiscriminatorColumn behind the scenes based on the concrete class instance. If you could change that field, it would allow modifying the #DiscriminatorColumn field so that your subclass and value in the field may not match.
One fundamental: You effectively should not need to retrieve your discriminator column from DB. You should already have that information within the code, of which you use in your #DiscriminatorValue tags. If you need read that from DB, reconsider carefully the way you are assigning discriminators.
If you need it in final entity object, one good practice can be to implement an Enum from discriminator value and return store it in a #Transient field:
#Entity
#Table(name="tablename")
#DiscriminatorValue(Discriminators.SubOne.getDisc())
public class SubClassOneEntity extends SuperClassEntity {
...
#Transient
private Discriminators discriminator;
// Setter and Getter
...
}
public enum Discriminators {
SubOne ("Sub1"),
SubOne ("Sub2");
private String disc;
private Discriminators(String disc) { this.disc = disc; }
public String getDisc() { return this.disc; }
}
I have 2 tables, devices which contains a list of devices and dev_tags, which contains a list of asset tags for these devices. The tables join on dev_serial_num, which is the primary key of neither table. The devices are unique on their ip_address field and they have a primary key identified by dev_id. The devices "age out" after 2 weeks. Therefore, the same piece of hardware can show up more than once in devices.
I mention that to explain why there is a OneToMany relationship between dev_tags and devices where it seems that this should be a OneToOne relationship.
So I have my 2 entities
#Entity
#Table(name = "dev_tags")
public class DevTags implements Serializable {
private Integer tagId;
private String devTagId;
private String devSerialNum;
private List<Devices> devices;
#Id
#GeneratedValue
#Column(name = "tag_id")
public Integer getTagId() {
return tagId;
}
public void setTagId(Integer tagId) {
this.tagId = tagId;
}
#Column(name="dev_tag_id")
public String getDevTagId() {
return devTagId;
}
public void setDevTagId(String devTagId) {
this.devTagId = devTagId;
}
#Column(name="dev_serial_num")
public String getDevSerialNum() {
return devSerialNum;
}
public void setDevSerialNum(String devSerialNum) {
this.devSerialNum = devSerialNum;
}
#OneToMany(mappedBy="devSerialNum")
public List<Devices> getDevices() {
return devices;
}
public void setDevices(List<Devices> devices) {
this.devices = devices;
}
}
and this one
public class Devices implements java.io.Serializable {
private Integer devId;
private Integer officeId;
private String devSerialNum;
private String devPlatform;
private String devName;
private OfficeView officeView;
private DevTags devTag;
public Devices() {
}
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "dev_id", unique = true, nullable = false)
public Integer getDevId() {
return this.devId;
}
public void setDevId(Integer devId) {
this.devId = devId;
}
#Column(name = "office_id", nullable = false, insertable=false, updatable=false)
public Integer getOfficeId() {
return this.officeId;
}
public void setOfficeId(Integer officeId) {
this.officeId = officeId;
}
#Column(name = "dev_serial_num", nullable = false, length = 64, insertable=false, updatable=false)
#NotNull
#Length(max = 64)
public String getDevSerialNum() {
return this.devSerialNum;
}
public void setDevSerialNum(String devSerialNum) {
this.devSerialNum = devSerialNum;
}
#Column(name = "dev_platform", nullable = false, length = 64)
#NotNull
#Length(max = 64)
public String getDevPlatform() {
return this.devPlatform;
}
public void setDevPlatform(String devPlatform) {
this.devPlatform = devPlatform;
}
#Column(name = "dev_name")
public String getDevName() {
return devName;
}
public void setDevName(String devName) {
this.devName = devName;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "office_id")
public OfficeView getOfficeView() {
return officeView;
}
public void setOfficeView(OfficeView officeView) {
this.officeView = officeView;
}
#ManyToOne()
#JoinColumn(name="dev_serial_num")
public DevTags getDevTag() {
return devTag;
}
public void setDevTag(DevTags devTag) {
this.devTag = devTag;
}
}
I messed around a lot with #JoinColumn(name=) and the mappedBy attribute of #OneToMany and I just cannot get this right. I finally got the darn thing to compile, but the query is still trying to join devices.dev_serial_num to dev_tags.tag_id, the #Id for this entity. Here is the transcript from the console:
13:12:16,970 INFO [STDOUT] Hibernate:
select
devices0_.office_id as office5_2_,
devices0_.dev_id as dev1_2_,
devices0_.dev_id as dev1_156_1_,
devices0_.dev_name as dev2_156_1_,
devices0_.dev_platform as dev3_156_1_,
devices0_.dev_serial_num as dev4_156_1_,
devices0_.office_id as office5_156_1_,
devtags1_.tag_id as tag1_157_0_,
devtags1_.comment as comment157_0_,
devtags1_.dev_serial_num as dev3_157_0_,
devtags1_.dev_tag_id as dev4_157_0_
from
ond.devices devices0_
left outer join
ond.dev_tags devtags1_
on devices0_.dev_serial_num=devtags1_.tag_id
where
devices0_.office_id=?
13:12:16,970 INFO [IntegerType] could not read column value from result set: dev4_156_1_; Invalid value for getInt() - 'FDO1129Y2U4'
13:12:16,970 WARN [JDBCExceptionReporter] SQL Error: 0, SQLState: S1009
13:12:16,970 ERROR [JDBCExceptionReporter] Invalid value for getInt() - 'FDO1129Y2U4'
That value for getInt() 'FD01129Y2U4' is a serial number, definitely not an Int! What am I missing/misunderstanding here? Can I join 2 tables on any fields I want or does at least one have to be a primary key?
The short answer is "no, you can't join 2 tables on any fields"; association will always refer to primary key on one side.
"mappedBy" attribute for #OneToMany is used for bi-directional assocations and specifies the name of the property on collection element that maps back to owner entity as #ManyToOne. In your case,
#OneToMany(mappedBy="devSerialNum")
declaration is invalid; it should be changed to
#OneToMany(mappedBy="devTag")
instead if you want to maintain a bi-directional relationship. #JoinColumn can be used with #ManyToOne to specify the name of the (foreign key) column pointing to the other table. In your case,
#ManyToOne()
#JoinColumn(name="dev_serial_num")
public DevTags getDevTag() {
declaration says that you have a column called dev_serial_num in your devices table that will be a foreign key pointing to dev_tags.tag_id which is also wrong.
I'm not quite clear on what you meant by "devices age out", but it seems to me that you're trying to merge two separate concepts into a single table which is where all these issues stem from. Consider instead separating your "devices" table (and entity) into two:
"Core" device (for the lack of better name) should contain truly unique attributes, like serial number. Your DevTags entity will be linked as one-to-many to this one.
Device "version" would contain attributes applicable to individual "version". There will be multiple "versions" for each "core" device; "version" is what will be updated every 2 weeks.