I have a Character-in-Category based Jpa relationship here:
// Character.kt
#Entity
class Character (
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
var name: String, //This one
#ManyToOne #JoinColumn(name = "category_name", referencedColumnName = "name")
var category: Category? = null,
var gender: Gender? = null
): Serializable {
enum class Gender {
MALE, FEMALE, OTHER
}
}
// Category.kt
#Entity
class Category (
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
var name: String? = null, //Also this one
#OneToMany(mappedBy = "category") #JsonIgnore
var characters: MutableSet<Character> = mutableSetOf()
) : Serializable
I have done some "findBy" queries by multiple columns before, but this is the first time that both of them have the same name and i suppose its not working because of that, how can i reach this without changing any of their property names?
// CharacterRepository.kt
#Repository
interface CharacterRepository : JpaRepository<Character, Long> {
fun findByNameAndCategory_Name(name: String, categoryName: String): Character
}
Edit: What is not working is that findBYNameAndCategory_Name, always returning Result must not be null as a EmptyResultDataAccessException despite the data actually exists in database.
Spring Data follows the path from the root entity in the implements (JpaRepository<Character, Long>): this should work:
Optional<Character> findByNameAndCategoryName(String name, String categoryName)
I don't use Kotlin, so this is Java syntax, but the same should apply for Kotlin: the method name matters.
I would like to do the following:
Inserting the CityHistory into the database using JPA.
The first time there is no data, so a new city will be inserted. (IT WORKS FINE)
the (IDENTIFICATION) within the city table is a unique field.
What I want to achieve is when I am inserting the same city again is to reuse the existing field instead of trying to create a new one (identification will be like a city's unique name).
So how can I do that using JPA or Hibernate?
#Entity
public class CityHistory extends History implements Serializable {
#Id
#Column(name = "KEY_CITY_HISTORY", nullable = false, precision = 19)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "CITY_ID", nullable = false, foreignKey = #ForeignKey(name = "FK_CITY_ID"))
private City cityId;
#Column(name = "CITY_NAME", nullable = false)
private String cityName;
}
#Entity
public class City implements Serializable {
#Id
#Column(name = "KEY_CITY", nullable = false, precision = 19)
private Long id;
#Column(name = "IDENTIFICATION", nullable = false, unique = true)
private String identification;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "MUNICIPALITY_ID", foreignKey = #ForeignKey(name = "FK_MUNICIPALITY_ID"))
private Municipality municipalityId;
}
UPDATE
Here is how I am writing the data to the database,
It's a Spring Batch itemWriter
#Component
public class InfoItemWriter implements ItemWriter<Object> {
#Autowired
private CityHistoryRepository cityHistoryRepository;
#Override
public void write(List<? extends Object> items) throws Exception {
if (items.size() > 0 && items.get(0) instanceof CityHistory) {
cityHistoryRepository.saveAll((List<? extends CityHistory>) items);
}
}
}
First of all thanks to all who tried to help!
Reading the resources that #Benjamin Maurer provided:
I don't think you want the cascade on the ManyToOne side, see One-To-Many
The most common Parent – Child association consists of a one-to-many and a many-to-one relationship, where the cascade being useful for the one-to-many side only
As the relation I have is ManyToOne it was really not useful to use the cascade and doesn't serve my need.
I used a different approache to reach the goal. I have created a service where it validates the existence of a city, then adds a new city if it does not exist.
#Service
public class CityHistoryServiceImpl implements CityHistoryService {
#Autowired
CityRepository cityRepository;
#Autowired
CityHistoryRepository cityHistoryRepository;
#Override
public Optional<CityHistory> addCityHistory(City city, String cityName, ..) {
if (city != null && cityName != null) {
City city1 = addCityIfNotExist(city);
CityHistory cityHistory = new CityHistory();
cityHistory.setCityId(city1);
cityHistory.setCityName(cityName);
cityHistoryRepository.save(cityHistory);
return Optional.of(cityHistory);
}
return Optional.empty();
} ....
private City addCityIfNotExist(City city) {
City city1 = cityRepository.findFirstByBagId(city.getBagId());
if (city1 == null) {
city1 = cityRepository.save(city);
}
return city1;
}
}
Hibernate will use the #Id property of City to determine if it is new or not. When it is null, Hibernate couldn't possibly know that a similar entry already exists.
So you need to perform a query to find each city first:
for (var history : histories) {
var cities = em.createQuery("select city from City city where city.identification = ?1", City.class)
.setParameter(1, history.getCityId().getIdentification())
.getResultList();
if (!cities.isEmpty()) {
history.setCityId(cities.get(0));
}
em.persist(history);
}
If you use Hibernate and City.identification is unique and always non-null, you can use it as a NaturalID:
In City:
#NaturalId
private String identification;
Then:
for (var history : histories) {
var city = em.unwrap(Session.class)
.byNaturalId(City.class)
.using("identification", history.getCityId().getIdentification())
.getReference();
if (city != null) {
history.setCityId(city);
}
em.persist(history);
}
But if you do have City.id set, i.e., not null, you can use EntityManager.merge to get a managed entity:
for (var history : histories) {
City city = history.getCityId();
if (city.getId() != null) {
city = em.merge(city);
history.setCityId(city);
}
em.persist(history);
}
One more remark: We are not in the relational domain, but we are mapping object graphs. So calling your fields cityId and municipalityId is arguably wrong - even the type says so: City cityId.
They are not just plain identifiers, but full fledged objects: City city.
I cannot get the below OneToMany mappings to work properly, even though they are supposedly validated (by hibernate.ddl-auto=validate). I can insert all entities in the application with no problems, but while doing a findAll or findById, the queries Hibernate generates for me are wrong and result in exceptions. This is very likely due to a problem with my OneToMany mappings, or lack of a ManyToOne mapping but I don't see how to make it work.
Currently, the following tables exist in my postgres12 database:
CREATE TABLE battlegroups (
id uuid,
gameworld_id uuid,
name varchar(255),
PRIMARY KEY(id)
);
CREATE TABLE battlegroup_players (
id uuid,
battlegroup_id uuid,
player_id integer,
name varchar(255),
tribe varchar(255),
PRIMARY KEY (id)
);
CREATE TABLE battlegroup_player_villages(
battlegroup_id uuid,
player_id integer,
village_id integer,
x integer,
y integer,
village_name varchar(255),
tribe varchar(255),
PRIMARY KEY(battlegroup_id, player_id, village_id, x, y)
);
These are mapped to the following entities in Kotlin:
#Entity
#Table(name = "battlegroups")
class BattlegroupEntity(
#Id
val id: UUID,
#Column(name = "gameworld_id")
val gameworldId: UUID,
val name: String? = "",
#OneToMany(mappedBy = "battlegroupId", cascade = [CascadeType.ALL],fetch = FetchType.EAGER)
private val players: MutableList<BattlegroupPlayerEntity>)
#Entity
#Table(name = "battlegroup_players")
class BattlegroupPlayerEntity(#Id
val id: UUID,
#Column(name = "battlegroup_id")
val battlegroupId: UUID,
#Column(name = "player_id")
val playerId: Int,
val name: String,
#Enumerated(EnumType.STRING)
val tribe: Tribe,
#OneToMany(mappedBy= "id.playerId" , cascade = [CascadeType.ALL], fetch = FetchType.EAGER)
val battlegroupPlayerVillages: MutableList<BattlegroupPlayerVillageEntity>)
#Entity
#Table(name = "battlegroup_player_villages")
class BattlegroupPlayerVillageEntity(
#EmbeddedId
val id: BattlegroupPlayerVillageId,
#Column(name ="village_name")
val villageName: String,
#Enumerated(EnumType.STRING)
val tribe: Tribe)
#Embeddable
data class BattlegroupPlayerVillageId(
#Column(name = "battlegroup_id")
val battlegroupId: UUID,
#Column(name = "player_id")
val playerId: Int,
#Column(name = "village_id")
val villageId: Int,
val x: Int,
val y: Int
): Serializable
This is the SQL hibernate generates when I do a findAll/findById on a battlegroup:
select
battlegrou0_.id as id1_2_0_,
battlegrou0_.gameworld_id as gameworl2_2_0_,
battlegrou0_.name as name3_2_0_,
players1_.battlegroup_id as battlegr2_1_1_,
players1_.id as id1_1_1_,
players1_.id as id1_1_2_,
players1_.battlegroup_id as battlegr2_1_2_,
players1_.name as name3_1_2_,
players1_.player_id as player_i4_1_2_,
players1_.tribe as tribe5_1_2_,
battlegrou2_.player_id as player_i2_0_3_,
battlegrou2_.battlegroup_id as battlegr1_0_3_,
battlegrou2_.village_id as village_3_0_3_,
battlegrou2_.x as x4_0_3_,
battlegrou2_.y as y5_0_3_,
battlegrou2_.battlegroup_id as battlegr1_0_4_,
battlegrou2_.player_id as player_i2_0_4_,
battlegrou2_.village_id as village_3_0_4_,
battlegrou2_.x as x4_0_4_,
battlegrou2_.y as y5_0_4_,
battlegrou2_.tribe as tribe6_0_4_,
battlegrou2_.village_name as village_7_0_4_
from
battlegroups battlegrou0_
left outer join
battlegroup_players players1_
on battlegrou0_.id=players1_.battlegroup_id
left outer join
battlegroup_player_villages battlegrou2_
on players1_.id=battlegrou2_.player_id -- ERROR: comparing integer to uuid
where
battlegrou0_.id=?
This results in an exception:
PSQLException: ERROR: operator does not exist: integer = uuid
Which makes perfect sense, since it is comparing the battlegroup_players id, which is a uuid, to the battlegroup_player_villages player_id, which is an integer. It should instead be comparing/joining on the battlegroup_player's player_id to the battlegroup_player_village's player_id.
If I change the sql to reflect that and manually execute the above query with the error line replaced:
on players1_.player_id=battlegrou2_.player_id
I get exactly the results I want. How can I change the OneToMany mappings so that it does exactly that?
Is it possible to do this without having a BattlegroupPlayerEntity object in my BattlegroupPlayerVillageEntity class?
Bonus points if you can get the left outer joins to become regular inner joins.
EDIT:
I tried the current answer, had to slightly adjust my embedded id because my code could not compile otherwise, should be the same thing:
#Embeddable
data class BattlegroupPlayerVillageId(
#Column(name = "battlegroup_id")
val battlegroupId: UUID,
#Column(name = "village_id")
val villageId: Int,
val x: Int,
val y: Int
): Serializable {
#ManyToOne
#JoinColumn(name = "player_id")
var player: BattlegroupPlayerEntity? = null
}
Using this still results in a comparison between int and uuid, for some reason.
Schema-validation: wrong column type encountered in column [player_id] in table [battlegroup_player_villages]; found [int4 (Types#INTEGER)], but expecting [uuid (Types#OTHER)]
Interestingly, if I try to put a referencedColumnName = "player_id" in there, I get a stackoverflow error instead.
I did some digging and found some issues with the mapping as well as classes, I will try to explain as much as possible.
WARNING!!! TL;DR
I will use Java for code, I hope that should not be a problem converting to kotlin.
There are some issues with classes also(hint: Serializable), so classes must implements Serializable.
Used lombok to reduce the boilerplate
Here is the changed BattleGroupPlayer entity:
#Entity
#Getter
#NoArgsConstructor
#Table(name = "battle_group")
public class BattleGroup implements Serializable {
private static final long serialVersionUID = 6396336405158170608L;
#Id
private UUID id;
private String name;
#OneToMany(mappedBy = "battleGroupId", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<BattleGroupPlayer> players = new ArrayList();
public BattleGroup(UUID id, String name) {
this.id = id;
this.name = name;
}
public void addPlayer(BattleGroupPlayer player) {
players.add(player);
}
}
and BattleGroupVillage and BattleGroupVillageId entity
#AllArgsConstructor
#Entity
#Getter
#NoArgsConstructor
#Table(name = "battle_group_village")
public class BattleGroupVillage implements Serializable {
private static final long serialVersionUID = -4928557296423893476L;
#EmbeddedId
private BattleGroupVillageId id;
private String name;
}
#Embeddable
#EqualsAndHashCode
#Getter
#NoArgsConstructor
public class BattleGroupVillageId implements Serializable {
private static final long serialVersionUID = -6375405007868923427L;
#Column(name = "battle_group_id")
private UUID battleGroupId;
#Column(name = "player_id")
private Integer playerId;
#Column(name = "village_id")
private Integer villageId;
public BattleGroupVillageId(UUID battleGroupId, Integer playerId, Integer villageId) {
this.battleGroupId = battleGroupId;
this.villageId = villageId;
this.playerId = playerId;
}
}
Now, Serializable needs to be implemented in every class as we have used #EmbeddedId which requires the container class to be Serializable as well, hence every parent class must implement serializable, otherwise it would give error.
Now, we can solve the problem using #JoinColumn annotation like below:
#OneToMany(cascade = CasacadeType.ALL, fetch =EAGER)
#JoinColumn(name = "player_id", referencedColumnName = "player_id")
private List<BattleGroupVillage> villages = new ArrayList<>();
name -> field in child table and referenceColumnName -> field in parent table.
This will join the column player_id column in both entities.
SELECT
battlegrou0_.id AS id1_0_0_,
battlegrou0_.name AS name2_0_0_,
players1_.battle_group_id AS battle_g2_1_1_,
players1_.id AS id1_1_1_,
players1_.id AS id1_1_2_,
players1_.battle_group_id AS battle_g2_1_2_,
players1_.player_id AS player_i3_1_2_,
villages2_.player_id AS player_i4_2_3_,
villages2_.battle_group_id AS battle_g1_2_3_,
villages2_.village_id AS village_2_2_3_,
villages2_.battle_group_id AS battle_g1_2_4_,
villages2_.player_id AS player_i4_2_4_,
villages2_.village_id AS village_2_2_4_,
villages2_.name AS name3_2_4_
FROM
battle_group battlegrou0_
LEFT OUTER JOIN
battle_group_player players1_ ON battlegrou0_.id = players1_.battle_group_id
LEFT OUTER JOIN
battle_group_village villages2_ ON players1_.player_id = villages2_.player_id
WHERE
battlegrou0_.id = 1;
But this would give 2 players if you check the BattleGroup#getPlayers() method, below is the test case to verify.
UUID battleGroupId = UUID.randomUUID();
doInTransaction( em -> {
BattleGroupPlayer player = new BattleGroupPlayer(UUID.randomUUID(), battleGroupId, 1);
BattleGroupVillageId villageId1 = new BattleGroupVillageId(
battleGroupId,
1,
1
);
BattleGroupVillageId villageId2 = new BattleGroupVillageId(
battleGroupId,
1,
2
);
BattleGroupVillage village1 = new BattleGroupVillage(villageId1, "Village 1");
BattleGroupVillage village2 = new BattleGroupVillage(villageId2, "Village 2");
player.addVillage(village1);
player.addVillage(village2);
BattleGroup battleGroup = new BattleGroup(battleGroupId, "Takeshi Castle");
battleGroup.addPlayer(player);
em.persist(battleGroup);
});
doInTransaction( em -> {
BattleGroup battleGroup = em.find(BattleGroup.class, battleGroupId);
assertNotNull(battleGroup);
assertEquals(2, battleGroup.getPlayers().size());
BattleGroupPlayer player = battleGroup.getPlayers().get(0);
assertEquals(2, player.getVillages().size());
});
If your use case was to get the single player from BattleGroup then you would have to use FETCH.LAZY, which is btw good for performance as well.
Why LAZY works?
Because LAZY loading will issue separate select statement when you really access them. EAGER will load whole graph, wherever you have it. It means, it will try to load all relationship mapped with this type, hence it will perform outer join (which may result in 2 rows for players as your criteria is unique because of villageId, which you cannot know before querying).
If you have more than 1 such fields i.e want join on battleGroupId as well, you would need this
#JoinColumns({
#JoinColumn(name = "player_id", referencedColumnName = "player_id"),
#JoinColumn(name = "battle_group_id", referencedColumnName = "battle_group_id")
}
)
NOTE: Used h2 in memory db for test case
I am newcomer.
How to migrate room database.
I tried to migrate using this instruction https://developer.android.com/training/data-storage/room/migrating-db-versions , but don't understand where should I use it.
I changed version from 1 to 2 and add column #ColumnInfo(name = "age") val age: Int
Could you help me to migrate it?
#Entity
data class User(
#PrimaryKey(autoGenerate = true) val uid: Int = 0,
#ColumnInfo(name = "first_name") val firstName: String?,
#ColumnInfo(name = "last_name") val lastName: String?,
#ColumnInfo(name = "age") val age: Int
)
#Dao
interface UserDao {
#Query("SELECT * FROM user")
fun getAll(): LiveData<List<User>>
#Query("SELECT * FROM user WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): List<User>
#Query(
"SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1"
)
fun findByName(first: String, last: String): User
#Insert
fun insertAll(vararg users: User)
#Delete
fun delete(user: User)
}
#Database(entities = arrayOf(User::class), version = 2)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
private val mBD = AppDatabase
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE 'User' ADD COLUMN 'age' INTEGER DEFAULT 0")
}
}
}
}
I have Two Entity User and Account. Both have one to one mapping.
Here is the Entity Classes :
User:
#Entity
#Table(name = 'docutools_users')
public class DocutoolsUser {
#Id
#Type(type = "pg-uuid")
UUID id = UUID.randomUUID();
#OneToOne(mappedBy = "user", cascade = ALL)
Account account;}
Account
#Entity
#Table(name = "accounts")
public class Account {
#Id
#Type(type = "pg-uuid")
private UUID id = UUID.randomUUID();
#Column(nullable = false)
private LocalDate activated = LocalDate.now();
#OneToOne
private DocutoolsUser user;
}
Query
#Query("SELECT u FROM DocutoolsUser u WHERE u.account IS NOT NULL")
Page<DocutoolsUser> findByAccountNotNull()
I am using JPA repositery.
The expression u.account IS NOT NULL always return true even there is no account in user.
Thanks
Exact Query is here
#Query("""SELECT u FROM DocutoolsUser u WHERE u.organisation = :org AND UPPER(CONCAT(u.name.firstName, ' ', u.name.lastName)) LIKE :search AND ((CASE WHEN ( (u.id = u.organisation.owner.id AND u.account IS NULL) OR ( u.account IS NOT NULL AND (u.account.subscription.until IS NULL or u.account.subscription.until > :currentDate)) ) THEN true ELSE false END) IS :isLicensed )""")
Page<DocutoolsUser> findByOrganisationAndLicense(#Param('org') Organisation organisation, #Param('search') String search, #Param('currentDate') LocalDate currentDate, #Param('isLicensed') Boolean isLicensed, Pageable pageable)
you can do this without #Query using JpaRepository and IsNotNull
#Repository
public interface DocutoolsUserRepository extends JpaRepository<DocutoolsUser, Long> {
// where Account not null
public List<DocutoolsUser> findByAccountIsNotNull();
}
for more informations : https://docs.spring.io/spring-data/jpa/docs/1.5.0.RELEASE/reference/html/jpa.repositories.html
for the whole query you can combine many jpaRepository services