Регистрация  |  Вход

Как триггер сделать универсальным для List

Требование к триггеру: до вставки(insert) записей проверить, есть ли запись(и) с таким Passport Number, и если есть запретить вставку записи.
Реализация триггера для 1 записи:

trigger CarOwnersTrigger on Car_Owner__c (before insert) {
List<Car_Owner__c> carOwnerList = [SELECT Passport_Number__c FROM Car_Owner__c WHERE Passport_Number__c =: Trigger.new[0].Passport_Number__c];
if(!carOwnerList.isEmpty()) {
Trigger.new[0].addError('TRIGGER: Incorrect data');
}
}

Я хочу переделать так, чтобы триггер срабатывал если записи вставляются списком(List).
Вопросы:
1. Где можно так почитать, чтобы разобраться в Bulk Apex Triggers (Trailhead я уже читал, разобраться с этим вопросом там сложно)?
2. Как это правильно сделать? Нужно ли делать List<List<Car_Owner__c>> и затем циклом перебирать по внешнему списку?

trigger CarOwnersTrigger on Car_Owner__c (before insert) {
List<Car_Owner__c> availablePassports = [SELECT Passport_Number__c FROM Car_Owner__c];

for(Car_Owner__c coNew: Trigger.New) {
for(Car_Owner__c coAva: availablePassports) {
if(coNew.Passport_Number__c == coAva.Passport_Number__c) {
coNew.addError('TRIGGER: Incorrect data');
}
}
}
}

Требование к триггеру: до вставки(insert) записей проверить, есть ли запись(и) с таким Passport Number, и если есть запретить вставку записи.
Реализация триггера для 1 записи:
[color=purple]
trigger CarOwnersTrigger on Car_Owner__c (before insert) {
    List<Car_Owner__c> carOwnerList = [SELECT Passport_Number__c FROM Car_Owner__c WHERE Passport_Number__c =: Trigger.new[0].Passport_Number__c];
    if(!carOwnerList.isEmpty()) {
        Trigger.new[0].addError('TRIGGER: Incorrect data');
    }
}
[/color]
Я хочу переделать так, чтобы триггер срабатывал если записи вставляются списком(List).
Вопросы:
1. Где можно так почитать, чтобы разобраться в Bulk Apex Triggers (Trailhead я уже читал, разобраться с этим вопросом там сложно)?
2. Как это правильно сделать? Нужно ли делать List<List<Car_Owner__c>> и затем циклом перебирать по внешнему списку?
[color=green]
trigger CarOwnersTrigger on Car_Owner__c (before insert) {
    List<Car_Owner__c> availablePassports = [SELECT Passport_Number__c FROM Car_Owner__c];
    
    for(Car_Owner__c coNew: Trigger.New) {
        for(Car_Owner__c coAva: availablePassports) {
            if(coNew.Passport_Number__c == coAva.Passport_Number__c) {
            	coNew.addError('TRIGGER: Incorrect data');
            }
        }
    }
}
[/color]

можно сделать так(говнокодец, так сказать)

trigger CarOwnersTrigger on Car_Owner__c (before insert) { 
Set<String> uniquePassportNumbers = new Set<String>();
for(Car_Owner__c o: [SELECT Passport_Number__c FROM Car_Owner__c]) {
if (String.isNotBlank(o.Passport_Number__c)) {
uniquePassportNumbers.add(o.Passport_Number__c);
}
}
for(Car_Owner__c n: Trigger.New) {
if (String.isNotBlank(n.Passport_Number__c) && uniquePassportNumbers.contains(n.Passport_Number__c)) {
n.addError('TRIGGER: Incorrect data');
}
}
}

а можно сделать немного по другому, сначала пробежаться по Trigger.New, собрать номера паспортов, а потом сделать поиск по этим номерам - но скорее всего номера паспортов это текст, и сделать это адекватнонормально не получится.

да и в принципе, проверять на уникальность такой записи не имеет смысла в триггере

имеет смыслсделать поле Passport_Number__c уникальным

можно сделать так(говнокодец, так сказать)
[code]
trigger CarOwnersTrigger on Car_Owner__c (before insert) { 
	Set<String> uniquePassportNumbers = new Set<String>();
	for(Car_Owner__c o: [SELECT Passport_Number__c FROM Car_Owner__c]) { 
			if (String.isNotBlank(o.Passport_Number__c)) {
				uniquePassportNumbers.add(o.Passport_Number__c);
			}
	}
	for(Car_Owner__c n: Trigger.New) { 
		if (String.isNotBlank(n.Passport_Number__c) && uniquePassportNumbers.contains(n.Passport_Number__c)) {
				n.addError('TRIGGER: Incorrect data'); 
		} 
	} 
} [/code]
а можно сделать немного по другому, сначала пробежаться по Trigger.New, собрать номера паспортов, а потом сделать поиск по этим номерам - но скорее всего номера паспортов это текст, и сделать это адекватнонормально не получится.

да и в принципе, проверять на уникальность такой записи не имеет смысла в триггере

имеет смыслсделать поле Passport_Number__c уникальным

Maxim Elets
а можно сделать немного по другому, сначала пробежаться по Trigger.New, собрать номера паспортов, а потом сделать поиск по этим номерам

Именно так и нужно.
Делать такое нельзя
for(Car_Owner__c o: [SELECT Passport_Number__c FROM Car_Owner__c]) {

а вдруг Car Owners будет 100к записей, а нам то и надо будет всего проверить 2-3 Passport Number.
Maxim Elets
- но скорее всего номера паспортов это текст, и сделать это адекватнонормально не получится.

А почему не получится? Скорее всего Passport Number будет Text(255) и по нему искать вполне возможно.
Maxim Elets
да и в принципе, проверять на уникальность такой записи не имеет смысла в триггере
имеет смыслсделать поле Passport_Number__c уникальным

Вот тут согласен. Лучше сделать поле уникальным.

[quote="Maxim Elets"]а можно сделать немного по другому, сначала пробежаться по Trigger.New, собрать номера паспортов, а потом сделать поиск по этим номерам[/quote]
Именно так и нужно.
Делать такое нельзя
[code]for(Car_Owner__c o: [SELECT Passport_Number__c FROM Car_Owner__c]) {[/code]
а вдруг Car Owners будет 100к записей, а нам то и надо будет всего проверить 2-3 Passport Number.
[quote="Maxim Elets"] - но скорее всего номера паспортов это текст, и сделать это адекватнонормально не получится.[/quote]
А почему не получится? Скорее всего Passport Number будет Text(255) и по нему искать вполне возможно.
[quote="Maxim Elets"]да и в принципе, проверять на уникальность такой записи не имеет смысла в триггере
имеет смыслсделать поле Passport_Number__c уникальным[/quote]
Вот тут согласен. Лучше сделать поле уникальным.

Dmitry Shnyrev
а вдруг Car Owners будет 100к записей, а нам то и надо будет всего проверить 2-3 Passport Number.

там стоит пометка - говнокодец :)

[quote="Dmitry Shnyrev"]а вдруг Car Owners будет 100к записей, а нам то и надо будет всего проверить 2-3 Passport Number. [/quote]
там стоит пометка - говнокодец :)

Dmitry Shnyrev
А почему не получится? Скорее всего Passport Number будет Text(255) и по нему искать вполне возможно.

только если собирать строку руками : WHERE PassportNUmber__c = '1' OR '2' и тд

[quote="Dmitry Shnyrev"]А почему не получится? Скорее всего Passport Number будет Text(255) и по нему искать вполне возможно. [/quote]
только если собирать строку руками : WHERE PassportNUmber__c = '1' OR '2' и тд


WHERE PassportNUmber__c IN :listOfString
?

:D 
WHERE PassportNUmber__c IN :listOfString
?

Балкификация АПЕКС тригеров - это базовая тема, достойная того, чтобы ее погуглить и изучить.

дело в том, что тригеры делаются не для простых бизнес-логик случаев, а для более сложных, там где, например, нужно что-то покверить на другом объекте или чтобы что создать новое (впрочем последнее можно уже делать и с П Билдером).

и все бы ничего, но записи могут прийти в тригер пачками вплоть до 200 шт, и если для каждой записи ты будет кверить нужную инфу отдельно, то быстро стукнешь лимиты.

поэтому действовать прямолинейно здесь не получится, и приходится кверить все требуемым данные одним-несколькими объединенными запросами, а не для каждой записи пришедшей в тригер отдельно.

то есть вначале проходишься циклом по пришедшим записям и собираешь инфу о том, что и как кверить.

потом кверишь, и создаешь Мэпы (если там несколько объектов включено), связывающие эти записи (иначе как ты поймешь, какай инфа к чему принадлежит?)

затем в основном цикле по пришедшим записям ты выполняешь требуемую бизнес-логику, использую те самые Мэпы.

и если создаются новые записи, или апдатируются записи не являющиеся пришедшими в тригер - то такие записи собираются в Листы, которые после цикла создаются или апдатируются одной ДМЛ операцией (а не многими, если бы это делалось в цикле)

вот и все.

есть много нюансов, например с тестами. может так случится, что подготовка требуемых данных работает правильно только в случае, если в тригер пришла одна запись, а вот если там пришло несколько - то там баг (неправильно Мапы создаются). И если твой тест проверяет правильность выполнения логики только на одной записи, то этот баг может остаться незамеченным. Нужно тестить логику на нескольких записей, отправляемых в тригер одновременно. Да, обычно тригеры тестятся на 200 записей, это помогает проверить лимиты, но не факт что такой балк-тест проверяет еще и логику (так как это для этого нужно создавать разные требуемые данные для разных записей)


я думаю, что в твоем случае, тригер простой, так как там нет сложной подготовки данных.
(1)собираешь в цикле пришедшие паспорта
(2) кверишь существующие записи по паспортам
(3) если ничего не выкверилось, то все свободны.

а вот дальше интереснее. Если тригер работает в все-или-ничего режиме и n.addError вызывает полный откат, то зачем вообще выяснять, какая запись имеет проблему, ведь все равно ни одна не будет создана?

но все же лучше пройтись по списку пришедших записей и повесить n.addError на вызвавшую проблему, для удобства пользователя
а если там несколько проблемных записей, то я не знаю можно ли с помощью n.addError собрать несколько проблем, или в мессадж уходит только первая или последняя проблема, или всеже таким образом можно собрать инфу по всем проблемным записям? если нет, то придется сначала собирать проблемы, а потом выводить их одним n.addError

и вот что совсем интересно. А работает ли тригер во все-или-ничего режиме в случае с мульти-записями? скорее всего так и есть, я не помню. но если так и есть, то как быть если требования такие: из всех пришедших на инсерт записей, выбрать БЕЗпроблемные и создать их, а другие НЕ создавать, выяснить в чем проблема, и дать знать пользователю с какой записью и в чем проблема. Т.е. вопрос в том как выборочно ПРЕДОТВРАТИТЬ создание записи в бефо инсерт тригере, не создав проблем другим создаваемым записям?

Балкификация АПЕКС тригеров - это базовая тема, достойная того, чтобы ее погуглить и изучить.

дело в том, что тригеры делаются не для простых бизнес-логик случаев, а для более сложных, там где, например, нужно что-то покверить на другом объекте или чтобы что создать новое (впрочем последнее можно уже делать и с П Билдером).

и все бы ничего, но записи могут прийти в тригер пачками вплоть до 200 шт, и если для каждой записи ты будет кверить нужную инфу отдельно, то быстро стукнешь лимиты.

поэтому действовать прямолинейно здесь не получится, и приходится кверить все требуемым данные одним-несколькими объединенными запросами, а не для каждой записи пришедшей в тригер отдельно.

то есть вначале проходишься циклом по пришедшим записям и собираешь инфу о том, что и как кверить.

потом кверишь, и создаешь Мэпы (если там несколько объектов включено), связывающие эти записи (иначе как ты поймешь, какай инфа к чему принадлежит?)

затем в основном цикле по пришедшим записям ты выполняешь требуемую бизнес-логику, использую те самые Мэпы.

и если создаются новые записи, или апдатируются записи не являющиеся пришедшими в тригер - то такие записи собираются в Листы, которые после цикла создаются или апдатируются одной ДМЛ операцией (а не многими, если бы это делалось в цикле)

вот и все.

есть много нюансов, например с тестами. может так случится, что подготовка требуемых данных работает правильно только в случае, если в тригер пришла одна запись, а вот если там пришло несколько - то там баг (неправильно Мапы создаются). И если твой тест проверяет правильность выполнения логики только на одной записи, то этот баг может остаться незамеченным. Нужно тестить логику на нескольких записей, отправляемых в тригер одновременно. Да, обычно тригеры тестятся на 200 записей, это помогает проверить лимиты, но не факт что такой балк-тест проверяет еще и логику (так как это для этого нужно создавать разные требуемые данные для разных записей)


я думаю, что в твоем случае, тригер простой, так как там нет сложной подготовки данных.
(1)собираешь в цикле пришедшие паспорта
(2) кверишь существующие записи по паспортам
(3) если ничего не выкверилось, то все свободны.

а вот дальше интереснее. Если тригер работает в все-или-ничего режиме и n.addError вызывает полный откат, то зачем вообще выяснять, какая запись имеет проблему, ведь все равно ни одна не будет создана?

но все же лучше пройтись по списку пришедших записей и повесить n.addError на вызвавшую проблему, для удобства пользователя
а если там несколько проблемных записей, то я не знаю можно ли с помощью n.addError собрать несколько проблем, или в мессадж уходит только первая или последняя проблема, или всеже таким образом можно собрать инфу по всем проблемным записям? если нет, то придется сначала собирать проблемы, а потом выводить их одним  n.addError

и вот что совсем интересно. А работает ли тригер во все-или-ничего режиме в случае с мульти-записями? скорее всего так и есть, я не помню. но если так и есть, то как быть если требования такие: из всех пришедших на инсерт записей, выбрать БЕЗпроблемные и создать их, а другие НЕ создавать, выяснить в чем проблема,  и дать знать пользователю с какой записью и в чем проблема. Т.е. вопрос в том как выборочно ПРЕДОТВРАТИТЬ создание записи в бефо инсерт тригере, не создав проблем другим создаваемым записям?

Dmitry Shnyrev
:D
WHERE PassportNUmber__c IN :listOfString
?

стринг так не работает :)

[quote="Dmitry Shnyrev"]:D 
WHERE PassportNUmber__c IN :listOfString
?[/quote]
стринг так не работает :)

че? реально? Пошел проверять

че? реально? Пошел проверять :D 

У меня работает

List<String> l = new List<String>{
'Apex CPU time limit exceeded',
'invalid ID field: null'
};

List<Log__c> logs = [SELECT Id FROM Log__c WHERE Summary__c IN :l];
SYSTEM.DEBUG('XXXXX: '+logs.size());

XXXXX: 10

У меня работает :)

[code]
List<String> l = new List<String>{
    'Apex CPU time limit exceeded',
    'invalid ID field: null'
};

List<Log__c> logs = [SELECT Id FROM Log__c WHERE Summary__c IN :l];
SYSTEM.DEBUG('XXXXX: '+logs.size());
[/code]

[b]XXXXX: 10[/b]

Пойду и я перепроверю)
Пару недель назад это все еще не работало

UPD: действительно работает, надо будет зарефакторить весь проект, потому что много мест где наши бэкендщики(не я) собирали строку xD

Пойду и я перепроверю)
Пару недель назад это все еще не работало

UPD: действительно работает, надо будет зарефакторить весь проект, потому что много мест где наши бэкендщики(не я) собирали строку xD

Да оно всю жизнь работает. Сколько себя помню использовал эту конструкцию

Да оно всю жизнь работает. Сколько себя помню использовал эту конструкцию :)

Оно не работает если у тебя филд Long Text или Rich Text

Оно не работает если у тебя филд Long Text или Rich Text

Dmitry Shnyrev
Да оно всю жизнь работает. Сколько себя помню использовал эту конструкцию :)

а я вот у нас в проекте не помню такого вообще, все всегда собирали полностью строку
вот и сижу и думаю - какого хера)

[quote="Dmitry Shnyrev"]Да оно всю жизнь работает. Сколько себя помню использовал эту конструкцию :)[/quote]
а я вот у нас в проекте не помню такого вообще, все всегда собирали полностью строку
вот и сижу и думаю - какого хера)

Может у вас используют поделки в роде fflib Query Builder а не SOQL напрямую?

Может у вас используют поделки в роде fflib Query Builder а не SOQL напрямую?

Поэтому и строят SOQL динамически.

ХОТЯ даже в динамический SOQL можно передать :listOfString и он должен подхватить сам лист.

Поэтому и строят SOQL динамически.

ХОТЯ даже в динамический SOQL можно передать :listOfString и он должен подхватить сам лист.

Dmitry Shnyrev
Может у вас используют поделки в роде fflib Query Builder а не SOQL напрямую?

у нас много всякого говна бесполезного xD

[quote="Dmitry Shnyrev"]Может у вас используют поделки в роде fflib Query Builder а не SOQL напрямую?[/quote]
у нас много всякого говна бесполезного xD