Ведение журнала SQL
Есть два способа просмотра вывода SQL из запросов; добавление logSql = true
в DataSource.groovy и настройка регистраторов Log4j. Подход Log4j намного более гибок, поскольку он не просто создает дамп в стандартный вывод, его можно направить в файл или другое приложение, удобно включить и отключить. Но оказывается, что logSql
SQL консоли легко переключать. Получите ссылку на sessionFactory
компонент sessionFactory
(например, используя внедрение зависимостей с def sessionFactory
) и включите его с помощью
1
|
sessionFactory.settings.sqlStatementLogger.logToStdout = true |
и покончить с
1
|
sessionFactory.settings.sqlStatementLogger.logToStdout = false |
stacktrace.log
Файл stacktrace.log становился очень большим, и они хотели сконфигурировать его для использования плавающего файла appender. Казалось, достаточно просто, но это заняло гораздо больше времени, чем я ожидал. Хитрость в том, чтобы создать аппендер с именем 'stacktrace'
; логика Grails, которая анализирует DSL Log4j, ищет существующий appender и использует его, а также настраивает стандартный по умолчанию, если он еще не настроен. Итак, вот тот, который настраивает RollingFileAppender
с максимум 10 файлами, каждый размером до 10 МБ, и со стандартным шаблоном макета. Кроме того, он включает логику, позволяющую определить, развернут ли он в Tomcat, чтобы он мог выполнять запись в папку журналов Tomcat или в целевую папку, если вы используете run-app.
Если вы развертываете в другом контейнере, измените расчет каталога журнала соответствующим образом.
01
02
03
04
05
06
07
08
09
10
11
|
appenders { String logDir = grails.util.Environment.warDeployed ? System.getProperty( 'catalina.home' ) + '/logs' : 'target' rollingFile name: 'stacktrace' , maximumFileSize: 10 * 1024 * 1024 , file: '$logDir/stacktrace.log' , layout: pattern( conversionPattern: '' %d [%t] %-5p %c{ 2 } %x - %m%n '' ), maxBackupIndex: 10 } |
Динамическое свойство fooId
Во множестве к одному, где у вас есть поле Foo foo
(или static belongsTo = [foo: Foo]
которое вызывает добавление поля ‘foo’), вы можете получить доступ к его внешнему ключу с fooId
свойства dynamic fooId
. Это можно использовать несколькими способами. Так как подобные ссылки по умолчанию являются ленивыми, проверка, существует ли пустая ссылка с использованием foo != null
включает загрузку всего экземпляра из базы данных. Но проверка fooId != null
не требует доступа к базе данных.
Другие запросы или обновления, которым действительно нужен только внешний ключ, будут дешевле, если использовать fooId
. Например, чтобы установить ссылку в другом экземпляре, вы обычно используете такой код:
1
2
|
bar2.foo = bar1.foo bar2.save() |
Но вы можете использовать метод load
1
2
|
bar2.foo = bar1.fooId ? Foo.load(bar1.fooId) : null bar2.save() |
и избегайте загрузки экземпляра Foo, просто чтобы установить его внешний ключ во втором экземпляре, а затем отбросить его.
Удаление по id тоже дешевле; Обычно вы используете get
для загрузки экземпляра и вызова его метода delete
, но извлечение всего экземпляра не требуется. Вы можете сделать это вместо этого:
1
|
Foo.load(bar.fooId).delete() |
СУХИЕ ограничения
Вы можете использовать метод importFrom
внутри блока constraints
в классе домена, чтобы избежать повторения ограничений. Вы можете импортировать все ограничения из другого класса домена:
1
2
3
4
5
|
static constraints = { someProperty nullable: true ... importFrom SomeOtherDomainClass } |
и необязательно использовать свойства include
и / или exclude
для использования подмножества:
1
2
3
4
5
|
static constraints = { someProperty nullable: true ... importFrom SomeOtherDomainClass, exclude: [ 'foo' , 'bar' ] } |
Flush слушатель события
Они наблюдали странное поведение, когда коллекции, которые не были явно изменены, изменялись и сохранялись, вызывая StaleObjectStateException
. Неясно, что вызвало такое поведение, поэтому я предложил зарегистрировать Hibernate FlushEventListener
для регистрации состояния грязных экземпляров и коллекций во время каждого сброса:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package com.burtbeckwith.blog import org.hibernate.HibernateException import org.hibernate.collection.PersistentCollection import org.hibernate.engine.EntityEntry import org.hibernate.engine.PersistenceContext import org.hibernate.event.FlushEvent import org.hibernate.event.FlushEventListener class LoggingFlushEventListener implements FlushEventListener { void onFlush(FlushEvent event) throws HibernateException { PersistenceContext pc = event.session.persistenceContext pc.entityEntries.each { instance, EntityEntry value -> if (instance.dirty) { println 'Flushing instance $instance' } } pc.collectionEntries.each { PersistentCollection collection, value -> if (collection.dirty) { println 'Flushing collection ' $collection.role ' $collection' } } } } |
В этом случае недостаточно использовать стандартную карту hibernateEventListeners
(описанную в документации здесь ), поскольку этот подход добавляет ваших слушателей в конец списка, и этот слушатель должен быть в начале. Поэтому вместо этого используйте этот код в BootStrap.groovy, чтобы зарегистрировать его:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
import org.hibernate.event.FlushEventListener import com.burtbeckwith.blog.LoggingFlushEventListener class BootStrap { def sessionFactory def init = { servletContext -> def listeners = [ new LoggingFlushEventListener()] def currentListeners = sessionFactory.eventListeners.flushEventListeners if (currentListeners) { listeners.addAll(currentListeners as List) } sessionFactory.eventListeners.flushEventListeners = listeners as FlushEventListener[] } } |
«Только для чтения» объекты и сеансы
Метод read
был добавлен в Grails некоторое время назад, и он работает как get
за исключением того, что он помечает экземпляр как доступный только для чтения в Hibernate Session. На самом деле это не только для чтения, но если он будет изменен, он не будет кандидатом на автоматическую очистку с использованием грязного обнаружения. Но вы можете явно вызвать save()
или delete()
и действие будет выполнено успешно.
Это может быть полезно во многих отношениях, и, в частности, оно более эффективно, если вы не будете изменять экземпляр, поскольку Hibernate не будет сохранять копию исходных данных базы данных для грязной проверки во время сброса, поэтому каждый экземпляр будет использовать около половины памяти, что было бы иначе.
Одним из ограничений метода read
является то, что он работает только для экземпляров, загружаемых индивидуально по id. Но есть и другие подходы, которые влияют на несколько экземпляров. Одним из них является сделать весь сеанс только для чтения:
1
|
session.defaultReadOnly = true |
Теперь все загруженные экземпляры будут по умолчанию доступны только для чтения, например, экземпляры из критериев запросов и поиска.
Удобным способом доступа к сеансу является метод withSession
в произвольном классе домена:
1
2
3
|
SomeDomainClass.withSession { session -> session.defaultReadOnly = true } |
Редко, что весь сеанс будет доступен только для чтения. Вы можете установить результаты запроса отдельных критериев только для чтения с setReadOnly
метода setReadOnly
:
1
2
3
4
5
6
7
|
def c = Account.createCriteria() def results = c { between( 'balance' , 500 , 1000 ) eq( 'branch' , 'London' ) maxResults( 10 ) setReadOnly true } |
Одним существенным ограничением этого метода является то, что на присоединенные коллекции не влияет состояние только для чтения экземпляра-владельца (и, похоже, нет способа настроить коллекцию таким образом, чтобы она игнорировала изменения для каждого экземпляра).
Подробнее об этом читайте в документации по Hibernate.
Ссылка: Материал, которому я научился консультироваться у нашего партнера JCG Берта Беквита в блоге « Армия солипсистов» .