I am trying to make a call out from salesforce most of the code is copied from another working package.
Can anyone tell me why the call out method below is never run?
I am saving into my custom table before and after the call out method is called but the saving into my custom table does not get called with in the call out method.
public class AutoSyncConnector {
public AutoSyncConnector()
{
}
public void Fire(string jsonToPost)
{
// 1. Authentication send the current session id so that request can be validated
String sessionId = UserInfo.getSessionId();
// 2. warp the request and post it to the configured end point
// This is how to get settings out for custom settings list
String connectorUrl = ASEndPoints__c.getValues('MainUrlEndPoint').autosync__MainSiteUrl__c;
CastlesMessageLog__c cd = new CastlesMessageLog__c();
cd.SentJson__c = 'before call out this is called';
insert cd;
AutoSyncConnector.CallOut(jsonToPost, connectorUrl);
CastlesMessageLog__c cd2 = new CastlesMessageLog__c();
cd2.SentJson__c = 'after call out this is called';
insert cd2;
}
public static void CallOut(String jsonToPost, String connectorUrl)
{
MakeCallout(jsonToPost,connectorUrl);
}
#future(callout=true)
public static void MakeCallout(String jsonToPost, String connectorUrl){
CastlesMessageLog__c cd = new CastlesMessageLog__c();
cd.SentJson__c = 'start inside before call out this is never called';
insert cd;
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setTimeout(120000);
// string authorizationHeader = 'Check I can add stuff to the header';
String sfdcConnectorUrl = connectorUrl + '/api/autosyncwebhook';
req.setEndpoint(sfdcConnectorUrl);
//req.setHeader('Authorization', authorizationHeader);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
req.setBody(jsonToPost);
h.send(req);
CastlesMessageLog__c cd2 = new CastlesMessageLog__c();
cd2.SentJson__c = 'end inside before call out this is never called';
insert cd2;
}
}
Go to Setup -> Monitoring -> Apex jobs. My gut feel is that you'll see lots of "uncommitted work pending" errors in there.
When you make any DML (insert/update/delete) you open a transaction with the database. If the next thing you'll do is a callout (which can have max timeout time 120 seconds) it'll mean you hold a lock on this record (or even whole table) for very long time. SF has no way of knowing whether the call will succeed or will have to be rolled back. So they protect the situation by banning such code right away ;)
Make callout first, then your DML.
Or make DML, call #future (that's the purpose, to switch to another thread, separate the contexts) and if the callout comes back with error - do whatever cleanup you'd consider a rollback (delete the record? update it to status = Sync failed? Send email to user / insert for him a Task to retry later?)
Related
We have a requirement to create a kind of user session. Our front end is react and backend is .net core 6 api and db is postgres.
When 1 user clicks on a delete button , he should not be allowed to delete that item when another user is already using that item and performing some actions.
Can you guys suggest me an approach or any kind of service that is available to achieve this. Please help
I would say dont make it too complicated. A simple approach could be to add the properties 'BeingEditedByUserId' and 'ExclusiveEditLockEnd' (datetime) to the entity and check these when performing any action on this entity. When an action is performed on the entity, the id is assigned and a timeslot (for example 10 minutes) would be assigned for this user. If any other user would try to perform an action, you block them. If the timeslot is expired anyone can edit again.
I have had to do something similar with Java (also backed by a postgres db)
There are some pitfalls to avoid with a custom lock implementation, like forgetting to unlock when finished, given that there is not guarantee that a client makes a 'goodbye, unlock the table' call when they finish editing a page, they could simply close the browser tab, or have a power outage... Here is what i decided to do:
Decide if the lock should be implemented in the API or DB?
Is this a distributed/scalable application? Does it run as just a single instance or multiple? If multiple, then you can not (as easily) implement an API lock (you could use something like a shared cache, but that might be more trouble than it is worth)
Is there a record in the DB that could be used as a lock, guaranteed to exist for each editable item in the DB? I would assume so, but if the app is backed by multiple DBs maybe not.
API locking is fairly easy, you just need to handle thread safety as most (if not all) REST/SOAP... implementations are heavily multithreaded.
If you implement at the DB consider looking into a 'Row Level Lock' which allows you to request a lock on a specific row in the DB, which you could use as a write lock.
If you want to implement in the API, consider something like this:
class LockManager
{
private static readonly object writeLock = new();
// the `object` is whatever you want to use as the ID of the resource being locked, probably a UUID/GUID but could be a String too
// the `holder` is an ID of the person/system that owns the lock
Dictionary<object, _lock> locks = new Dictionary<object, _lock>();
_lock acquireLock(object id, String holder)
{
_lock lok = new _lock();
lok.id = id;
lok.holder = holder;
lock (writeLock)
{
if (locks.ContainsKey(id))
{
if (locks[id].release > DateTime.Now)
{
locks.Remove(id);
}
else
{
throw new InvalidOperationException("Resource is already locked, lock held by: " + locks[id].holder);
}
}
lok.allocated = DateTime.Now;
lok.release = lok.allocated.AddMinutes(5);
}
return lok;
}
void releaseLock(object id)
{
lock (writeLock)
{
locks.Remove(id);
}
}
// called by .js code to renew the lock via ajax call if the user is determined to be active
void extendLock(object id)
{
if (locks.ContainsKey(id))
{
lock (writeLock)
{
locks[id].release = DateTime.Now.AddMinutes(5);
}
}
}
}
class _lock
{
public object id;
public String holder;
public DateTime allocated;
public DateTime release;
}
}
This is what i did because it does not depend on the DB or client. And was easy to implement. Also, it does not require configuring any lock timeouts or cleanup tasks to release locked items with expired locks on them, as that is taken care of in the locking step.
The problem I am running into is that I cannot get the API call to our servers to run when someone enters donations with batches. If the donation is entered individually, it works perfectly fine. But when a batch is used, an error is returned that says a future method cannot called from a future or batch method. So I uncommented "#future (callout=true)" above the method, and it creates the record, but the API call is never made. But if I leave it uncommented, we get the error.
Basically this is how the structure of the code is set up:
Apex trigger class triggers when a new donation is added (Pulls data relating to donation)
The trigger calls a class that has a method that will pull more data about the account and contact from the data pulled from the trigger.
That method then called another method (in the same class) that has an API call to our servers that will send the data to our servers for us to process
Here is the code:
Trigger:
trigger NewDonor on Donation (after insert) {
for(Donation d : Trigger.new)
{
if(d.Stage == 'Closed Won' || d.Stage == 'Received'){
//Get contacts account id
string accountId = d.AccountId;
decimal money = d.Amount;
string donation_amount = money.toPlainString();
string donation_id = d.Id;
ProcessDonor.SerializeJson(accountId, donation_amount, donation_id);
}
}
}
Class:
public class ProcessDonor {
String account;
String JSONString;
//Method to make http request
public static String jsonHandler(String json1, String json2, String account, String donoAmount, String donoId){
String JSONString1 = json1;
String JSONString2 = json2;
String accountId = account;
String donation_amount = donoAmount;
String donation_id = donoId;
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://urlToTerver');
request.setMethod('POST');
//Set headers
request.setHeader('Content-Type', 'application/json;charset=UTF-8');
request.setHeader('accountDataFields', JSONString1);
request.setHeader('contactDataFields', JSONString2);
request.setHeader('accountId', accountId);
request.setHeader('donationAmount', donation_amount);
request.setHeader('donationId', donation_id);
// Set the body as a JSON object
HttpResponse response = http.send(request);
// Parse the JSON response
if (response.getStatusCode() != 200) {
System.debug('The status code returned was not expected: ' +
response.getStatusCode() + ' ' + response.getBody());
return 'The status code returned was not expected: ' + response.getStatusCode() + ' ' + response.getStatus();
} else {
System.debug(response.getBody());
return response.getBody();
}
}
//Main method 2
//#future (callout=true)
public static void SerializeJson(String account, String amount, String donationId) {
String accountId = account;
String donationAmount = amount;
String donoId = donationId;
List<Account> accountQuery = Database.query('SELECT Id, Name, FirstGiftDate__c, LatestGiftDate__c, LifetimeDonationCount__c FROM Account WHERE Id = :accountId');
Decimal donationCount = accountQuery[0].LifetimeDonationCount__c;
Date latestGiftDate = accountQuery[0].LatestGiftDate__c;
Date firstGiftDate = accountQuery[0].FirstGiftDate__c;
if(donationCount == 1.00) {
System.debug(accountId);
System.debug(donationAmount);
//Make database query and set result to json
String JSONString1 = JSON.serialize(Database.query('SELECT Id, Contact__c, Name, Billing_Street, Billing_City, Billing_State, Billing_Postal_Code, EnvelopeSalutation__c, FirstGiftDate__c FROM Account WHERE Id = :accountId limit 1'));
String JSONString2 = JSON.serialize(Database.query('SELECT AccountId, Addressee__c, salutation FROM Contact WHERE AccountId = :accountId limit 1'));
String response = jsonHandler(JSONString1, JSONString2, accountId, donationAmount, donoId);
}
}
}
Maybe I just don't understand how batches fully work, but I am not sure how else to make this work.
Any help on this would be greatly appreciated!
The issue is that you are looping in the trigger and trying to make multiple future calls. Instead, move the loop to the future call. Loop through the records and make the callouts there. But be aware, there is a limitation on how many callous you can make. If the calls to your server don't have to happen in realtime, You might want to consider writing a batch class that would process all the donations at night.
Are you using Non-Profit Starter Pack (NPSP)? There's no standard object called Donation, NPSP renames Opportunities to Donations... That trigger looks suspicious (if it's Opportunities - it should say StageName, not Stage), you sure it compiled? And the way it's written it'll fire every time somebody edits a closed won opportunity. You probably want to run it only once, when stage changes. Or when some field "sent ok" is not set yet (and on successfull callout you'd set that field?)
"a future method cannot called from a future or batch method" - this suggests there's something more at play. For example a batch job (apex class with implements database.batchable in it) that inserts multiple donations -> calls trigger -> this calls #future -> problem. You shouldn't get that error from "normal" operation, even if you'd use Data Loader for bulk load -> trigger...
Or - do you update some field on donation on successful call? But at that point you're in #future, it will enter your trigger again, schedule another #future - boom headshot. It's bit crude but SF protects you from recursion. Did you post complete code? Do you think you have recursion in there?
The reason calls to external servers have to be asynchronous (#future etc) is that in a trigger you have a lock on the database table. SF can't block write access to this table for up to 2 minutes depending on a whim of 3rd party server. It has to be detached from main execution.
You can make 50 calls to #future method in single execution and each can have 100 HTTP callouts (unless they hit 120s combined max timeout), this should be plenty of room to do what you need.
Assuming worst case scenario, that you really have a batch job that inserts donations and this has side effect of making callouts - you won't be able to do it like that. You have few options to detach it.
Option 1: In trigger you can run System.isBatch() || System.isFuture() || System.isQueueable() and run current code only if it's all false. If we're in "normal" mode, not already asynchronous - you should be able to call the #future method and sendyour donation. This would sort situations where user manually creates one or you have "normal" load with Data Loader for example.
If one of these returns true - don't run the code. Instead have some other batch job that runs every hour? every 5 minutes? and "cleans". Picks donations that are closed won/received but don't have some hidden "sent ok" checkbox set and send just them.
Option 2 Similar but using Platform Events. It's bit fancier. Your trigger would raise an event (notification that can be read by other apex code, flows but also 3rd party systems), you'd write a trigger on events and send your callouts from that trigger. It'd be properly detached and it shouldn't matter whether it's called from UI, Data Loader or apex batch job. https://trailhead.salesforce.com/en/content/learn/modules/platform_events_basics/platform_events_subscribe might help
Im making a discord bot with JDA and I want to know how to wait for a message. Something like this
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
public class Listener extends ListenerAdapter {
public void onGuildMessageReceived(GuildMessageReceivedEvent event) {
String message = event.getMessage().getContentRaw();
boolean isBot = event.getAuthor().isBot();
// Check if message is not from a bot
if (!isBot) {
if (message.equalsIgnoreCase("hi")) {
event.getChannel().sendMessage("Hello, what's your name?").queue();
// Wait for second message
String name = event.getMessage().getContentRaw(); // Saving content from second message
event.getChannel().sendMessage("Your name is: " + name).queue();
}
}
}
}
User: Hi
Bot: Hello, what's your name?
User: sends name
Bot: Your name is: name
How do i get the second message?
As already told by Minn in the comment. You could use that method given in the thread. However, I'd recommend using the JDA-Utilities EventWaiter.
If you're using Maven or Gradle, use either one of these:
<!--- Place this in your repositories block --->
<repository>
<id>central</id>
<name>bintray</name>
<url>http://jcenter.bintray.com</url>
</repository>
<!--- Place this in your dependencies block --->
<dependency>
<groupId>com.jagrosh</groupId>
<artifactId>jda-utilities</artifactId>
<version>JDA-UTILITIES-VERSION</version> <!--- This will be the latest JDA-Utilities version. Currently: 3.0.4 --->
<scope>compile</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId> <!--- This will be your JDA Version --->
<version>JDA-VERSION</version>
</dependency>
For gradle it's much easier:
# Add this to the dependencies (What's inside this block, not the block itself.
dependencies {
compile 'com.jagrosh:jda-utilities:JDA-UTILITIES-VERSION'
compile 'net.dv8tion:JDA:JDA-VERSION'
}
# Same story for the repo's
repositories {
jcenter()
}
When this has loaded in, I'd recommend checking the ExampleBot main class. for adding your own stuff to it. Since I will only explain the basics of getting the EventWaiter to work.
This will be the main class of the bot to start with, when you run this, it will start the bot with these commands. ShutdownCommand and PingCommand are a part of the Utilities. So these will be added automaticly.
public static void main(String[] args) throws IOException, LoginException, IllegalArgumentException, RateLimitedException
{
// the first is the bot token
String token = "The token of your bot"
// the second is the bot's owner's id
String ownerId = "The ID of your user account"
// define an eventwaiter, dont forget to add this to the JDABuilder!
EventWaiter waiter = new EventWaiter();
// define a command client
CommandClientBuilder client = new CommandClientBuilder();
// sets the owner of the bot
client.setOwnerId(ownerId);
// sets the bot prefix
client.setPrefix("!!");
// adds commands
client.addCommands(
// command to say hello
new HelloCommand(waiter),
// command to check bot latency
new PingCommand(),
// command to shut off the bot
new ShutdownCommand());
// start getting a bot account set up
new JDABuilder(AccountType.BOT)
// set the token
.setToken(token)
// set the game for when the bot is loading
.setStatus(OnlineStatus.DO_NOT_DISTURB)
.setActivity(Activity.playing("loading..."))
// add the listeners
.addEventListeners(waiter, client.build())
// start it up!
.build();
}
Now, for the HelloCommand. You start by making a new class called "HelloCommand". In this class you will extend the "Command" portion of the utilities.
public class HelloCommand extends Command
{
private final EventWaiter waiter; // This variable is used to define the waiter, and call it from anywhere in this class.
public HelloCommand(EventWaiter waiter)
{
this.waiter = waiter; // Define the waiter
this.name = "hello"; // The command
this.aliases = new String[]{"hi"}; // Any aliases about the command
this.help = "says hello and waits for a response"; // Description of the command
}
#Override
protected void execute(CommandEvent event)
{
// ask what the user's name is
event.reply("Hello. What is your name?"); // Respond to the command with a message.
// wait for a response
waiter.waitForEvent(MessageReceivedEvent.class,
// make sure it's by the same user, and in the same channel, and for safety, a different message
e -> e.getAuthor().equals(event.getAuthor())
&& e.getChannel().equals(event.getChannel())
&& !e.getMessage().equals(event.getMessage()),
// respond, inserting the name they listed into the response
e -> event.reply("Hello, `"+e.getMessage().getContentRaw()+"`! I'm `"+e.getJDA().getSelfUser().getName()+"`!"),
// if the user takes more than a minute, time out
1, TimeUnit.MINUTES, () -> event.reply("Sorry, you took too long."));
}
}
Now, this is where we get to the jucy stuff. When using the event waiter. There are a few area's you need to check for.
// wait for a response
waiter.waitForEvent(MessageReceivedEvent.class,
// make sure it's by the same user, and in the same channel, and for safety, a different message
e -> e.getAuthor().equals(event.getAuthor())
&& e.getChannel().equals(event.getChannel())
&& !e.getMessage().equals(event.getMessage()),
// respond, inserting the name they listed into the response
e -> event.reply("Hello, `"+e.getMessage().getContentRaw()+"`! I'm `"+e.getJDA().getSelfUser().getName()+"`!"),
// if the user takes more than a minute, time out
1, TimeUnit.MINUTES, () -> event.reply("Sorry, you took too long."));
Explained in more detail:
waiter.waitForEvent(GuildMessageReceivedEvent.class, // What event do you want to wait for?
p -> p.getAuthor().equals(message.getAuthor()) && p.getChannel().equals(message.getChannel()), // This is important to get correct, check if the user is the same as the one who executed the command. Otherwise you'll get a user who isn't the one who executed it and the waiter responds to it.
run -> {
// Run whatever stuff you want here.
}, 1, // How long should it wait for an X amount of Y?
TimeUnit.MINUTES, // Y == TimeUnit.<TIME> (Depending on the use case, a different time may be needed)
() -> message.getChannel().sendMessage("You took longer then a minute to respond. So I've reverted this message.").queue()); // If the user never responds. Do this.
This should be the BASICS to using the EventWaiter. This may depend on your use case. And it can be done by only using the eventwaiter class as well.
I am trying to send all the table records in email body though send mail task
My flow:
I uses SQL execute task to fetch the rows from table and stored in an object
Uses for each loop container and in that I use a script task to store the rows in an EmailMessage body
I used Send mail task to send the email
I am only getting last records of the table in the message body.
Please guide me how to send all the table data at once in a message body
Actaul flow
error
I think I would take a slightly different approach and recurse the recordset directly in the script task but this looks like it would work too. I would guess that your problem is that you overwrite User::EmailMessage at every iteration. You say you get last few records but looking at your code I would think you will get 1 unless you uncomment the IF (varcollection == string.empty) in which case you might get more.
Anyway, the main offending problem is
varCollection["User::EmailMessage"].Value = header;
That resets your EmailMessage body to the header row any time it is called.
Edit: Adding as per your comment to reset message at every new shipment number. Add another package variable PrevShippingNum which will hold the previous looped number to test if it is the same or has changed. Make sure that this variable is listed as ReadWriteVariable to the script task. then modify your script to include something like this:
Dts.VariableDispenser.GetVariables(ref varCollection);
bool newMessage = (varCollection["User::PrevShippingNum"].value != varCollection["User::ShppingNum"].value) ? true : false;
if (string.IsNullOrWhiteSpace(varCollection["User::EmailMessage"].Value.ToString()) || newMessage)
{
varCollection["User::EmailMessage"].Value = string.Format("{0}........");
}
varCollection["User::EmailMessage"].Value += string.Format("{0}......");
The positive about this is you can also use your new variable as a constraint to determine when to send email task.
A different Approach:
Note pretty big edit to add new sub to take care of sending emails per ShippingNum:
Way I would proceed pass the recordset variable you are using to a script task and let it do the email message building. Just to be clear this is to replace your foreach loop! here is some code adapted from one of my solutions:
Add Reference to System.Data.DataSetExtensions
Add following namespaces:
using System.Data.OleDb;
using System.Net.Mail;
using System.Linq;
using System.Collections.Generic;
private void Main()
{
//using System.Data.OleDb;
OleDbDataAdapter oleAdapter = new OleDbDataAdapter();
DataTable dt = new DataTable();
oleAdapter.Fill(dt, Dts.Variables["User::OleDbRecordSetVar"].Value);
//build header row
string headerRow = string.Format("{0}........", "ShippingNum ....");
//get distinct shippingNums
var shippingNums = (from DataRow dr in dt.Rows
select (int)dr["ShppingNum"]).Distinct();
//Now Build the Differnt Emails
foreach (var num in shippingNums)
{
string emailBody = headerRow;
List<DataRow> emailLines = (from DataRow dr in dt.Rows
where (int)dr["ShippingNum"] == num
select dr).ToList<DataRow>();
foreach (DataRow line in emailLines)
{
emailBody += string.Format("{0}....", line["ColumnName1"].ToString(), line["ColumnName2"].ToString());
}
SendEmail(emailBody);
}
Dts.TaskResult = (int)ScriptResults.Success;
}
private void SendEmail(string messageBody)
{
//get the smtp server address from the SSIS connection manger
ConnectionManager smtpConnectionManager = Dts.Connections["Name Of SMTP Connection Manager"];
//note this is for trusted authentication if you want to use a username and password you will have to do some discovery
SmtpClient emailClient = new SmtpClient(smtpConnectionManager.Properties["SmtpServer"].GetValue(smtpConnectionManager).ToString());
MailMessage email = new MailMessage();
email.Priority = MailPriority.Normal;
email.IsBodyHtml = false; //change to true if you send html
//can hard code addresses if you desire I use variables to make it more flexible
email.From = new MailAddress(Dts.Variables["User::FromAddress"].Value.ToString());
email.To.Add(Dts.Variables["User::ToAddress"].Value.ToString());
email.Body = messageBody;
emailClient.Send(email);
}
I've done considerable research it doesn't work for me for some reason I don't even get into the situation where I get "zombie" instances(like existing in session but not in db anymore - I wish I had this situation) since remove() is not propagated into database. The child record continues to exist inside my db.
Consider two tables: User and Token(FK_User_Id) , relationship one-to-many respectively
Inside DAO final code which doesn't work:
public void deleteToken(Token t) {
Token b = em.merge(t);
em.remove(b); // does nothing
em.flush();
}
Inside controller:
Object obj;
Token token;
obj = tokenService.getByString(urlToken); // query returning detached object
User user;
if (obj != null) {
token = (Token) obj; // Detached, query is "fetch" overriding lazy init, so token contains its user parent
user = token.getUser(); // simply get the user to which this token belongs
if ((token.getExpiryDate().compareTo(new GregorianCalendar()
.getTime())) == 1) {
userService.activateUser(user); // user gets merged inside and its relevant property is changed and propagated to db successfully
tokenService.deleteToken(token); // this line calls DAO method described in snippet above - it fails to delete the token, but no error or exception - record simply stays in database
model.addAttribute("activationSuccess", "true");
}...
User entity:
public class User {
public static final String FIND_USER_BY_TOKEN = "findUserByToken";
public static final String FIND_USER_BY_USERNAME = "findUserByUsername";
public static final String FIND_USER_BY_EMAIL = "findUserByEmail";
#Id
#GeneratedValue
private Long id;
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval=true)
private List<Token> tokens = new ArrayList(); ...
Token entity:
#Entity
#Table(name = "token")
#NamedQueries({
#NamedQuery(name=Token.FIND_TOKEN_BY_STRING, query="Select t From Token T where t.tokenString=:string")
})
public class Token {
public static final String FIND_TOKEN_BY_STRING = "findTokenById";
#Id
#GeneratedValue
Long id;
#ManyToOne(optional=true)
private User user; ...
Now if I call something like:
User c = b.getUser();
em.remove(c);
Inside DAO snippet, it deletes both token and user which is not what I want. Only token must be deleted.
Basically what I am trying to do is to retrieve Token by string property and along with it the user parent which owns this token. Then I retrieve this user from token and change some property for the user(changes propagated into db). All successful so far. Then I want to delete the token as a final operation but in a way that user will not be deleted.
I am on my fifth hour on this please help... I managed to setId to null for the token(propagated into db), but it only gets me the point where token no longer has owner but still persists in database. To delete it I tried to merge the Token inside DAO with null which threw me exception. Then I tried to set Tokens list value to null inside User(which was retrieved from this token) and it also through me exception.
How I am supposed to delete child token entity which I retrieved with its parent but keep parent present in db?
SQL Hibernate log shows no delete query...after passing remove method.
Thanks,
If you do not dereference the token from User's list of tokens, cascade persist (you have cascade all set which includes persist) will cause the token to be resurrected at some point. You must clear all references to the child, especially ones marked cascade persist when you want to remove entities. Something like:
if ((token.getExpiryDate().compareTo(new GregorianCalendar()
.getTime())) == 1) {
user.getTokens().remove(token);
userService.activateUser(user);
//tokenService.deleteToken(token); //no longer needed due to orphanremoval being true
model.addAttribute("activationSuccess", "true");
...