All Articles

Linking entities to users in One-To-Many relationships

Network linking
Links between entities and users are key!

In the previous couple of articles, we’ve covered users as entities, and then covered how we could get users into our application and stored in our databases through OAuth using Google. Now, this is all great, but now we need to ensure that the users can see the items that belong to them, and only them.

Since any user can have multiple entities, be those cash accounts, stocks, cryptocurrencies or any other investment vehicle, we will need to use the OneToMany and ManyToOne annotations. This article will cover those, so it will be rather short.

Starting classes

For a recap, we will be connecting the User to the Account. Both entities, as they are defined beforehand, are below:

@Entity
@Table(name = "account", indexes = [Index(columnList = "currency")])
data class Account(
    @Id val id: UUID = UUID.randomUUID(),
    @Column(name = "name", nullable = false) val name: String,
    @Column(name = "amount", nullable = false) val amount: Double,
    @Column(name = "currency", nullable = false) val currency: Currency,
    @Column(name = "description") val description: String? = null,
    @Column(name = "added_on", nullable = false) val addedOn: LocalDate = LocalDate.now()
)
@Entity
@Table(name = "users")
data class User(
    @Id
    @GeneratedValue(generator = "UUID")
    @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
    var id: UUID? = null,
    @Column(name = "email", nullable = false, unique = true) @NotNull val email: String,
    @OneToOne(cascade = [CascadeType.ALL]) @JoinColumn(name = "preferences_id") val preferences: UserPreferences = UserPreferences(),
    @Column(name = "username") val username: String?,
    @Column(name = "firstName") val firstName: String? = null,
    @Column(name = "lastName") val lastName: String? = null,
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false
        other as User

        return id != null && id == other.id
    }

    override fun hashCode(): Int = javaClass.hashCode()
}

@Entity
@Table(name = "preferences")
class UserPreferences(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,
    @Column(name = "language") val locale: Locale = Locale.UK,
    @Column(name = "currency") val currency: Currency = Currency.getInstance("EUR"),
    @Column(name = "darkMode") val darkMode: Boolean = true
)

One To Many

Now, as mentioned, we need to link them up with one another. Essentially, the OneToMany relationship is a very simlpe one, which only adds a foreign key of the parent to its own entry in the child’s table.

Now, we need to think about which side should be the owner of the relationship. Will we require user.accounts or account.user more often? This is important for JPA when we store the values. If the user is the owner, we will use the path of the OneToMany. If the account shall be the owner, we would go with the ManyToOne as the main annotation. Or we can combine both and have a bidirectional relationship.

Ownership

In this case, it makes sense to have the user as the owning entity. We will generally start out in the user context, and retrieve his accounts.

Uni- or bidirectional

Unidirectional

Let’s first start with the unidirectional approach, and see what happens.

As mentioned, we will now only have the link from users to accounts, but the accounts have no knowledge whatsoever about the users. You may already see the issue here - how would that be represented in a table, since the relationship should be covered through the foreign key? Let’s find out!

We add the accounts to the user like so:

@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true) 
val accounts: MutableList<Account> = arrayListOf()

Of course, we now need to slightly adapt our AccountService, as this was previously only storing the account. This is not going to work for the linking, as the account has no knowledge of the user. So we adapt the createAccount to the following:

    fun createAccount(account: Account) {
        val user = SecurityContextHolder.getContext().authentication.principal as User
        user.addAccount(account)
        userRepository.save(user)
    }

(And we add the addAccount method in the User)

    fun addAccount(account: Account) {
        accounts.add(account)
    }

Now, let’s start up the application after generating the changelog with JPABuddy, and give the user a couple of accounts. After this is done, let’s check out the DB structure (technically we don’t need to add the accounts, we can simply look at the generated changelogs, but oh well…)

First, let’s check that the accounts were created:

Saved accounts
Accounts stored as expected

Okay, we can see that they were indeed created. If we check out the user again through the SecurityContextHolder, we can also see that they are attached to him. But… how?! Let’s find out:

Users accounts lookup
What are those weird tables doing there?

Wait, we never expected this users_accounts table… So how did it get there?

Well, there needs to be some way for the linking to take place. So an additional, intermediary table, which looks very much like a ManyToMany adaptation is created, where it creates an entry for every item to the user.

Creating an entire additional table doesn’t sound like it’s beneficial - and it’s really not. If you look at all the SQL statements required for these insertions, you’ll notice that there are too many. So let’s now try the bidirectional way.

Bidirectional

Let’s now explore the bidirectional way of this relationship. In theory, this would be achieved DB-wise by simply adding the users ID as a foreign key in the account. We first add the second direction of the relationship in the Account:

@ManyToOne(fetch = FetchType.LAZY) val user: User

and we adapt the mapping in the User:

@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true, mappedBy = "user")
val accounts: MutableList<Account> = arrayListOf()

Let’s generate the next changelog, and if we compare it to the previous ony, we see that it is indeed simply adding the foreign key and a constraint into the Account!

Adding an account with this setup, we have:

Accounts with user IDs
The foreign key describing the users is now there

SecurityConceptHolder setting

Now, I should mention some issues I’ve been having. Whenever I’ve created an account, I got the following Exception: failed to lazily initialize a collection of role: com.mauquoi.moneymanagement.moneymanagement.domain.entities.User.accounts, could not initialize proxy - no Session.

The reason for that exception was that we’ve put the introduced User as the principal in the SecurityContextHolder. This counts as a completed transaction for the user, but since the accounts are lazily loaded, this generates an error when trying to access them outside of a transaction.

There are several fixes for this. One potential solution would be to load the accounts eagerly by changing the annotation like so @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true, mappedBy = "user", fetch = FetchType.EAGER).

This is however not ideal, for reasons described here.

Another reason this is not ideal is simply that we will have multiple other entities in the future. There are hardly any transactions where all these entities need to be loaded always. So, the different solution is to change the principal to contain the UserDetails, but then load only those parts of the User that are actually required in the methods annotated with @Transactional. This will then require two lookups for the User in the same request, but only the required lookups for the entities, which will be more complex in bigger tables.

The models.UserDetails only require a few fields, and look as follows

data class UserDetails (
    val id: UUID,
    val email: String
)

In the OAuthGoogleSecurityFilter, the try block becomes:

try {
    val idToken: GoogleIdToken = googleIdTokenVerifier.verify(idTokenString.substring(7))
    val user = userService.loadUserByUsername(idToken.payload.email)
    SecurityContextHolder.getContext().authentication =
        UsernamePasswordAuthenticationToken(user.toUserDetails(), "not-applicable", listOf())
}

One small additional item I changed was to have hibernate generate the account’s ID:

    @Id
    @GeneratedValue(generator = "UUID")
    @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
    val id: UUID? = null,
    ...

Now, the method in the AccountService can store the account itself, and the user will be automatically aware in it in subsequent requests:

    @Transactional
    fun createAccount(account: Account) {
        val user = userService.getLoggedInUser()

        user.addAccount(account)
        accountRepository.save(account)
    }

There you have it - we finally have a linking between the User and all the entities we may want to provide him with!